Summary
Three coordinated changes that have to land together before 1.0 to make the WASM ABI evolvable without breaking every capsule in the ecosystem on every WIT touch. They reinforce each other — landing one without the others is wasted work.
- Split
astrid:capsule@0.1.0 into per-domain packages.
- Multi-version kernel registration (
add_to_linker per supported version).
- Frozen-WIT-file-per-version discipline (never mutate a published WIT file in place).
Motivation
Hit this concretely tonight trying to run sphere-capsule locally. PR #4 (canonical wit) added principal: option<string> to the ipc-message record. PR #5 (and PR #746 syncing it into core) added shutdown-how enum + 14 new functions to net. Both were treated as "additive" but kept the same package version string astrid:capsule@0.1.0. wasmtime's Component Model linker is strict structural typing — any record-field add or function-add is a shape change, so:
- Every installed capsule built against the prior
astrid:capsule@0.1.0 shape fails to instantiate against the new kernel.
- The single mega-package means EVERY capsule that imports ANY interface from
astrid:capsule is affected on EVERY change anywhere in the file.
- Today the only recovery path is rebuild the entire ecosystem against a fresh sdk-rust release. Not viable post-1.0.
The pattern in the linker error (component imports instance 'astrid:capsule/ipc@0.1.0', but a matching implementation was not found in the linker) shipped on day one of any release is a brand-killer.
Scope
1. Split astrid:capsule into per-domain packages
Replace the single astrid:capsule@0.1.0 package containing 11 interfaces (ipc, net, kv, http, fs, process, sys, elicit, approval, uplink, identity) with one package per domain:
astrid:ipc@1.0.0
astrid:net@1.0.0
astrid:kv@1.0.0
astrid:http@1.0.0
astrid:fs@1.0.0
astrid:process@1.0.0
astrid:sys@1.0.0
astrid:elicit@1.0.0
astrid:approval@1.0.0
astrid:uplink@1.0.0
astrid:identity@1.0.0
Plus a minimal astrid:core@1.0.0 for truly cross-cutting types only — principal, caller-context, common error shapes. Keep this package as small as humanly possible; it's the hot path for cross-domain breakage. Every type that lives here ripples across every interface that imports it.
Each domain package owns its own types. astrid:ipc/types (ipc-envelope, ipc-message) lives in astrid:ipc, NOT in a shared types module. Per-interface types isolate per-interface evolution.
Cross-cutting concerns like the principal field on ipc-message need rethinking: either accept that astrid:ipc itself bumps when principal handling changes, OR push principal out of the payload into the kernel's caller-context (resource-typed, more permissive evolution).
Capsules opt into exactly the subset they need:
world my-capsule {
import astrid:ipc/host@1.0.0;
import astrid:kv/host@1.0.0;
// not net, not http — capsule doesn't use them
}
Result: a kv-only capsule is unaffected when astrid:net adds a function.
2. Multi-version kernel registration
wasmtime CM has no version negotiation. The kernel must explicitly register every version it claims to support. Today bindings::Capsule::add_to_linker is called once with one set of bindings; needs to be called once per (package, version) pair the kernel supports:
// pseudocode
bindings::ipc_v1_0::add_to_linker(&mut linker, host_v1_0_handler)?;
bindings::ipc_v1_1::add_to_linker(&mut linker, host_v1_1_handler)?;
bindings::ipc_v2_0::add_to_linker(&mut linker, host_v2_0_handler)?;
// ... per package, per supported version
Implementation requires:
- Build-time codegen: one binding module per (package, version) pair.
- Host-side handlers that convert between version shapes (e.g. v1.0
ipc-message lacks principal — the v1.0 handler ignores it when emitting events to v1.0 capsules and synthesizes it from caller-context when receiving from v1.0 capsules).
- A registered support window per package (e.g. kernel supports
astrid:ipc@{1.0.0, 1.1.0, 2.0.0} simultaneously).
- A deprecation policy: when the kernel drops a version from the support window, capsules built against it stop loading. Drop dates communicated in advance.
3. Frozen-WIT-file-per-version discipline
Once an astrid:ipc@1.0.0 is shipped, the file at interfaces/ipc@1.0.0.wit is immutable forever. New shapes get a new file:
interfaces/
ipc@1.0.0.wit # frozen
ipc@1.1.0.wit # frozen
ipc@2.0.0.wit # current/active
When a shape change is needed: copy the latest frozen file to a new version, modify, register the new version in the kernel, leave the old files alone. The current unicity-astrid/wit pattern of editing the file in place dies.
Enforced by CI lint: any PR that touches a file matching *@X.Y.Z.wit where that version is referenced in a kernel release manifest fails the build. New shapes = new file.
Why-uniquely-Astrid framing
Post-1.0 Astrid is a platform that third-party capsule authors target. Every WIT change is a contract break for those authors. The pre-1.0 "rebuild the whole ecosystem" cost is acceptable from us; it is not acceptable from a third party who shipped their capsule six months ago and didn't touch it since. The fix is the three pieces above; without them, 1.0 is a marketing version number, not a stable contract.
Out of scope
- Resource-typed evolution patterns. Worth exploring as a future v2.x option but not blocking v1.0; record-based ABIs work, they just need the discipline above.
- Cross-language type-shape diff detection (e.g. detect that adding a struct field in sdk-rust broke a JS-consuming capsule). Manual WIT review + the immutability rule covers it.
- Tooling to auto-bump versions on shape change. Authors do the version bump explicitly; CI just enforces the rule.
Concrete deliverables
On unicity-astrid/wit:
On unicity-astrid/astrid:
On unicity-astrid/sdk-rust and unicity-astrid/sdk-js:
Migration of existing first-party capsules:
Open questions
- Single
astrid:core vs no shared types at all. Strictly purer: zero shared types, each interface has its own principal definition. More duplication, less cross-cutting risk. Probably go with minimal astrid:core for sanity, document loudly which types are in it.
- Naming:
astrid:ipc/host@1.0.0 (separate host vs guest worlds) or just astrid:ipc@1.0.0. Worth checking the wasmtime CM idiom.
- Pre-1.0 cleanup approach. Cut over hard (delete
astrid:capsule@0.1.0 from canonical, force every capsule to migrate before 1.0 release) vs leave it as a deprecated alias. Recommend hard cut — pre-1.0 we have license to break, post-1.0 we don't.
- Resource-based evolution for the principal/caller-context piece. Worth one design pass to see if it removes the cross-cutting type entirely. If it does, the
astrid:core package gets even smaller (or vanishes).
Acceptance
- Per-domain WIT files in
unicity-astrid/wit, each frozen-by-version.
- Kernel supports at least two simultaneous versions of one package (smoke test: register
astrid:ipc@1.0.0 and astrid:ipc@1.1.0 together, load a capsule that imports each).
- CI lint blocks edits to frozen WIT files.
- First-party capsules rebuild against per-domain packages and load.
- Documentation in
core/docs/wit-evolution.md walks a capsule author through bumping a package version.
- The current failure mode ("
astrid:capsule/ipc@0.1.0 not found in linker" when sdk-rust and kernel are at different commits) cannot recur, by construction.
Thanks
Thank you BobDowns for discovering this issue, its a big one!
Summary
Three coordinated changes that have to land together before 1.0 to make the WASM ABI evolvable without breaking every capsule in the ecosystem on every WIT touch. They reinforce each other — landing one without the others is wasted work.
astrid:capsule@0.1.0into per-domain packages.add_to_linkerper supported version).Motivation
Hit this concretely tonight trying to run sphere-capsule locally. PR #4 (canonical wit) added
principal: option<string>to theipc-messagerecord. PR #5 (and PR #746 syncing it into core) addedshutdown-howenum + 14 new functions tonet. Both were treated as "additive" but kept the same package version stringastrid:capsule@0.1.0. wasmtime's Component Model linker is strict structural typing — any record-field add or function-add is a shape change, so:astrid:capsule@0.1.0shape fails to instantiate against the new kernel.astrid:capsuleis affected on EVERY change anywhere in the file.The pattern in the linker error (
component imports instance 'astrid:capsule/ipc@0.1.0', but a matching implementation was not found in the linker) shipped on day one of any release is a brand-killer.Scope
1. Split
astrid:capsuleinto per-domain packagesReplace the single
astrid:capsule@0.1.0package containing 11 interfaces (ipc,net,kv,http,fs,process,sys,elicit,approval,uplink,identity) with one package per domain:Plus a minimal
astrid:core@1.0.0for truly cross-cutting types only —principal,caller-context, common error shapes. Keep this package as small as humanly possible; it's the hot path for cross-domain breakage. Every type that lives here ripples across every interface that imports it.Each domain package owns its own types.
astrid:ipc/types(ipc-envelope, ipc-message) lives inastrid:ipc, NOT in a shared types module. Per-interface types isolate per-interface evolution.Cross-cutting concerns like the
principalfield onipc-messageneed rethinking: either accept thatastrid:ipcitself bumps when principal handling changes, OR push principal out of the payload into the kernel's caller-context (resource-typed, more permissive evolution).Capsules opt into exactly the subset they need:
Result: a kv-only capsule is unaffected when
astrid:netadds a function.2. Multi-version kernel registration
wasmtime CM has no version negotiation. The kernel must explicitly register every version it claims to support. Today
bindings::Capsule::add_to_linkeris called once with one set of bindings; needs to be called once per (package, version) pair the kernel supports:Implementation requires:
ipc-messagelacksprincipal— the v1.0 handler ignores it when emitting events to v1.0 capsules and synthesizes it from caller-context when receiving from v1.0 capsules).astrid:ipc@{1.0.0, 1.1.0, 2.0.0}simultaneously).3. Frozen-WIT-file-per-version discipline
Once an
astrid:ipc@1.0.0is shipped, the file atinterfaces/ipc@1.0.0.witis immutable forever. New shapes get a new file:When a shape change is needed: copy the latest frozen file to a new version, modify, register the new version in the kernel, leave the old files alone. The current
unicity-astrid/witpattern of editing the file in place dies.Enforced by CI lint: any PR that touches a file matching
*@X.Y.Z.witwhere that version is referenced in a kernel release manifest fails the build. New shapes = new file.Why-uniquely-Astrid framing
Post-1.0 Astrid is a platform that third-party capsule authors target. Every WIT change is a contract break for those authors. The pre-1.0 "rebuild the whole ecosystem" cost is acceptable from us; it is not acceptable from a third party who shipped their capsule six months ago and didn't touch it since. The fix is the three pieces above; without them, 1.0 is a marketing version number, not a stable contract.
Out of scope
Concrete deliverables
On
unicity-astrid/wit:host/astrid-capsule.witinto 11 per-domain WIT files + 1 minimalastrid:corepackage.astrid:capsule@0.1.0to a deprecated location or delete (no consumers can rely on it post-1.0).interfaces/<name>@<version>.witper frozen file.scripts/lint-wit-immutability.sh— fails if any file matching the frozen-version pattern was modified.astrid:core, or pushed out of payload entirely (resource handle on host side).On
unicity-astrid/astrid:bindings::Capsulegeneration to per-(package, version) modules.engine/wasm/mod.rsadd_to_linkerbecomes a loop over the supported-versions table.supported_versions = [...]for the kernel's package support window.On
unicity-astrid/sdk-rustandunicity-astrid/sdk-js:astrid_sdk::ipc::v1_0, etc.).Migration of existing first-party capsules:
unicity-astrid/capsule-*repo: which interfaces does each actually use?Open questions
astrid:corevs no shared types at all. Strictly purer: zero shared types, each interface has its ownprincipaldefinition. More duplication, less cross-cutting risk. Probably go with minimalastrid:corefor sanity, document loudly which types are in it.astrid:ipc/host@1.0.0(separate host vs guest worlds) or justastrid:ipc@1.0.0. Worth checking the wasmtime CM idiom.astrid:capsule@0.1.0from canonical, force every capsule to migrate before 1.0 release) vs leave it as a deprecated alias. Recommend hard cut — pre-1.0 we have license to break, post-1.0 we don't.astrid:corepackage gets even smaller (or vanishes).Acceptance
unicity-astrid/wit, each frozen-by-version.astrid:ipc@1.0.0andastrid:ipc@1.1.0together, load a capsule that imports each).core/docs/wit-evolution.mdwalks a capsule author through bumping a package version.astrid:capsule/ipc@0.1.0not found in linker" when sdk-rust and kernel are at different commits) cannot recur, by construction.Thanks
Thank you BobDowns for discovering this issue, its a big one!