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 krate CLI with run, version, doctor, and manifest commands.
  • A Wasmtime based runtime that loads WebAssembly components.
  • Phase 2 UAPI slices for io, fs, net, time, and locale.
  • UCap manifests, launch grants, policy checks, and grant logs.
  • Sample apps for krate-clock, krate-cat, and krate-curl.
  • CI and evidence scripts for samples, UCap enforcement, language variants, adapter boundaries, benchmarks, and exit readiness.
  • The Phase 3 GUI path: ui/gfx/audio WIT 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 the krate-hello-gui sample (import-pure, runs on all three OSes, sh scripts/demo-hello-gui.sh).
  • The agent surface: krate_runtime::embed, krate run --json (schema krate.run.v1), and krate-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-notes flagship (windows and first widgets exist; the rest of the surface does not yet).
  • Graphics (gfx) and audio beyond honest unsupported stubs; sensors, identity, and production app lifecycle APIs.
  • A .krate bundle 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 originRuns on
Windows appWindows, Linux, ChromeOS, Android, macOS, iOS
Linux appWindows, Linux, ChromeOS, Android, macOS, iOS
Web appWindows, Linux, ChromeOS, Android, macOS, iOS
Android appWindows, Linux, ChromeOS, Android, macOS, iOS
macOS appWindows, Linux, ChromeOS, Android, macOS, iOS
iOS appWindows, 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 problemMiddle layerWhat changed
Many CPUsLLVM IROne compiler front end can reach many chips.
Many serversJVM and .NET bytecodeOne backend app can run on many OSes.
Many browsersHTML, CSS, and JSOne site can reach almost every device.

Krate tries to bring that pattern to native apps:

  1. WebAssembly is the portable program format.
  2. UAPI is the standard app API.
  3. UCap is the permission model.
  4. Host adapters translate Krate calls into native OS calls.
  5. 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:

AreaTarget
HostsWindows, macOS, Linux, iOS, Android, and web where possible
App format.krate bundle
RuntimeFast cold start and predictable memory use
APIsFiles, network, time, locale, UI, graphics, sensors, identity
PermissionsClear grants instead of silent host access
Developer flowNew project to running app in about a minute
Real proofParkSure 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 .wasm component 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, --json reports, MCP server) is complete. The original contract-layer note follows: the first gui world and the ui, gfx, and audio WIT 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

#PhaseGoalEstimateStatus
0FoundationMake the project real enough to work in public.Done enough for development; external items pendingMostly done
1Runtime proofRun one WASM component on Linux, macOS, and Windows.DoneEngineering done
2UAPI v0.1Build useful CLI APIs and sample apps.in progressActive; exit evidence in progress
3Desktop UIRun one GUI app on Windows, macOS, and Linux.est. 6 to 10 weeksAbout half done: real windows on all three OSes, native macOS widgets, first drawn pass, agent surface complete
4Mobile hostsRun the same app on iOS and Android.est. 8 to 12 weeksPlanned
5Developer SDKMake project creation, debug, and packaging smooth.est. 6 to 10 weeksPlanned
6DistributionAdd bundles, signing, updates, and identity.est. 8 to 12 weeksPlanned
7v1.0 hardeningMigrate a real app and clean up for public launch.est. after Phase 6Planned

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:

  1. 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.
  2. 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 NSButton and NSTextField, and receives the click back — end-to-end through the permission layer and the runtime dispatcher. It proves the riskiest architectural bet first.
  3. 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

  1. The runtime can execute one component on Linux, macOS, and Windows.
  2. The Phase 2 API surface is active for CLI style apps:
    • file access
    • network requests
    • time and locale
    • standard input and output
  3. Sample apps are implemented:
    • krate-clock
    • krate-cat
    • krate-curl
  4. Capability checks are in place, so apps only get access they request and are granted.
  5. 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.
  6. AI agents can run apps safely: a library API, a --json run 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.
  7. 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
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. Phase 3 has started at the contract layer. The first desktop gui world and the first ui, gfx, and audio API drafts now parse and have a checker. This is not a finished GUI yet. It is the first map for the work.
  19. 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.
  20. 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.
  21. 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.
  22. 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.
  23. 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.
  24. 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.
  25. 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.
  26. 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.
  27. 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.
  28. 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.
  29. 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.
  30. 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.
  31. 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.
  32. 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.
  33. 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.
  34. 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.
  35. The window layer has a clearer name now. In simple terms, WindowAdapter is the lower layer for creating windows and reading host window events. UiAdapter sits 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.
  36. 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.
  37. 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.
  38. 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.
  39. 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.
  40. 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.
  41. 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.
  42. 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.
  43. 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.
  44. The macOS prototype can now attach a first draw view. In simple terms, the opt-in AppKit path can put an NSView inside 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.
  45. 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.
  46. 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.
  47. 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.
  48. 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.
  49. 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.
  50. 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.
  51. 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.
  52. 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.

AreaStatus today
Runtime baseWorking
Security model (capability checks)Working in current Phase 2 scope
CLI sample appsWorking
Phase 2 proof trackingWorking, with a readiness command and evidence pages
CI and docs stability proofWorking, with a GitHub run-history recorder
Hosted full cross-host proofLatest full Linux, macOS, Windows run is green
Self-hosted full-gate proofReady to record through GitHub run history
Fuzz proofReady to record as smoke or longer soak evidence
UAPI freeze decision pathWorking, with a draft packet and CI checker
Outside walkthrough proofReady to collect, with a timing packet, checker, and local rehearsal
Phase 3 handoffStarted at contract level, still waiting on Phase 2 outside review for formal phase close
Desktop GUI pathWIT 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 pathNot started in implementation
Packaging and app store style distributionNot 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:

  1. Final UAPI freeze review.
  2. Cross host evidence for the sample apps and adapters.
  3. Keep Go experimental for runtime parity until its compiled components stop importing host APIs directly.
  4. Longer fuzz, benchmark, and dependency signoff runs.
  5. One timed outside developer walkthrough using the packet checker.
  6. 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-clock can use time, locale, timezone, and stdout.
  • krate-cat can read granted files and deny missing or out-of-scope file grants.
  • krate-curl can 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.

ConceptMeaning
WASM componentThe portable program file that Krate runs.
RuntimeThe krate engine installed on a host machine.
UAPIThe common app API. Apps call this instead of calling each OS directly.
UCapThe permission system. It decides what an app is allowed to use.
Host adapterThe code that turns a Krate API call into a native OS call.
.krate bundleThe future app package containing code, assets, manifest, and signature.
ManifestThe file that describes an app and the permissions it asks for.
MarketplaceThe 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.

  1. The app calls the Krate file API.
  2. UCap checks whether that app has a grant for the file path.
  3. The host adapter calls the native file API for the current OS.
  4. 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:

  • io
  • fs
  • net
  • time
  • locale

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 .wasm file 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:

  1. App calls a UAPI function.
  2. Runtime turns that call into a capability string.
  3. The session policy checks whether that capability was granted.
  4. 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.open does not call the file adapter
  • a denied net.fetch does 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 UiAdapter trait 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_adapter selects the current host UI adapter and exposes capability info for that backend
  • Phase3UiDispatcher::pump_event_loop_once gives 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 winit window 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, and rename
  • 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.0
  • krate:io/streams@0.1.0
  • krate:io/stdio@0.1.0
  • krate:io/args@0.1.0
  • krate:io/log@0.1.0
  • krate:fs/types@0.1.0
  • krate:fs/files@0.1.0
  • krate:net/types@0.1.0
  • krate:net/http-client@0.1.0
  • krate:time/clock@0.1.0
  • krate:time/sleep@0.1.0
  • krate:locale/types@0.1.0
  • krate:locale/info@0.1.0
  • krate: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, and list require a matching fs.read:PATH grant 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 file handle.
    • read needs fs.read:PATH; write, append, and read-write also need the matching write grant.

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:PATH for 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:PATH before 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:PATH for 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 n bytes from the current file cursor.

  • read(n: u32) -> result<list<u8>, fs-error>
    • Reads up to n bytes from an opened file handle.
    • The runtime rechecks the handle path before each adapter read.

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.args is 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 -- in krate run.
    • Current encoding is newline-separated text, so SDK helpers should parse it for app code.

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.log is 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, and io.stderr are 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, and io.stderr are 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 n bytes from the stream.

  • read(n: u32) -> result<list<u8>, io-error>
    • Reads up to n bytes from an input stream.
    • A short read is valid; an empty read means the stream has no more bytes right now or is closed.

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-utf8 instead 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-all or 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/Singapore when 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

  • get and fetch require a matching net.connect:HOST:PORT grant 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, and Content-Length under host control.
    • Timeouts, oversized bodies, malformed responses, and missing grants are typed as net-error cases.

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.clock and time.monotonic are 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-millis requires time.sleep.

Rust SDK Example

#![allow(unused)]
fn main() {
krate::time::sleep_millis(100);
}

Functions

Block the calling task for at least millis milliseconds.

  • 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:

ModuleUse it for
krate::ioapp arguments, stdin, stdout, stderr, logging
krate::fsopening, reading, writing, and checking files
krate::netHTTP client calls
krate::timewall clock, monotonic clock, sleep
krate::localelocale, 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-wasip1 installed
  • cargo-component installed

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.rs
  • apps/krate-cat/Cargo.toml
  • apps/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, and locale.
  • 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.go
  • packages/sdk-go/examples/krate-cat/main.go
  • packages/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:

  • go
  • tinygo

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/sdk package 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.ts
  • packages/sdk-ts/examples/krate-cat.ts
  • packages/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:

  • node
  • npm
  • jco

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, and io.stderr as default-granted app plumbing
  • fs.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

  1. Read the Code of Conduct. We enforce it.
  2. Skim the vision and roadmap.
  3. Find a good first issue: https://github.com/incyashraj/krate/labels/good%20first%20issue
  4. Read your first PR for a complete walkthrough.

Guides

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 issue label 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

  1. Copy docs/adr/template.md to docs/adr/NNNN-short-title.md (next sequential number).
  2. Fill out every section. Be honest about alternatives rejected.
  3. Open a PR titled ADR: <title>.
  4. Minimum 2 maintainers approve, or 1 approve + 7 days open.
  5. Merged ADRs are immutable. To supersede an ADR, write a new one that references the old one with Supersedes: ADR-NNNN.

Index

ADRTitleStatus
ADR-0001Rust for the Krate runtimeAccepted
ADR-0002Wasmtime as runtime engineAccepted
ADR-0003Component Model from day oneAccepted
ADR-0006WIT versioning strategyAccepted
ADR-0007UCap v0.1 soft enforcementAccepted
ADR-0008Host async runtimeAccepted
ADR-0009Sandbox link-semantics guardrailsAccepted
ADR-0010Locale and timezone discovery fallbacksAccepted
ADR-0011Phase 2 benchmark regression policyAccepted
ADR-0012Adapter crate split per host OSAccepted
ADR-0013Widget lowering strategyProposed
ADR-0014Layout engine uses TaffyProposed

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, or misc.

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 callCapability
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-denied where 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_WASM
  • KRATE_GO_CAT_WASM
  • KRATE_GO_CURL_WASM
  • KRATE_TS_CLOCK_WASM
  • KRATE_TS_CAT_WASM
  • KRATE_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.wasm
  • test/integration/language-variants/krate_go_cat.wasm
  • test/integration/language-variants/krate_go_curl.wasm
  • test/integration/language-variants/krate_ts_clock.wasm
  • test/integration/language-variants/krate_ts_cat.wasm
  • test/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_parse for Manifest::parse
  • logical_path_parse for shared path normalization and operation intent checks
  • policy_match for 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)
  • tinygo and go: for the Phase 2 Go binding path
  • node, npm, and jco: 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.dev or 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

AreaStatus
LicensesMIT and Apache-2.0 files are present.
Rust workspaceCargo build and test pass.
ToolchainRust 1.91.1 is pinned.
Dependency policycargo-deny passes.
CIFormat, lint, tests, docs, and dependency audit run on GitHub Actions.
Docs sitemdBook is live at https://incyashraj.github.io/krate/.
Project docsREADME, CONTRIBUTING, SECURITY, code of conduct, ADR template, and first-PR guide exist.
Issues and labelsFirst five good-first issues were created and labeled.
Phase 1 kickoffGitHub issue #6 exists.

Still Important

AreaNext action
Public repo settingsConfirm public visibility, social preview, description, homepage, and topics.
Branch rulesKeep main protected by green CI and pull request review. Owner bypass should stay temporary.
CI historyKeep main green for five consecutive days. First green runs started on 2026-05-03.
README reviewAsk one outside reader to follow the README and mark what confused them.
Discord or community homeCreate channels, rules, and an invite link before public launch.
AnnouncementPublish only after docs, repo settings, and community home are ready.
DomainSecure krate.dev or a chosen equivalent.
NamingFinish official trademark and registry checks.
External PRGet one outside contributor PR opened and merged.
RetrospectiveFinish 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/runtime embeds Wasmtime.
  • crates/cli builds the krate command.
  • krate run, krate version, and krate doctor work.
  • A small hello-world WASM component prints Hello, Krate!.
  • Runtime fuel and memory limits fail with clear errors.
  • CI builds one hello .wasm artifact and runs the same bytes on Linux, macOS, and Windows.
  • v0.1.0-rc1 is published with five archives and SHA256SUMS.
  • 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

FieldValue
Date2026-05-02
CPUApple M4
Cores10 physical / 10 logical
Memory16 GB
OSmacOS 26.3.1 (25D2128)
Architecturearm64
Rustrustc 1.91.1
Wasmtime43.0.2

Microbenchmarks

Run with:

scripts/build-phase1-components.sh
cargo bench -p krate-runtime --bench startup
scripts/check-benchmark-regression.sh
MetricBaselinePhase 1 targetNotes
Wasmtime engine construction864 ns< 100 msMeasures Runtime::new.
Component::from_binary for hello-world2.366 ms< 20 msMeasures component compilation from bytes.
Cold runtime run to run() completion2.451 ms< 200 msIncludes runtime creation, compile, instantiate, and one host print to a sink.
First host print on a loaded component10.067 usTrackMeasures instantiate plus one host call with output suppressed.
1,000 host print calls on a loaded component88.189 us< 1 us/callCriterion throughput: about 88 ns per call including loop overhead.
RSS after hello-world exits14.9 MiB< 40 MiBMeasured 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

ArtifactSize
target/release/krate11 MB
dist/krate-0.1.0-dev-aarch64-apple-darwin.tar.gz4.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-runtime Wasmtime embedding
  • The temporary krate:phase1/host WIT 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
  • .krate bundles, manifests, signing, and marketplace distribution
  • Long-running app lifecycle, windows, background services, and updates
  • Running adversarial WebAssembly as a hardened security boundary

Assets

AssetWhy It Matters
Host process memoryThe runtime must not let a component corrupt host memory.
Host filesystem and networkPhase 1 should not expose either to components.
Terminal outputComponents can write through print; users should know that output is untrusted.
Runtime availabilityComponents should not trivially hang or exhaust the host.
Build and dependency integrityWasmtime 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

CategoryThreatCurrent MitigationResidual Risk
SpoofingA 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.
TamperingA 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.
RepudiationA 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 DisclosureA 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 ServiceA 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 PrivilegeA 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-deny checks advisories, licenses, bans, and sources.
  • krate run --fuel N can bound instruction execution.
  • krate run --mem-limit MB bounds 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 krate in Phase 1. Treat krate run foo.wasm exactly 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 ItemTarget PhaseNotes
Capability declarations and grantsPhase 2UCap begins when real UAPI modules exist.
Filesystem/network permission enforcementPhase 2First useful CLI apps will need explicit capabilities.
Structured runtime logging and audit recordsPhase 2Needed for debugging and repudiation controls.
GUI/window security boundariesPhase 3UI introduces input, focus, clipboard, accessibility, and rendering risk.
Mobile platform sandbox integrationPhase 4iOS/Android require adapter-specific review.
Bundle manifests and deterministic package verificationPhase 5Developer SDK starts producing app bundles.
Code signing, release trust, and marketplace moderationPhase 6Required before user-facing distribution.
External security auditPhase 7Must 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 run starts 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/runtime embeds Wasmtime with Component Model support.
  • crates/cli ships the first krate binary with run, version, and doctor commands.
  • A temporary krate:phase1/host WIT interface supports print and exit for the hello-world proof.
  • scripts/test-phase1.sh validates runtime behavior locally and in CI.
  • CI builds one shared hello .wasm fixture, records its SHA-256 hash, and runs that same artifact on Linux, macOS, and Windows.
  • v0.1.0-rc1 published five platform archives plus SHA256SUMS.
  • 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:

  • io
  • fs
  • net
  • time
  • locale

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-curl
  • krate-cat
  • krate-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)

FieldValue
Date2026-05-05
CPUApple M4
OSmacOS
Architecturearm64
Rustrustc 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=warn for warning-only reports
  • BENCH_REGRESSION_MODE=fail for hard gate mode
  • BENCH_REGRESSION_THRESHOLD_PCT=<n> for threshold tuning
  • BENCH_BASELINE_FILES=<path[:path...]> for custom baseline sets

Current CI usage:

  • Hosted full benchmark job uses warn mode against the Phase 2 baseline file.
  • Self-hosted CI can run the same check in fail mode for strict local gating.

Dispatch Baseline (2026-05-05)

PathLocal resultPhase 2 targetNotes
Default stdout grant~192 ns< 1 usDefault low-risk IO capability.
Filesystem open with read grant~1.16 ustrackPath grant check plus adapter call.
File handle read with grant re-check~1.20 ustrackRe-checks the opened file path before read.
File handle write with grant re-check~1.19 ustrackRe-checks the opened file path before write.
Missing filesystem read grant~579 ns< 1 usDenial path stops before adapter work.
HTTP fetch grant check~954 ns< 1 usURL 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)

PathLocal resultPhase 2 targetNotes
Compile Phase 2 smoke component from bytes~2.86 mstrackWasmtime component compile path.
Cold runtime + run Phase 2 smoke app~3.16 ms< 150 msReads a granted file, uses time, locale, and stdout.
Run preloaded Phase 2 smoke app~81.05 ustrackUAPI calls with component already loaded.
Run preloaded krate-clock with fixed time~49.92 ustrackTime, 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 run with Phase 2 krate:app/cli@0.1.0 components
  • 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

AssetWhy it matters
Host filesystemPrevent reads and writes outside granted paths.
Host networkPrevent unauthorized outbound requests.
Runtime process stabilityAvoid crashes, panics, and unbounded memory or handle growth.
Capability intentManifest and launch grants must match actual runtime behavior.
User trustApp 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

  1. Manifest capability parsing and normalization
  2. Grant matching rules for paths and net.connect resources
  3. Resource handles reused across calls after open
  4. URL parsing and HTTP request framing
  5. Localhost fixture and test behavior on restricted runners
  6. Runtime limits for reads, writes, list sizes, and resource table growth

STRIDE Summary

TypeExample in Phase 2Current controlsResidual risk
SpoofingA random component claims to be a trusted appManifest identity is explicit during grant flowsNo signing chain yet
TamperingBypass policy and reach adapter directlyDispatcher enforces checks before adapter callsNew call paths can drift if not tested
RepudiationApp denies requested or effective grants--dump-caps and grant logging paths existNo central audit backend yet
Information disclosureRead outside granted sandbox pathPath normalization and grant checks happen before adapter callsSymlink and host edge cases still need continued hardening
Denial of serviceOversized reads, writes, response bodies, or handlesExplicit bounds on reads/writes/listings, response caps, resource capsBenchmark and fuzz evidence still growing
Elevation of privilegeEscape WASM and execute host behavior outside UAPIWasmtime isolation + narrow host surfaceDepends on upstream engine correctness

Required Security Invariants

These are non-negotiable in Phase 2:

  1. Every UAPI call that can touch host resources must pass capability checks first.
  2. Handle-based methods (file and stream resources) must re-check grants before use.
  3. Denials must happen before adapter and OS calls.
  4. Manifest parsing must reject malformed capability resources early.
  5. 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.

AreaPhase 1Phase 2
App shapeone proof componentCLI component world
Host APIkrate:phase1/host.print and exitio, fs, net, time, locale
Permissionsnonemanifest capabilities plus run-session grants
App metadatanonesidecar manifest.toml
Sampleshello-worldkrate-clock, krate-cat, krate-curl
SDKdirect generated bindingsfirst 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

  1. Replace Phase 1 generated host calls with the Rust SDK.
  2. Return an exit code from Guest::run.
  3. Add manifest.toml.
  4. Declare every non-default capability.
  5. 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:

AreaCountExamples
Filesystem16open, read, write, stat, list, remove, rename, canonicalize
Network5resolve address, connect TCP, apply timeouts, write request, read response
IO6stdin, stdout, stderr, stream writes, stream flushes
Time2clock discovery, sleep
Locale5locale 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.sh passed on each host
  • cargo test -p krate-adapter-common passed on each host
  • the native adapter crate test passed for that host:
    • Linux: krate-adapter-linux
    • macOS: krate-adapter-macos
    • Windows: krate-adapter-windows

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-latest
  • adapter-evidence-macos-latest
  • adapter-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-clock
  • krate-cat
  • krate-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:

  1. krate-clock with fixed time, locale, and timezone
  2. krate-cat against two small fixture files
  3. krate-curl against 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-latest
  • sample-evidence-macos-latest
  • sample-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:

  1. scripts/smoke-rust-sdk.sh
  2. cargo doc -p krate --no-deps

It also records whether the packaged SDK contains:

  • Cargo.toml
  • README.md
  • src/lib.rs
  • src/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.sh
    • scripts/test-phase2-language-variants.sh
  • fixture presence and SHA-256 hashes for:
    • krate_go_clock.wasm
    • krate_go_cat.wasm
    • krate_go_curl.wasm
    • krate_ts_clock.wasm
    • krate_ts_cat.wasm
    • krate_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-tools versions
  • 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-clock
  • krate-cat
  • krate-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:

  1. Runtime deny matrix for non-default capabilities
  2. Dispatcher deny-before-adapter matrix for every non-default Phase 2 boundary
  3. krate-cat denies missing fs.read grant
  4. krate-curl denies missing net.connect grant
  5. Manifest-required capability deny path
  6. 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-latest
  • ucap-enforcement-evidence-macos-latest
  • ucap-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 pass or fail
  • 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:

Your First UAPI App In Rust

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 run startup 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:

  1. cargo bench -p krate-runtime --bench startup
  2. cargo bench -p krate-runtime --bench uapi_dispatch
  3. scripts/check-benchmark-regression.sh
  4. cargo build -p krate-cli --release
  5. scripts/build-krate-clock-component.sh
  6. a measured external krate run loop for krate-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, and locale
  • wasm-tools validates the world and each dependency package
  • check-uapi checks 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.sh passes on a clean checkout
  • scripts/generate-uapi-freeze-evidence.sh refreshes the published evidence page without manual edits
  • scripts/generate-uapi-freeze-lock.sh refreshes the published WIT hash lock without manual edits
  • scripts/check-phase2-freeze-decision.sh passes
  • 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.connect grant parsing

Samples

  • krate-clock has deterministic fixed-time output
  • krate-cat reads granted files and denies missing grants
  • krate-curl fetches 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.0
  • krate:fs@0.1.0
  • krate:net@0.1.0
  • krate:time@0.1.0
  • krate: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.sh passes
  • scripts/check-uapi-freeze-lock.sh passes
  • scripts/record-phase2-uapi-freeze-review.sh --strict passes
  • scripts/record-phase2-exit-bundle.sh --strict passes
  • 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.

FieldValue
ReviewerPending
Freeze commitPending
Hosted CI runPending
Self-hosted full gate runPending
Exit bundle path or artifactPending
Walkthrough packetPending
Decision datePending

If Accepted Later

When the freeze is accepted:

  1. Replace the decision line with the frozen wording.
  2. Fill the reviewer signoff table.
  3. Update the Phase 2 exit ledger gate P2E-01.
  4. Record the change in Plan/Phase-2-Plan.md.
  5. 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:

  1. Run the recorder in strict mode.
  2. Confirm the working tree only contains expected review output.
  3. Check the exit ledger for anything still partial, pending, or blocked.
  4. Record the freeze decision in the Phase 2 plan.
  5. 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
  • cli world shape is stable
  • imported interface set matches the Phase 2 plan
  • world export is run: func() -> s32
  • public names use kebab case
  • fs-error and net-error include permission-denied
  • public WIT items have contract docs

Packages

  • krate:app@0.1.0
  • krate:fs@0.1.0
  • krate:io@0.1.0
  • krate:locale@0.1.0
  • krate:net@0.1.0
  • krate:time@0.1.0

Imported Interfaces

  • krate:fs/files@0.1.0
  • krate:fs/types@0.1.0
  • krate:io/args@0.1.0
  • krate:io/log@0.1.0
  • krate:io/stdio@0.1.0
  • krate:io/streams@0.1.0
  • krate:io/types@0.1.0
  • krate:locale/format@0.1.0
  • krate:locale/info@0.1.0
  • krate:locale/types@0.1.0
  • krate:net/http-client@0.1.0
  • krate:net/types@0.1.0
  • krate:time/clock@0.1.0
  • krate: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

FileLinesBytesSHA-256
wit/krate/phase2/deps/fs/fs.wit782611b0fb112d9f3aa7f1ab9cf4f16710a3b67fdbaac931107e4602af077b28f96610
wit/krate/phase2/deps/io/io.wit92259961efe9abd8f86899aed66d3cc01c959a8a3e8982004af09ac0edac853cdb40a2
wit/krate/phase2/deps/locale/locale.wit6314673278729cfe72935fe4621750214cdd024740e8f07e63e9fc1c9390385d2e423c
wit/krate/phase2/deps/net/net.wit862280720ecc62ba371ca3faf7e241a3aae28326b0eeb5ee63360c96ceffe0cda53066
wit/krate/phase2/deps/time/time.wit174825a8f5bb2f2967ae817b166f22b0865f98171c25acfc7475f66b0ebb210bc2770
wit/krate/phase2/world.wit195236a7cc928a9ffc0d3af2a9c00ba50932c91670b08a742240edee6d8089c287f2b

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 --all output
  • 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

GateCriterionStatusEvidenceNext step
P2E-01UAPI modules frozenStrong draftscripts/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 PacketRun the final freeze review and fill the decision packet after cross-host and language evidence are ready.
P2E-02Desktop host adaptersPartialscripts/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 EvidenceKeep collecting Linux macOS Windows evidence; adapter reports now include boundary, shared adapter behavior tests, and native adapter crate tests.
P2E-03Rust bindings usablePartialscripts/smoke-rust-sdk.sh, scripts/record-phase2-rust-sdk-evidence.sh, hosted CI artifact rust-sdk-evidence, Rust SDK Evidence, First Rust CLIKeep package and outside-workspace smoke evidence green; publish only after UAPI v0.1 is frozen.
P2E-04Go bindings usablePartialscripts/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 DecisionGo is experimental for runtime parity in Phase 2; keep TinyGo smoke and import-purity checks, and revisit promotion when artifacts import only krate:*.
P2E-05TypeScript bindings usablePartialscripts/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 testsKeep TS curl success evidence stable across restricted runners and keep cross-language curl error parity green (5, 20, 21 classes).
P2E-06curl cross-hostPartialscripts/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 EvidenceMove 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-07cat cross-hostPartialscripts/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 EvidenceKeep strict cross-host cat hash parity green in hosted and self-hosted evidence runs.
P2E-08clock cross-hostPartialscripts/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 EvidenceKeep strict fixed-time clock hash parity green in hosted and self-hosted evidence runs.
P2E-09UCap enforcementPartialcrates/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 EvidenceKeep collecting clean Linux macOS Windows deny evidence for the full deny-before-adapter matrix.
P2E-10Startup performancePartialcargo 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 EvidenceCollect Linux macOS Windows benchmark reports for the same commit, then set a full-CLI startup threshold from that evidence.
P2E-11Dispatch performancePartialcargo 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 EvidenceKeep stable regression checks, cross-host benchmark evidence, and dependency-audit signoff linked to the final Phase 2 commit.
P2E-12Timed developer walkthroughPendingscripts/check-phase2-rust-walkthrough-rehearsal.sh, scripts/record-phase2-walkthrough-template.sh, scripts/check-phase2-walkthrough-evidence.sh, First Rust CLI, Timed Walkthrough EvidenceAsk an outside Rust developer to fill the walkthrough packet, time the run, and pass the filled-packet checker.
P2E-13Generated UAPI referenceDonescripts/generate-uapi-reference.sh, Generated ReferenceKeep CI freshness checks enabled.
P2E-14WIT style guideDoneWIT Style GuideKeep using it during freeze review.
P2E-15ADR setDonedocs/adr/0006-wit-versioning.md through docs/adr/0012-adapter-crate-split.mdAdd 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, and locale
  • 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:

  1. freeze UAPI v0.1 after review
  2. collect clean Linux, macOS, and Windows evidence for the same samples
  3. keep the Go track experimental until import purity passes
  4. run longer fuzz and benchmark evidence
  5. do one timed external walkthrough
  6. 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.sh after 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, and locale
  • 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.

AreaCurrent stateNext step
UAPI freezeStrong draftRun final freeze review after the remaining proof is collected.
Cross-host evidencePartialKeep Linux, macOS, and Windows reports aligned for one commit.
Go runtime parityExperimentalKeep TinyGo smoke builds, but wait for import-pure Krate components before promotion.
Long fuzz soakPartialRun a longer self-hosted fuzz pass on the final candidate.
Timed outside walkthroughPendingAsk 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.0 with a gui world
  • krate:ui@0.1.0 for windows, widget trees, events, dialogs, clipboard, and menus
  • krate:gfx@0.1.0 for 2D canvas and a small future 3D surface
  • krate:audio@0.1.0 for playback and capture shape
  • scripts/check-phase3-uapi.sh so 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 events
  • adapter-common::ui, the first host-neutral widget tree model for stable widget IDs, widget kinds, labels, roles, style hints, and parent links
  • adapter-common::ui::WindowAdapter, the named lower boundary for window lifecycle and host event-loop signals
  • adapter-common::ui::UiAdapter, the shared trait that native UI adapters will implement
  • runtime::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 Krate WindowId
  • the first macOS AppKit handoff method, ready for the native AppKit window prototype while the default adapter remains headless draft
  • an opt-in macOS AppKitWindowBackend that creates an owned NSWindow on 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 queue
  • AppKitWindowNativeEvent and AppKitWindowEventState, 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
  • AppKitWindowDelegateCallback and AppKitWindowDelegateBridge, so the coming Objective-C delegate can stay thin and hand event translation to tested Rust code
  • AppKitDrawSurfaceState, which tracks size, scale, clear color, redraw requests, and frame metadata before the real AppKit view paints pixels
  • AppKitDrawViewSurface, an opt-in AppKit NSView attachment path that sets a visible clear color, marks the view dirty, and records the first frame snapshot
  • AppKitWindowNativeDelegate, a retained AppKit NSWindowDelegate object that records native close, resize, focus, and backing-scale callbacks for the Rust session to drain
  • AppKitWindowEventLoopDriver, a non-blocking AppKit tick proof that refreshes native state, drains delegate callbacks, and queues redraw requests through the shared event stream
  • Phase3UiRuntime::with_host_adapter, which selects the current host UI adapter and reports whether it is still headless or native
  • Phase3UiRuntime::try_with_host_adapter_mode, which keeps the default headless path stable while allowing macOS to explicitly request the AppKit prototype adapter
  • UiEventLoopTick and Phase3UiDispatcher::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.0 with a gui world
  • krate:ui@0.1.0 for windows, widget trees, events, dialogs, clipboard, and menus
  • krate:gfx@0.1.0 for simple 2D drawing and an early 3D surface shape
  • krate:audio@0.1.0 for 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:

CapabilityMeaning today
ui.window:createA 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:writeClipboard access is sensitive and must be granted explicitly.
ui.dropzone:<mime-type>Drag and drop can be scoped by MIME type.
gfx.gpu:basicBasic GPU-backed drawing is a default grant.
gfx.gpu:computeCompute access is explicit.
audio.playbackPlayback is defined, but not default yet.
audio.captureMicrophone 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:

  1. Record prepared and cold layout benchmark numbers on the target hosts.
  2. Create the first real Linux and Windows winit window and feed its actual callbacks into the Winit collector.
  3. Connect real host input events to the draft pointer, key, and text routes.
  4. Add a small notes app skeleton that uses the same path.
  5. 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:

ApproachWhat it meansWhy Krate is not using it as the main path
Draw everythingThe framework paints every control itselfEasier to match pixels, but often feels less native
Use only native controlsEvery widget is a host widgetGood feel, but too rigid for custom app surfaces
Embed a browserThe app is a web UI in a desktop shellUseful 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):

HostWindowingWidget lowering
macOSAppKit (own bridge)Native AppKit controls, drawn fallback for the long tail
WindowswinitNative Win32 common controls as child windows, drawn fallback for the long tail
LinuxwinitDrawn 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:

WidgetFirst use
Window rootPut the app in a host window
StackArrange controls vertically or horizontally
TextShow labels and short text
ButtonTrigger actions
Text fieldEdit one line
Text areaEdit note content
ListShow notes
ScrollMove through long content
CheckboxBasic on or off state
MenuApp and window commands
CanvasCustom 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::ui has the first host-neutral widget tree types: WidgetId, WidgetKind, WidgetNode, WidgetStyle, and WidgetTree.
  • 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-layout can 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.
  • WindowAdapter now names the lower window/event-loop boundary. UiAdapter builds 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 NSView step a small state object to use.
  • AppKit now has a first draw view surface too. It can attach an owned NSView to 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 UiAdapter now have one common event-loop pump method. Headless adapters return no native tick, and the AppKit prototype maps its native step into the shared UiEventLoopTick report.
  • 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 Button and TextField to real native controls; a human-verified click round trip flows from NSButton back 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 LayoutSnapshot keyed 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:

FieldMeaning
widthoptional logical width
heightoptional logical height
growflex grow factor
paddingsame 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:

  • app is null when no manifest was provided.
  • capabilities.granted lists the effective session grants, with boundaries (fs.read:data/**, net.connect:host:port).
  • capabilities.denied is non-empty only when required capabilities were missing and the run was refused before the component started; the process exits 5 in that case, matching the interactive flow.
  • exit.class is one of success, permission-denied (app exit code 5 by Krate convention, or a refusal before the run), app-error, limit-exceeded, invalid-component, or trap. exit.code is null when the runtime stopped the component (limit-exceeded, invalid-component, trap).
  • duration_ms is null when the run was refused before starting.
  • stdout holds the app's captured stdout as lossy UTF-8.

Current status

Done now:

  • Runtime::run_bytes_captured / run_file_captured capture app stdout for embedding callers.
  • krate_runtime::embed::run_component with EmbedOutcome and EmbedExitClass, doc-tested.
  • krate run --json emitting krate.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:

ExitMeaning
0A click on the native button reached the component (the full round trip)
1Clean bounded run with no click — the normal headless outcome
2The user closed the window before clicking
3032Window 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:

  1. 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.
  2. 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) and pump_app_events in crates/adapter-macos/src/appkit.rs.
  • Window appears but clicks do nothing: the event-queue pump is not running — same place.
  • Exit 32 headless: a widget-tree call failed; run with --json to 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:

  • .krate bundle 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 Krate demo app running on Linux under CI: a clicked button, the text "hi krate" typed into the field, a checkbox, a progress bar, and a list scrolled down to line eight

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.

Glossary

TermDefinition
ABIApplication Binary Interface: the contract between caller and callee in compiled code.
ADRArchitecture Decision Record: a short document capturing a decision and its context.
AOTAhead-Of-Time compilation.
App BundleThe .krate distributable package.
CapabilityAn unforgeable token granting the right to perform an operation on a resource.
Component ModelWebAssembly specification adding typed interfaces and composition to WASM modules.
DIDDecentralized Identifier: a W3C standard for self-sovereign identity.
Host AdapterThe per-OS module inside the Krate runtime that maps UAPI calls to native OS calls.
JITJust-In-Time compilation.
Manifestmanifest.toml: describes a Krate app's metadata and required capabilities.
KrateThe universal application platform described here.
UAPIUniversal API: the standard library exposed to every Krate app.
UCapUniversal Capabilities: the permission model.
UIRUniversal Intermediate Representation: the bytecode every app compiles to (= WASM + Component Model).
WASIWebAssembly System Interface: a standard set of host interfaces for WASM.
WASMWebAssembly.
WITWebAssembly Interface Types: the IDL for Component Model interfaces.

References

Specifications

Runtimes

Languages / Toolchains

UI frameworks (prior art)

Capability systems (prior art)

Documentation methodology