Phase 2: UAPI v0.1
Status: Active; exit evidence in progress Estimate: est. 4 to 8 weeks Goal: Make Krate useful for small command line apps.
Security baseline for this phase is now published: Phase 2 Threat Model v0.2.
Phase 2 replaces the temporary Phase 1 host interface with real APIs:
iofsnettimelocale
The first draft of those WIT contracts now lives at wit/krate/phase2.
It is not frozen yet, but it is real source code and CI parses it so syntax
mistakes are caught early.
Phase 2 also has a quick readiness command now:
scripts/phase2-exit-readiness.sh. It reads the exit ledger and prints the
current count of done, partial, pending, and blocked gates, so progress checks
do not depend on memory.
Hosted CI stability can now be recorded too:
scripts/record-phase2-ci-stability-evidence.sh writes recent CI and Pages run
history into one markdown report for exit review.
The same report can be included in the exit bundle with
scripts/record-phase2-exit-bundle.sh --strict --include-ci-stability when we
prepare a final review packet.
Self-hosted full-gate history has its own recorder too:
scripts/record-phase2-self-hosted-evidence.sh. This keeps the local runner
proof separate from hosted CI while still letting the exit bundle include it
when needed.
The UAPI freeze decision has its own packet now as well:
scripts/check-phase2-freeze-decision.sh checks that the packet names the
scope, required evidence, no-go conditions, and pending reviewer signoff before
we call the contract frozen.
The capability layer has also started. Krate can parse a sidecar
manifest.toml, check launch-time grants, and carry the session policy into the
runtime. The newest piece is a runtime UAPI guard: it translates calls like
fs.read ./data/file.txt or net.connect api.example.com:443 into the exact
capability check that must pass before an adapter talks to the host OS.
Manifest capability parsing now also validates resource shapes earlier: unsafe
filesystem resource patterns and malformed net.connect endpoint forms fail
during manifest parsing instead of reaching runtime policy resolution.
Manifest net.connect host parsing is stricter now too: invalid dot placement,
invalid leading or trailing - in host labels, and malformed numeric IPv4
host forms are rejected at parse time.
Session policy matching now also normalizes net.connect host casing and
numeric ports before wildcard matching, so grant checks stay stable when users
type host names with mixed case or port values like 0443.
Manifest capability parsing now stores that same canonical net.connect
resource shape too, so duplicate-capability checks and capability displays stay
deterministic across host case and numeric port formatting differences.
Manifest capability parsing now applies the same deterministic storage rule to
fs.* resources through shared logical-path normalization, so formatting
variants like ./notes/** and notes\\** are stored as one canonical
notes/** shape for duplicate checks and capability output.
Interactive grant prompts now keep rationale text aligned with those canonical
capability shapes too, so prompts still show the right human reason lines even
when the manifest used non-canonical resource formatting.
There is also a first dispatcher scaffold now. In simple terms: we have the place where generated UAPI calls will enter the runtime, get checked by policy, and then move to the host adapter. The current tests prove denied file and network calls stop before any adapter code runs.
The Phase 2 WIT also has a Rust host-binding checkpoint now. That means CI asks
Wasmtime to generate bindings for the new cli world and checks a few important
names. So far the shape is usable: run returns an i32, OpenMode::Read
exists, and HttpMethod::Get exists.
There is also a first Rust guest SDK crate now. It lives at
crates/bindings-rust, builds as package krate, and gives app code simple
module names such as krate::io, krate::time, and krate::locale.
The Rust sample apps now use that SDK facade, so normal app code talks to
krate::fs::open, krate::net::get, and krate::time::now_millis
instead of deep generated binding paths.
The SDK now has its first small helper layer too: argument helpers, stdout and stderr text helpers, common file helpers, HTTP body helpers, and top-level time and locale shortcuts. The important rule is still the same: guest apps should import Krate UAPI, not host WASI APIs. The current sample components are checked for that. The Rust SDK also has a packaged-crate smoke now: CI creates a temporary app outside the workspace and checks that it can compile a tiny Krate component against the packaged SDK.
Go now has a clear Phase 2 decision too. The Go examples and TinyGo smoke build
stay in scope, but Go runtime parity is experimental until the compiled
components import only krate:* UAPI packages. We are not promoting Go
fixtures that still import wasi:* directly, because that would weaken the
runtime boundary Phase 2 is meant to prove.
The latest runtime piece is the generated type bridge. It maps WIT records and errors into the dispatcher's Rust types, then back again. Put simply: the runtime now understands the words that generated WIT code will use when it asks for files, network, time, locale, and logs.
The generated host traits are also wired for the first useful slice. HTTP,
path-level filesystem operations, time, locale, logs, and stdio now call the
dispatcher, which means UCap sits in front of those calls. A small resource
table now owns opened file and stdio handles, so reads, writes, seeks, stats,
and flushes can route through the adapter without exposing raw host IDs. The
local runtime now also caps that open-handle table, so Phase 2 components
cannot grow stream/file handles without bound. Generated resource drop
callbacks now close underlying local adapter handles too, so released
resources return slots back to the same local runtime session. The generated
host-side resource table now has its own active-resource cap too, so both host
and adapter layers have bounded handle growth in this phase. Both tables now
also reuse released resource IDs before allocating fresh IDs, so long-running
sessions keep resource identity stable and avoid unbounded ID growth.
The runtime also has an initial Phase 2 execution path now. krate run keeps
supporting the Phase 1 proof world, then falls back to the Phase 2 cli world
and installs the generated UAPI imports. The local adapter currently covers
stdio, basic filesystem calls, time, locale, and a first plain HTTP request
path. Relative filesystem paths now resolve through an explicit runtime
sandbox root. The default is ., and --sandbox-root <dir> lets a run point
app-relative file access at a specific directory. Shared path cleanup also keeps
the filesystem adapter and UCap grant matcher using the same rules. It now also
rejects colon-based prefix forms up front, so Windows drive-style and alternate
data stream style paths do not cross host-specific parsing rules. It also
rejects reserved Windows device-style names such as con, nul, com1, and
lpt1 before host I/O, and now rejects path segments ending in . or a
trailing space to avoid Windows filename normalization edge behavior. It now also
rejects oversized path segments and oversized normalized logical paths before
host I/O so cross-host path behavior remains predictable in this phase. Read
and list bounds are now explicit too: one file read call is capped at 8 MiB,
and one directory list call is capped at 4096 entries in this early adapter
slice. Write bounds now match that direction: one stream or file write call is
capped at 8 MiB in the same early adapter path. Absolute
logical paths are now sandbox-rooted too, so /notes/file.txt resolves under
the configured sandbox root instead of host root. For relative
paths, the local adapter now checks canonical existing targets, or the canonical
parent for new files, before host I/O. If a symlink would take the path outside
the sandbox root, the adapter denies the call. On Unix and Windows hosts, file
open also uses a no-follow final-symlink flag, so the final filename cannot be
a symlink at open time.
That no-follow open behavior now routes through the per-OS adapter crate path
instead of runtime-local cfg blocks.
Runtime path resolution now also denies symlinked path segments inside the
sandbox traversal path itself, for both existing-target and create-path
operations, so this phase has less directory traversal race exposure.
For Windows parity, the same traversal guard now treats reparse-point segments
as blocked link semantics too, so junction-style hops are denied in the same
sandbox traversal path checks.
This behavior is now captured as an architectural decision in
docs/adr/0009-sandbox-link-semantics.md.
Destructive filesystem operations now go through a shared operation-intent check
too. That means remove and rename cannot target root-like paths such as . or
/ before the adapter reaches native host I/O.
The HTTP path is still small on purpose: it is for localhost and
test-server proofs while HTTPS, redirects, streaming, and production hardening
stay open. It now forwards lower-level fetch methods, app headers, and
buffered bodies, while keeping host-controlled transport headers owned by the
adapter. It also has a response-size guard and typed errors for oversized
responses, timeouts, and malformed HTTP responses, so apps can react to the real
problem instead of receiving one generic network failure. The shared URL parser
now also rejects whitespace, control characters, empty ports, and port 0
before anything reaches the request line or socket layer. It also rejects
unsupported authority forms in this early plain-HTTP slice and rejects control
characters in app-provided header values. Transfer-Encoding is now treated as
host-controlled with Host, Connection, and Content-Length.
Response parsing now also goes through shared adapter-common code, with strict
validation for HTTP version, status range, malformed header lines, header count
limits, and unsafe header values before data reaches runtime-facing response
types.
That shared response parser now also enforces a strict header-block size cap
(16 KiB), so oversized header sections fail early in a deterministic way.
Request framing now also enforces a strict total request-frame size cap, so
oversized combinations of valid headers and body fail before socket writes.
The response read loop is shared too, so timeout mapping and full-response size
limits use one helper across adapters.
Response integrity checks now reject unsupported response Transfer-Encoding
and conflicting or mismatched Content-Length shapes in this early plain-HTTP
slice.
Request framing now also enforces a shared buffered body size limit so this
early plain-HTTP path cannot consume unbounded request payloads.
Shared host parsing now also rejects invalid domain-label forms and invalid
numeric IPv4 literals so URL and capability-path validation stay aligned.
Numeric IPv4 textual parsing is now strict dotted-decimal as well, so ambiguous
leading-zero forms like 001.2.3.4 are rejected in both URL endpoint parsing
and manifest net.connect validation. Runtime dispatcher tests now also prove
these URLs fail as invalid before adapter network calls.
Host length is now bounded in the same way for both runtime URL endpoint
parsing and manifest net.connect parsing (maximum 253-byte host text), so
oversized hostnames fail early with consistent behavior.
It now also rejects wildcard or non-unicast numeric IPv4 endpoint forms
(0.0.0.0, 255.255.255.255, and multicast ranges) during endpoint parsing,
so runtime URL checks and manifest net.connect validation stay aligned on
connectable target shapes. Dispatcher tests now also prove these values are
rejected before any adapter network call runs.
For manifest host patterns, wildcard is now constrained to explicit shapes only:
a full left-most * label (* or *.example.com). Partial-label wildcard
forms (for example exa*mple.com) and multiple wildcard labels are rejected.
Policy matching now also treats *.example.com as a single-label wildcard
scope only (for example api.example.com, but not deep.api.example.com),
while * host grants remain the broad opt-in form. Runtime dispatcher tests
now cover this grant behavior directly.
Host names are now normalized to lowercase in shared URL parsing, so capability
checks stay stable across input case differences like EXAMPLE.com and
example.com. URL scheme checks are now case-insensitive as well, so
HTTP:// and HTTPS:// forms follow the same grant matching path.
Resolved socket-address lists now also go through shared normalization:
duplicates are removed, IPv4 addresses are preferred before IPv6 while
first-seen order is still preserved inside each family, unspecified wildcard
targets (0.0.0.0 and ::) are dropped, unusable targets (port=0 and
scope-less IPv6 link-local addresses) are filtered, non-unicast targets
(IPv4 broadcast, IPv4 multicast, and IPv6 multicast) are filtered, and
connect-attempt lists are capped to a fixed maximum so DNS result variance
cannot create unbounded retry loops.
When all resolved targets are filtered out by these guards, runtime resolution
now maps that case to a deterministic not-found path instead of leaving
host-specific behavior to later connect loops.
In this early plain-HTTP slice, URL parsing is also ASCII-only. Non-ASCII URLs
are rejected early so request framing and capability endpoint checks stay
deterministic until broader URL handling lands in a later hardening pass.
Request targets now also have a shared size limit before request framing so
this early plain-HTTP path rejects oversized path/query payloads up front.
The runtime's network capability gate now also uses a shared endpoint parser for
http:// and https:// URLs, so policy checks and adapter-side URL validation
no longer drift as separate parsers evolve. The plain http:// URL parser now
reuses that same authority parsing path to keep host/port validation in one
place.
For helper-style net.http-client.get calls, the default request timeout is now
runtime-configurable through krate run --http-timeout-millis. The default is
5000 milliseconds, and --http-timeout-millis 0 disables that default timeout
for the helper get path. For multi-address connect attempts, timed requests
now spend one shared connect-time budget across all resolved addresses instead
of applying the full timeout per address.
Time is also starting to move into shared adapter code. The local runtime now uses a common host clock helper for fixed test time, Unix-epoch milliseconds, monotonic elapsed time, and sleep. That keeps future desktop adapters from each making slightly different clock choices. It now also guards edge cases: monotonic nanoseconds saturate instead of wrapping, and out-of-range Unix-millisecond values fail with a clear conversion error.
Locale has the same first shared path now. The runtime uses common helper code
for LC_ALL/LANG locale detection, TZ fallback, basic locale normalization,
deterministic baseline date/number formatting, and locale-tag canonicalization
to stable language/script/region casing with a safe fallback for malformed
values, including stricter primary locale-subtag and bounded-subtag checks so
invalid locale-tag shapes fall back to en-US. Timezone normalization is now
conservative too, accepting only simple timezone-name shapes for this phase and
falling back to UTC on invalid input. It now also accepts normalized UTC
offset forms such as UTC+05:30, GMT-02:00, +0530, and -07, then stores
them as canonical UTC±HH:MM. Locale discovery now also has practical
fallbacks for LC_TIME, LC_NUMERIC, LC_MONETARY, LC_CTYPE,
LC_COLLATE, LC_MESSAGES, LANGUAGE (first preferred token), and
AppleLocale when LC_ALL/LANG are absent. Timezone discovery now has a
Unix fallback too: when TZ is not set and /etc/localtime is a zoneinfo
symlink, Krate derives a normalized timezone from that link target. It now
also falls back to /etc/timezone parsing if the localtime symlink path is not
usable, with strict parsing rules (ignore comments/blank lines, accept first
valid candidate, reject malformed timezone shapes). Real ICU4X formatting and
broader host-native per-OS locale/timezone discovery are still open, but the
early behavior now has one home instead of being copied in the runtime. This
fallback order is captured in
docs/adr/0010-locale-timezone-discovery-fallbacks.md.
Date formatting now also applies normalized UTC offsets to the rendered time
value, so UTC+05:30 and UTC-01:00 shift timestamps instead of only changing
the timezone label text.
As part of the per-OS adapter split, runtime locale and clock construction now
route through dedicated Linux, macOS, and Windows adapter crates, with fallback
logic kept for unsupported hosts.
The same is now true for time.sleep-millis, and locale read/format paths
(current, timezone, format-date, format-number), which now route
through the per-OS adapter path instead of runtime-local direct calls.
Filesystem no-follow open-flag setup and plain HTTP TCP connect calls now also
route through the same per-OS adapter crate path.
Socket-address resolution for plain HTTP fetch now follows that same per-OS
adapter route too.
Socket timeout setup for that fetch path now also routes through per-OS
adapters.
Sandbox link-metadata checks now also route through per-OS adapters, so both
symlink/reparse-point detection and network host calls are less runtime-local.
Filesystem stat and directory listing host calls now route through per-OS
adapters too.
Filesystem mutation host calls (remove-file, remove-dir, mkdir, rename)
now route through per-OS adapters as well.
Sandbox symlink metadata reads now also route through per-OS adapters.
Sandbox-root and parent-path canonicalization now also route through per-OS
adapters.
Filesystem open host calls now also route through per-OS adapters.
Runtime component-file reads and sandbox-root directory creation now also route
through per-OS adapters.
File-handle read, write, seek, and metadata host calls now also route through
per-OS adapters.
Stdin reads and stderr write or flush host calls now also route through per-OS
adapters.
Plain HTTP request writes and response reads now also route through per-OS
adapters.
Shared stdout print, write, and flush paths now also route through per-OS
adapters.
There is also a first smoke app under test/integration/phase2-smoke. It is not
one of the final sample apps yet. Its job is smaller: prove that a real Phase 2
component can read a file, call time and locale, and print through the UAPI
path. CI builds that component and runs it through krate run on the host
test matrix. The same smoke app now has a missing-grant test too: without
fs.read, the host returns permission denied and the component exits with a
clear stderr message.
The first named sample app has started too. apps/krate-clock is a Rust
component that reads time and locale through UAPI, then prints through UAPI
stdout. The CLI now has a hidden --test-time flag, so tests can freeze the
clock and check stable output. It now also has hidden --test-locale and
--test-timezone flags, so fixture tests can pin all clock output fields and
assert one exact snapshot across hosts.
The second sample path has started as well. apps/krate-cat reads app
arguments through krate:io/args.raw, opens files through krate:fs/files,
and writes bytes to UAPI stdout. The tests prove both sides: it reads files with
the right fs.read grant, and gets permission denied without that grant. It
also denies a file outside the granted glob with exit code 5, matching the
CLI's permission-denied convention. In this phase, raw app-argument transport
is intentionally conservative: empty arguments, newline/NUL delimiter
characters, too many argument entries, and oversized raw payloads are rejected
before they reach guest argument parsing. Krate CLI now also does the same
check as a preflight step, so these invalid argument shapes fail before runtime
startup.
The third sample path has started now too. apps/krate-curl reads a URL from
Krate app args, calls krate:net/http-client.get, and writes the response
body through UAPI stdout. Its first tests use a local HTTP server: with
net.connect:127.0.0.1:PORT it fetches, without that grant it exits before the
runtime opens a socket. Permission denial also exits with code 5.
Oversized responses, timeouts, and malformed HTTP responses now print specific
curl messages too.
The generated UAPI reference has also grown past a raw signature list. It now pulls capability strings from the manifest crate and adds short behavior notes under each function and resource method, so the docs explain both the call shape and the permission model in one place. The WIT files now carry first-pass contract comments for the Phase 2 world, interfaces, records, variants, enum cases, resource methods, and functions. Those comments flow into the generated reference, so the published API page is closer to a real freeze candidate instead of only showing type signatures.
The UAPI gate is stricter now too. scripts/check-uapi.sh first runs
wasm-tools component wit across the Phase 2 world and all dependency
packages (io, fs, net, time, locale), then runs the contract-shape
checks in krate-tools --bin check-uapi. That checker now also fails if
public Phase 2 WIT items are missing contract docs. Hosted and self-hosted CI
both run this before UAPI reference regeneration.
The Rust SDK publish-readiness smoke is stricter too. It still packages the
krate crate and compiles a tiny outside-workspace component against that
packaged crate, and now it also checks that the packaged crate contains the
public README, SDK root, and generated bindings files before passing.
There is now a Rust SDK Evidence page too.
scripts/record-phase2-rust-sdk-evidence.sh records package smoke results,
SDK doc-build results, and packaged-file presence in one report. Normal hosted
CI uploads that report as a rust-sdk-evidence artifact, which gives P2E-03
a concrete proof source while crates.io publishing waits for UAPI freeze.
There is now a dedicated UAPI Freeze Review
page. It turns the remaining freeze work into a clear checklist for contract
shape, runtime behavior, samples, language tracks, and evidence.
There is also a generated UAPI Freeze Evidence
page now. It records the current package set, imported interface set, world
shape, and contract checks in one place, so freeze review has a concrete
snapshot instead of only a checklist. Hosted CI and self-hosted CI now
regenerate that page and fail if it is stale.
There is also a generated UAPI Freeze Lock.
It records SHA-256 hashes for the exact Phase 2 WIT files under review. CI
regenerates it and fails if the committed lock is stale, so UAPI contract drift
has to be intentional and visible.
The UAPI Freeze Review Evidence
page now explains the local review recorder. That recorder checks the WIT
contract, generated reference, freeze evidence, freeze lock, adapter-boundary
guard, and exit ledger in one report. It gives the freeze review a concrete
artifact without pretending the final decision is already made.
There is now a Phase 2 Exit Evidence ledger too.
It tracks all 15 exit gates with a status, proof source, and next step, and CI
checks the page shape so the list cannot silently drift from the plan.
The quick local review path is now Exit Bundle.
scripts/record-phase2-exit-bundle.sh runs the core local exit checks and
writes one report with command results, the current gate snapshot, working tree
state, and log tails. This is not a completion stamp. It is a cleaner handoff
file for review before the final Phase 2 decision.
For the one human proof gate, there is now a
Timed Walkthrough Evidence page and a
template generator. This keeps the outside Rust walkthrough tied to a commit,
with one place for timing, step results, and notes.
The adapter split now has a guard too. The
Adapter Boundary page explains the rule in
plain terms: the runtime checks policy, then the OS adapter touches the host.
scripts/check-adapter-boundary.sh checks 34 runtime wrappers and verifies that
the Linux, macOS, and Windows adapter crates expose the matching functions.
Hosted CI and the self-hosted full gate run that check so direct host-call
backsliding is easier to catch.
There is now an Adapter Evidence flow too:
scripts/record-phase2-adapter-evidence.sh records a host report and
scripts/compare-phase2-adapter-evidence.sh compares Linux/macOS/Windows
reports for one commit. Hosted full CI now publishes per-OS adapter evidence
artifacts and runs the compare gate automatically.
The adapter report now includes more than boundary shape: it also runs shared
adapter behavior tests and the native adapter crate tests for that host. This
keeps P2E-02 pointed at real behavior proof instead of only checking that
function names exist.
Sample evidence has a repeatable recorder now too. The
Sample Evidence page explains how to run
scripts/record-phase2-sample-evidence.sh on each desktop host. The recorder
builds the CLI and Rust clock/cat/curl components, runs fixed fixtures, and
writes a markdown report with host metadata, commands, exit codes, stdout
hashes, and output snapshots. That gives the Phase 2 exit review a practical
way to compare Linux, macOS, and Windows sample behavior.
There is also a companion comparator now:
scripts/compare-phase2-sample-evidence.sh wraps
krate-tools --bin compare-phase2-sample-evidence so three host reports can
be checked in one run. It fails fast when clock/cat/curl stdout hashes drift
across hosts and supports a temporary --allow-blocked-curl exception for
restricted localhost environments. That exception is now narrow: if two hosts
run curl successfully and one host is blocked, the successful curl hashes must
still match.
Hosted full CI now also records per-OS sample evidence artifacts and runs a
sample-evidence-compare job automatically. That compare step currently uses
the curl blocked exception, while still enforcing strict cross-host hash parity
for clock and cat outputs plus any successful curl outputs.
Language-variant evidence now has a recorder as well:
Language Variant Evidence explains
how to run scripts/record-phase2-language-variant-evidence.sh. That report
captures fixture readiness plus test outcomes for Rust, Go, and TypeScript in
one markdown artifact, with fixture hashes and log tails for review.
There is now a companion comparator too:
scripts/compare-phase2-language-variant-evidence.sh checks three host reports
in one run, verifies same commit metadata, enforces passed build/test steps,
and fails when fixture availability is not aligned or a present fixture is
missing its hash. It does not require TypeScript fixtures generated by jco on
different operating systems to be byte-identical. This lane proves portable
behavior through Krate, not reproducible compiler output.
Hosted full CI now runs that comparator automatically after Linux, macOS, and
Windows full-test lanes upload their evidence artifacts.
UCap deny evidence now follows the same pattern:
UCap Enforcement Evidence shows how
to record one report per host with scripts/record-phase2-ucap-evidence.sh and
compare those reports with scripts/compare-phase2-ucap-evidence.sh.
The report now includes a named dispatcher matrix that proves every non-default
filesystem and network boundary returns permission denied before an adapter can
touch the host.
Hosted full CI now uploads per-host UCap evidence artifacts and runs a compare
job so deny-path regressions are caught as a cross-host gate, not only in local
checks.
Performance evidence now has the same repeatable flow:
Benchmark Evidence shows how to record
startup and dispatch benchmark results, run baseline regression checks, and
compare Linux/macOS/Windows benchmark reports for commit and gate consistency.
That report now also includes full external CLI startup evidence for
krate run krate-clock, so P2E-10 is tracking the command users will
actually run, not only the in-process runtime path.
The benchmark comparator also enforces per-host threshold bounds from the
metric table, so a host can no longer pass this lane with an over-threshold
metric hidden behind warning-only regression mode.
This gives P2E-10 and P2E-11 clearer cross-host proof tracking even when
raw timing numbers differ by hardware.
Dependency evidence is tracked beside those performance checks:
Dependency Evidence records the
cargo-deny wrapper output, tool versions, and any advisory-database warning
so dependency signoff is explicit instead of buried in CI logs.
The first terminal grant prompt exists too. krate run --prompt app.wasm
shows the app identity, lists missing manifest capabilities, accepts all or a
numbered subset, and stores the approved caps only for that run. In a normal
terminal the same prompt can appear automatically when required capabilities
are missing. In non-interactive runs, Krate keeps the safer behavior and
fails with a clear permission message.
There is now a small manifest trust check as well. If the sidecar manifest says
the app entry is app.wasm, then krate run must be pointed at that same
file. Running a different component with that manifest is rejected before any
grant prompt or runtime execution.
For debugging, krate run --dump-caps app.wasm now prints the effective
session capabilities and exits before the component starts. It is a simple way
to see what the current grant resolution actually produced.
The proof apps are:
krate-curlkrate-catkrate-clock
The Go and TypeScript SDK tracks now also include matching clock/cat/curl sample
sources with CI shape checks. The CLI test harness now also has optional
fixture assertions for Go and TypeScript variants behind KRATE_GO_* and
KRATE_TS_* WASM env vars. We now also run a pre-test fixture build step
(scripts/build-phase2-language-variant-fixtures.sh) in hosted full CI and
self-hosted CI. Hosted full CI now runs that step in ts mode by default and
allows npx install for jco (with a pinned package version). The full-test
matrix also pins Node 22 for this lane, so TypeScript runtime fixtures stay
active without manual runner setup. Go lane status remains explicit.
Hosted CI now also uses Node 24-ready core actions (actions/checkout@v5 and
actions/setup-node@v5) to remove Node 20 deprecation warnings in daily runs.
The same fixture build step now also auto-attempts Go runtime fixture promotion
when go, tinygo, and wasm-tools are available, so both language tracks
use one orchestration entry point.
When fixture mode is set to go or both, that Go promotion path now runs in
strict required mode so missing or non-pure Go fixtures fail that run clearly.
The shared fixture build step now also prints per-language readiness reasons
and includes those reasons in strict mode failures, so CI triage is faster when
language fixture lanes fail on toolchain or fixture-state issues.
TypeScript fixture generation now enforces Krate-only imports and uses the
real WIT variant shape for filesystem open mode ({ tag: "read" }), which made
the TypeScript cat fixture runtime path stable in local tests.
TypeScript curl fixture coverage now also includes non-localhost denial and
unresolved-host error checks, so restricted runners still prove key curl failure
paths even when localhost fixture sockets are unavailable.
TypeScript curl error classification is now aligned with Rust for two critical
failure paths: missing net grant now exits with code 5 and invalid URL now
exits with code 20. The CLI harness now includes explicit Rust versus
TypeScript parity checks for both of those paths.
Go curl fixture coverage now includes matching non-localhost denial and
unresolved-host checks with stable stderr markers.
The Go curl sample now also uses a stable, case-insensitive error classifier
with unit tests for key Krate net error classes, so common failure paths
map to predictable messages and exit codes (5, 20, 21).
The dedicated Go curl fixture tests now assert those same contracts directly,
instead of allowing broad fallback statuses, so parity regressions fail earlier.
The language-variant parity tests now check curl error paths across Rust, Go,
and TypeScript too: missing grant, invalid URL, and unresolved-host cases are
validated together without requiring localhost fixture sockets.
The Go fixture promotion diagnostics are also clearer now. When TinyGo outputs
are not Krate-import pure, the import checker reports every failing Go
artifact instead of stopping at the first one. Current local smoke artifacts
build successfully, but still import WASI host APIs, so they are correctly not
promoted into runtime fixtures yet.
There is now a Go readiness evidence recorder as well. It builds the TinyGo
smoke artifacts, records Go, TinyGo, and wasm-tools versions, hashes the
artifacts, and stores the import-purity log in one review file. This does not
make Go complete. It makes the remaining Go blocker visible enough for a clean
Phase 2 exit decision.
When both Go and TypeScript fixture sets are available, the CLI harness now
also runs cross-language parity checks for krate-clock, krate-cat, and
krate-curl. Those checks run Rust, Go, and TypeScript samples with the same
inputs and assert byte-identical stdout. The curl parity check uses a local
fixture server and skips cleanly on restricted runners that cannot use local
socket fixtures.
Self-hosted CI now runs a TinyGo WASI Preview 2 build-smoke lane for Go
clock/cat/curl samples and then tries to promote those outputs into
test/integration/language-variants/krate_go_*.wasm.
Promotion only happens when import-purity checks pass (krate:* imports
only). If current TinyGo outputs still import host wasi:*, the promotion step
prints a clear skip message and keeps Go runtime fixtures disabled.
Krate-runtime fixture proof for Go therefore remains pending, but it is now
guarded by an explicit readiness gate.
We now also have three language walkthroughs in the docs: Rust, Go, and
TypeScript, so contributors can onboard per language without guessing the
current phase boundaries.
We also now have the first fuzz-harness set for manifest parsing, shared path
normalization, and policy matching. Nightly multi-hour fuzz runs are still a
remaining exit item, but the run path is now configurable without editing
scripts. scripts/run-phase2-fuzz-smoke.sh accepts
KRATE_FUZZ_MAX_TOTAL_TIME (seconds per target), and self-hosted CI now
accepts a matching manual input. There is also a dedicated
Self-hosted Fuzz Nightly workflow that defaults to long runs on local
runners.
The first self-hosted fuzz smoke already paid off by finding and helping fix a
real non-ASCII path-parsing panic in shared path handling.
Timezone fallback coverage also now includes extra Unix timezone file paths
(/etc/TIMEZONE and /var/db/timezone/timezone) in addition to /etc/timezone,
which helps hosts that do not expose timezone only through /etc/localtime.
The closeout material is also drafted now. The Phase 2 retrospective draft and
Phase 3 kickoff issue draft exist, and a small checker keeps both in draft form
until the final exit review is actually done. That checker now runs in hosted
CI, self-hosted CI, and the exit bundle.
If Phase 2 works, those apps should produce the same output on Linux, macOS, and Windows while running through the same Krate runtime model.
If you used the Phase 1 proof app, read Migrating From Phase 1 To Phase 2.
See Plan/Phase-2-Plan.md.