Canonical WIT contracts for Astrid.
This repo is the single source of truth for two kinds of typed contracts: capsule-to-capsule IPC interfaces (interfaces/) and the kernel-to-capsule host ABI (host/). The kernel and every SDK (Rust, JS/TS, Python, Go, etc.) submodule from this repo so contract drift across consumers becomes detectable instead of silent.
The Astrid CLI installs the capsule-to-capsule interfaces to ~/.astrid/wit/astrid/ during astrid init for offline astrid build resolution.
The kernel-to-capsule contract. Capsules import these interfaces; the kernel provides the implementations. Lives outside interfaces/ because the consumer set is fixed (the kernel + each SDK that wraps the imports) rather than open-ended.
Each domain is its own package, frozen at a per-file version. A capsule imports only the domains it uses; bumping one domain does not affect capsules that do not import it.
| File | Package | Description |
|---|---|---|
host/fs@1.0.0.wit |
astrid:fs@1.0.0 |
Filesystem operations within the workspace boundary — whole-file IO, file handles with positional read/write, metadata, canonicalize, read-link, hard-link. |
host/io@1.0.0.wit |
astrid:io@1.0.0 |
Foundation I/O — three interfaces. error: downcastable error resource carried by stream errors. poll: Astrid-owned pollable resource for readiness multiplexing across heterogeneous host resources. streams: input-stream / output-stream resources with read / write / skip / flush / splice for high-throughput host-side byte movement. Shape mirrors wasi:io@0.2.0 but Astrid-owned — every operation is audited, principal-scoped, cancellable, and quota-bounded. |
host/ipc@1.0.0.wit |
astrid:ipc@1.0.0 |
Publish/subscribe IPC event bus. |
host/uplink@1.0.0.wit |
astrid:uplink@1.0.0 |
Inbound message ingestion from external platforms. |
host/kv@1.0.0.wit |
astrid:kv@1.0.0 |
Per-capsule, per-principal key-value storage with atomic compare-and-swap and paginated key listing. |
host/net@1.0.0.wit |
astrid:net@1.0.0 |
Unix-domain sockets, gated outbound TCP, inbound TCP listener, gated UDP (unconnected + connected mode), gated DNS resolution. |
host/http@1.0.0.wit |
astrid:http@1.0.0 |
HTTP client with SSRF protection and streaming. |
host/http@1.1.0.wit |
astrid:http@1.1.0 |
Additive successor to @1.0.0 — caller-set per-request timeouts, redirect policy, response/decompression size caps, scheme restriction, subresource integrity, response metadata, streaming request bodies (uploads), and response trailers. An empty request-options reproduces @1.0.0 behaviour. |
host/sys@1.0.0.wit |
astrid:sys@1.0.0 |
Logging, config, time, caller context, entropy, sleep, capability introspection. |
host/process@1.0.0.wit |
astrid:process@1.0.0 |
OS-sandboxed host process spawn (with stdin/env/cwd), wait, signal, kill, read-logs, stdin streaming. |
host/process@1.1.0.wit |
astrid:process@1.1.0 |
Additive successor to @1.0.0 — host-verified, read-only per-spawn file injection (file-injections on spawn-request). Empty file-injections reproduces @1.0.0 behaviour. |
host/elicit@1.0.0.wit |
astrid:elicit@1.0.0 |
Interactive user input during install/upgrade lifecycle. |
host/approval@1.0.0.wit |
astrid:approval@1.0.0 |
Human-in-the-loop approval gate for sensitive actions. |
host/identity@1.0.0.wit |
astrid:identity@1.0.0 |
Multi-platform identity resolve and link. |
host/guest@1.0.0.wit |
astrid:guest@1.0.0 |
Guest export contract — astrid-hook-trigger, run, astrid-install, astrid-upgrade. Each entry point lives in its own world (interceptor, background, installable, upgradable) so capsules include only what they implement. |
A capsule's world declares only the imports it uses plus the guest-export worlds it actually implements:
// Interceptor-only capsule:
world router {
include astrid:guest/interceptor@1.0.0;
import astrid:ipc/host@1.0.0;
// intentionally not importing net, http, identity, …
}
// Run-loop capsule with an install hook:
world cli {
include astrid:guest/interceptor@1.0.0;
include astrid:guest/background@1.0.0;
include astrid:guest/installable@1.0.0;
import astrid:ipc/host@1.0.0;
import astrid:uplink/host@1.0.0;
import astrid:net/host@1.0.0;
}Per-export worlds matter: the wasm32-wasip2 toolchain auto-stubs every export declared in a world the component targets. Bundling all four entry points into one mandatory world forced stubs for the unused ones, which then required kernel-side parsing to distinguish real implementations from toolchain stubs. With per-export worlds, an export only appears in the wasm binary when the capsule actually implements it.
Once a host/<name>@X.Y.Z.wit file is shipped (i.e. on main), it is immutable forever. The wasmtime Component Model linker enforces structural typing on every (package, version) pair, so any record-field add or function add in a published WIT file causes every capsule built against the prior shape to fail to instantiate. The fix is to never edit a published file in place.
Shape changes ship as a new file at a new version:
host/
ipc@1.0.0.wit # frozen
ipc@1.1.0.wit # frozen (additive change from 1.0.0)
ipc@2.0.0.wit # current (breaking change from 1.x)
To evolve a package:
- Copy the latest frozen file:
cp host/ipc@1.0.0.wit host/ipc@1.1.0.wit. - Bump the package declaration inside the new file:
package astrid:ipc@1.1.0;. - Make your shape changes in the new file.
- Leave the existing frozen file untouched.
- The kernel registers both versions in its linker (
bindings::ipc_v1_0::add_to_linkerandbindings::ipc_v1_1::add_to_linker) so old and new capsules both load.
The rule is currently a documented convention rather than a CI gate. The automated frozen-file check was retired during pre-adoption iteration (no SDK or capsule is bound to @1.0.0 yet, so in-place amendments don't break anyone). Once a real downstream consumer ships against a versioned file, re-enable the check (the original script lives in git history) so accidental edits surface in review.
See RFC: Host ABI for the full design (per-domain packages, multi-version kernel registration, frozen-file rule) and issue #750 for the motivating bug.
The capsule-to-capsule contracts. Distinct namespace (astrid-bus:*) from the host ABI (astrid:*) — the medium is different: host fns are direct wasmtime CM linker calls, bus interfaces are schemas for events that flow over the IPC bus between capsules. Capsules declare which they import/export in Capsule.toml; the kernel validates at boot that every required import has a matching export.
| File | Package | Description |
|---|---|---|
interfaces/llm.wit |
astrid-bus:llm@1.0.0 |
LLM generation requests and streaming responses |
interfaces/session.wit |
astrid-bus:session@1.0.0 |
Conversation session storage and retrieval |
interfaces/spark.wit |
astrid-bus:spark@1.0.0 |
Agent identity and system prompt construction |
interfaces/context.wit |
astrid-bus:context@1.0.0 |
Context window compaction and management |
interfaces/prompt.wit |
astrid-bus:prompt@1.0.0 |
Prompt assembly from components |
interfaces/tool.wit |
astrid-bus:tool@1.0.0 |
Tool dispatch and execution results |
interfaces/hook.wit |
astrid-bus:hook@1.0.0 |
Lifecycle hook fan-out and response collection |
interfaces/registry.wit |
astrid-bus:registry@1.0.0 |
Model registry operations |
interfaces/types.wit |
astrid-bus:types@1.0.0 |
Shared types used across bus interfaces |
interfaces/users.wit |
astrid-bus:users@1.0.0 |
Within-principal user identity store — platform-to-AstridUserId mapping |
(Plus agent, approval, client, elicit, onboarding, system, user — see interfaces/ for the full set.)
Capsules declare which host packages and bus interfaces they import/export in their Capsule.toml. The two namespaces stay distinct:
# Host ABI — kernel-mediated syscalls.
[imports.astrid]
fs = "1.0.0"
ipc = "1.0.0"
# Capsule-to-capsule event schemas, on the IPC bus.
[imports.astrid-bus]
llm = "^1.0"
session = { version = "^1.0", optional = true }
[exports.astrid-bus]
session = "1.0.0"The kernel validates at boot that every required astrid-bus import has a matching astrid-bus export from another loaded capsule. The WIT files in interfaces/ define the message schemas carried over the IPC bus.
SDKs use wit-bindgen (Rust), ComponentizeJS (JS/TS), or equivalent toolchains to generate typed bindings from these definitions. The generated types match the IPC payload schemas so capsule authors get compile-time type safety.
The kernel uses wasmtime::component::bindgen! against each host/<name>@<version>.wit to generate one binding module per (package, version) pair. The kernel's linker setup registers every supported version explicitly — there is no implicit version negotiation in the Component Model.
Both kinds of WIT files change rarely but breakingly. The repos that submodule from here are:
astrid-runtime/astrid-- kernel (host implementations bound to eachhost/<name>@<version>.wit)astrid-runtime/sdk-rust-- Rust SDK (guest bindings + capsule contracts)astrid-runtime/sdk-js-- JavaScript / TypeScript SDK (same)
When a contract changes here, each consumer bumps its submodule pointer. CI lints can compare submodule SHAs across consumers to catch silent drift.
Dual-licensed under MIT and Apache 2.0.
Copyright (c) 2025-2026 Joshua J. Bouw and Unicity Labs.