Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(kernel)!: migrate to per-domain WIT host ABI - #752

Merged
joshuajbouw merged 20 commits into
mainfrom
feat/wit-per-domain-split
May 25, 2026
Merged

joshuajbouw merged 20 commits into
mainfrom
feat/wit-per-domain-split

Conversation

@joshuajbouw

@joshuajbouw joshuajbouw commented May 22, 2026

Copy link
Copy Markdown
Member

Linked Issue

Closes #751.

Summary

Kernel-side migration to the per-domain WIT host ABI. The legacy bundled astrid:[email protected] world is replaced by typed-func dispatch over the split per-domain packages (astrid:[email protected], astrid:[email protected], astrid:[email protected], astrid:[email protected], …). Every host call now routes through typed error-code variants, the wasmtime ResourceTable (no parallel HashMap storage), audit envelopes per domain, and cancellation-token races on every blocking path. No wasi:* interfaces are exposed to capsules anywhere — readiness multiplexing and byte streams are Astrid-owned (astrid:io/poll, astrid:io/streams), so the hermit-rs unikernel target stays viable on the same WIT contract.

The migration is paired with unicity-astrid/sdk-rust#44 and the 18 unicity-astrid/capsule-*#feat/per-domain-wit PRs that flip every capsule to wasm32-unknown-unknown against this kernel. End-to-end LLM round-trip verified through the pure-astrid stack: astrid run "say hi" → real LM Studio response.

Changes

Foundation

  • WIT submodule. wit/ is now a submodule of unicity-astrid/wit. build.rs in astrid-capsule stages the per-domain layout under wit-staging/deps/astrid-<pkg>/ for wasmtime::component::bindgen!. CI workflows check the submodule out recursively.
  • bindings.rs. Single bindgen! over an inline kernel world that imports every host package — one generated module keeps the type universe deduplicated. imports: { "astrid:io/streams": trappable } + trappable_error_type lowers stream-error to wasmtime-wasi-io's runtime enum so the streams impl can delegate cleanly.
  • No wasi: linker registration.* Both engine/wasm/mod.rs load paths and astrid-hooks/handler/wasm.rs removed the wasmtime_wasi::p2::add_to_linker_sync call. Capsules see exactly what the per-domain WIT describes and nothing more. configure_kernel_linker is the single source of truth shared between the load path and the lifecycle-hook path.
  • astrid-build target-agnostic. Drops the hardcoded --target wasm32-wasip2; reads whatever the capsule's .cargo/config.toml selects, probes both wasm targets when locating the artifact, and runs wit_component::ComponentEncoder over wasm32-unknown-unknown outputs to wrap them into a Component Model component in place.
  • wasmtime 43 → 45. Closes RUSTSEC-2026-0149 (wasmtime-wasi 43.0.2 path_open(TRUNCATE) FilePerms::WRITE bypass). No call-site changes required; the WasiCtx / WasiView / DynPollable / DynInputStream / DynOutputStream / IoError types we depend on are stable across the 43→45 window.

Per-domain host impls

Real implementations with typed errors + audit envelope + cancel-token races:

Package Status
astrid:kv full — kv-get/set/delete/list-keys/list-keys-page/clear-prefix/cas (atomic compare_and_swap plumbed through KvStore / ScopedKvStore in this branch)
astrid:sys full — get-config/get-caller/log/signal-ready/clock-ms/clock-monotonic-ns/sleep-ns/random-bytes/check-capsule-capability. trigger-hook removed (moved to capsule bus); audit-logged
astrid:uplink full — uplink-register/uplink-send with typed UplinkProfile
astrid:approval full — ApprovalResponse carries the typed ApprovalDecision enum; allowance hits map to Allowance
astrid:elicit full — typed ElicitType enum, ElicitResponse variant
astrid:identity full — typed errors (UserNotFound, LinkNotFound, AlreadyLinked, StoreUnavailable)
astrid:http http-request full (SSRF airlock + safe DNS resolver preserved); http-stream.read-chunk, status, headers, close real
astrid:io/poll full — Astrid-owned with 256-pollable cap, audit, cancel-token race on block
astrid:io/error full — downcastable error resource (to-debug-string)
astrid:io/streams full read/write/skip/check-write/flush/splice via wasi-sync delegation; cancel + audit + per-call bytes accounting wraps every op
astrid:net bind-unix, UnixListener.{accept, poll-accept} (session-token handshake), connect-tcp (SSRF airlock), lookup-host (airlocked DNS), TcpStream byte methods + 20 socket options
astrid:ipc publish, publish-as, subscribe returning Resource<Subscription>, Subscription.{poll, recv} with cancel + per-message principal context install
astrid:fs path-based ops on the new FileStat shape (kind/mode/modified/created/accessed) including fs-mkdir-all
astrid:process spawn (sync), spawn-background returning Resource<ProcessHandle>, ProcessHandle.{read-logs, wait, kill, os-pid, signal}. Marked desktop-only per the WIT

Stubbed for documented follow-ups (return Unknown("port pending") or CapabilityDenied — no panics):

  • bind-tcp / TcpListener (inbound capsule-hosted TCP)
  • udp-bind / UdpSocket (datagram I/O)
  • tcp-stream.{read-stream, write-stream} + http-stream.body-stream (wasmtime-wasi-io InputStream/OutputStream adapter — pairs with capsule-hosted TCP work)
  • subscribe-* pollables on every resource (the pollable adapter lands as one commit covering everything)
  • fs-open + FileHandle resource (positional pread/pwrite, fsync, set-len)
  • fs-stat-symlink, fs-append, fs-copy, fs-rename, fs-remove-dir-all, fs-canonicalize, fs-read-link, fs-hard-link
  • ProcessHandle.{write-stdin, close-stdin, wait-with-output} — unblocked in a follow-up to support a capsule-level MCP server. MCP servers communicate via JSON-RPC over stdio; for a capsule-mcp to spawn an MCP server subprocess and drive it, the kernel needs stored stdin pipes (Stdio::piped() + ManagedProcess field), a background write task, and an atomic-drain wait-with-output. Out of scope for feat(kernel)!: migrate to per-domain WIT host ABI #752 itself.

HostState shape

Removed (ResourceTable is canonical storage now):

  • active_streams: HashMap<u64, NetStream> + next_stream_id
  • subscriptions: HashMap<u64, EventReceiver> + next_subscription_id
  • background_processes: HashMap<u64, ManagedProcess> + next_process_id

Added (O(1) quota-gate counters maintained on insert/drop, per Gemini review):

  • net_stream_count: usize
  • subscription_count: usize
  • process_count_total: usize
  • process_count_by_principal: HashMap<PrincipalId, usize>

Engine init, lifecycle init, test_fixtures, and the hook handler all updated.

Audit channels

  • astrid.audit.fs — path-based fs ops
  • astrid.audit.io — stream + pollable ops (read/write/splice/block/poll)
  • astrid.audit.net — TCP / Unix socket ops
  • astrid.audit.ipc — publish/subscribe/poll/recv
  • astrid.audit.http — request / stream ops
  • astrid.audit.process — spawn / spawn-background

Every event carries capsule_id, principal (effective per-invocation), op name, and a domain-appropriate payload.

Why Astrid-owned and not wasi:io

The wasmtime-wasi Host impl skips four things Astrid considers non-negotiable for security:

  • Cancellation. pollable.block() and stream.blocking-read() strand the host task on a future that may never complete when the capsule unloads. Astrid's wrappers race against cancel_token.
  • Audit. Every read/write/poll/splice is invisible to the audit log. Astrid emits per-call events.
  • Per-principal accounting. No quota dial on pollable / stream handles, no rate limit on poll-loop spam.
  • Uniformity. A carve-out for "foundation types" undermines defense-in-depth.

Owning the namespace also matters for the hermit-rs unikernel target: contract stays stable, host-side impl swaps for native unikernel wait/io primitives.

Review fixups landed in this PR

3-agent review + Gemini review surfaced the following, all addressed in-branch:

  • ipc::recv mixed-principal batches truncated at the first publisher boundary (truncate_to_homogeneous_principal).
  • TcpStream::write propagates peer-disconnect IO kinds as ErrorCode::ConnectionReset instead of swallowing them as Ok(()).
  • TcpStream::read cancellation returns Closed (not Pending).
  • spawn_background registers the spawned PID in ProcessTracker; the drop path unregisters.
  • Subscription resource handle stays valid across multiple recv calls (EventReceiver behind Arc<Mutex<...>>).
  • read_file re-checks payload size post-read for TooLarge (eliminates pre-stat TOCTOU).
  • ProcessHandle::wait uses spawn_blocking(child.wait) raced against tokio::time::timeout.
  • unix_listener::accept 100ms back-off on credential failure.
  • All count_* resource-table iteration replaced with O(1) counter fields on HostState.
  • HTTP per-chunk timeout extracted to HTTP_STREAM_READ_TIMEOUT.
  • 21 new unit tests covering the fixes.
  • Atomic kv_cas through KvStore / ScopedKvStore in astrid-storage (was originally deferred; landed in this branch).

Test Plan

Automated

  • cargo test --workspace passes
  • cargo build --workspace clean
  • cargo clippy --workspace --all-features -- -D warnings clean
  • cargo test -p astrid-capsule --lib — 272 passed, 0 failed (includes 21 new regression tests)
  • wasmtime 45.0.0 bump verified — all tests pass, no API breakage at our call sites

Manual

  • End-to-end LLM round-trip: astrid run "say hi in one short sentence" returns a real LM Studio response, full pure-astrid stack (router → session → react → openai-compat → http → LM Studio), zero wasi:* imports anywhere, daemon survives astrid restart cycles.

Out of scope (separate PRs)

  • Stream-half adapters (read-stream / write-stream / body-stream / FileHandle) — paired with capsule-hosted TCP server work
  • Pollable wiring for subscribe-* methods
  • bind-tcp, UdpSocket (capsule-hosted networking)
  • MCP-driven stdio: ProcessHandle.{write-stdin, close-stdin, wait-with-output} real impls + ManagedProcess stdin-pipe storage + write-task plumbing. Required for capsule-mcp to drive JSON-RPC-over-stdio MCP servers as subprocesses.
  • Restoring ipc_tests.rs against the new Subscription-resource shape

Lands the foundation for the new per-domain host ABI from
unicity-astrid/wit PR #7. The kernel workspace compiles end-to-end
against the split contracts; per-domain implementations port back
incrementally in follow-up commits.

What landed:

- wit/ is now a submodule of unicity-astrid/wit. build.rs in
  astrid-capsule stages the per-domain WIT into wit-staging/deps/ so
  wasmtime::component::bindgen! can resolve cross-package use clauses
  (wasi:io/[email protected] etc.).

- engine/wasm/bindings.rs: single bindgen! over a synthetic kernel
  world that imports every host package (astrid:[email protected],
  astrid:[email protected], ...). One generated module keeps types
  deduplicated. wasi:io/poll is reused via the with: map; a thin
  forwarder on HostState delegates poll::Host + HostPollable to
  HostState.resource_table.

- Per-export guest worlds: the kernel no longer instantiates against
  a bundled Capsule world. linker.instantiate() returns an Instance
  and exports (astrid-hook-trigger, run, astrid-install,
  astrid-upgrade) are looked up by name with get_typed_func.

- Host trait impls rewritten to the typed error-code variants:
  - kv: full port (cas emulated via get-then-set pending storage CAS)
  - sys: trigger_hook removed (moved to astrid-bus); random_bytes,
    clock_monotonic_ns, sleep_ns implemented
  - approval: ApprovalResponse now carries the typed ApprovalDecision
    enum; allowance hits map to Decision::Allowance
  - elicit: typed ElicitType / ElicitResponse variants
  - identity: typed errors (UserNotFound / LinkNotFound /
    AlreadyLinked / StoreUnavailable / etc.)
  - uplink: typed UplinkProfile enum
  - http: full http_request port + HttpStream resource scaffolding;
    SSRF airlock + safe DNS resolver preserved

- Host trait stubs (impl shape + types correct; bodies return todo!()
  pending follow-up commits that port back the previous impl):
  - fs: FileHandle resource + 9 new path-based fns
  - ipc: Subscription resource (publish/publish_as ported)
  - net: UnixListener/TcpListener/TcpStream/UdpSocket resources
  - process: ProcessHandle resource

- astrid-hooks/handler/wasm.rs: same Kernel::add_to_linker +
  get_typed_func pattern.

Known TODOs (each their own PR):

- Port fs/ipc/net/process implementations into the new resource model
- SDK-rust + 19 capsule repos: WIT submodule + Capsule.toml
  [imports.astrid]/[imports.astrid-bus] + guest world updates
- Restore ipc_tests.rs against the Subscription-resource API
…1.0.0

Tracks the WIT fix at unicity-astrid/wit#fix/astrid-io-poll-namespace.
The host ABI is now fully Astrid-owned: no wasi:* interfaces are
exposed to capsules. Every readiness operation routes through Astrid
host code with audit, cancel-token, and per-principal accounting.

What changes:

- wit/ submodule bumped to fix/astrid-io-poll-namespace
  (9d71a05 feat astrid:io/[email protected]).

- engine/wasm/bindings.rs: drops the wasi:io/poll `with:` mapping,
  imports astrid:io/[email protected] in the synthetic kernel world. The
  pollable resource storage type is still wasmtime_wasi::p2::DynPollable
  (it is just a Future, not a syscall — keeping the storage type lets
  us re-use the wasmtime executor for now; hermit-rs swap is a future
  port-back). The Host trait is OURS, not wasmtime-wasi's.

- engine/wasm/host/io.rs (new): implements astrid::io::poll::Host and
  HostPollable on HostState. Hard cap of 64 pollables per poll call.
  Both poll() and block() race the calling capsule's cancellation
  token — capsule unload always wins over a stuck future. Audit
  events emitted under target="astrid.audit.io" with principal,
  capsule, count, and elapsed wait.

- engine/wasm/host/mod.rs: deletes the wasi_poll_forward module added
  during the initial scaffolding pass. wasi:io is no longer reachable
  from the kernel\'s linker setup.

- engine/wasm/mod.rs + astrid-hooks/handler/wasm.rs: removes
  `wasmtime_wasi::p2::add_to_linker_sync` from both load paths. That
  call would have exposed the full wasi-p2 stack (filesystem, sockets,
  cli, clocks, random, http) directly to capsules, bypassing the VFS,
  SSRF airlock, sys.random_bytes audit, log routing, and capability
  allowlists. The host ABI is now exactly what the per-domain WIT
  describes and nothing more.

Forward-compat note: when Astrid ships as a hermit-rs unikernel, the
host/io.rs impl swaps wasmtime-wasi-io futures for hermit\'s native
wait primitives. WIT contract unchanged; capsules don\'t see the
mechanism.
Matches the upstream wit cleanup (cfe24c0 chore(deps): drop vendored
wasi-io). The host ABI is self-contained; build.rs no longer copies
wit/deps/wasi-io/ into the staging tree because that source path no
longer exists. The Astrid bindgen world has no wasi:* imports, so
the wit-staging/deps/ directory is populated solely from host/
package files now.
… on tcp/http

Tracks the wit branch through 13daba9, which:
- adds astrid:io/error (downcastable error resource) and
  astrid:io/streams (input-stream / output-stream / splice)
- wires tcp-stream.{read-stream, write-stream} and
  http-stream.body-stream for high-throughput byte movement
  (capsule-hosted TCP proxies, HTTP body forwarding)
- intentionally does NOT wire stream halves on process-handle:
  the IPC bus already handles capsule-to-capsule throughput,
  write-stdin/read-logs cover all realistic child stdio
  patterns, and the unikernel target has no fork/exec — process
  is a desktop-only package

Kernel-side scaffolding in this commit:

- engine/wasm/bindings.rs: imports astrid:io/[email protected] and
  astrid:io/[email protected] in the synthetic kernel world. `with:`
  map reuses wasmtime-wasi-io storage types (IoError,
  DynInputStream, DynOutputStream) — same trick as DynPollable.
  The Host traits themselves are ours, not wasmtime-wasi's.

- engine/wasm/host/io.rs:
  - astrid_error_impl: trivial `to_debug_string` delegating to
    the underlying IoError stored in the resource table.
  - astrid_streams_impl: STUB shell — HostInputStream and
    HostOutputStream impls return `StreamError::Closed` for every
    operation. Real byte-movement integration lands in a follow-up
    alongside the resource accessors below. Subscribe methods are
    `unimplemented!()` pending pollable wiring.

- engine/wasm/host/net/mod.rs: tcp-stream gains stub
  read_stream / write_stream methods (todo!() pending the splice
  wiring).
- engine/wasm/host/http.rs: http-stream gains stub body_stream.
- engine/wasm/host/process.rs: no changes — the stdin/stdout/stderr
  methods I tentatively added in an earlier iteration came out
  alongside the wit trim.

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
…L_LIST to 256

WIT branch fix/astrid-io-poll-namespace squash-merged as
astrid-runtime/wit@324d4ab. Submodule pointer moves c33fd3e -> 324d4ab.

The wit change also raised astrid:io/[email protected]'s per-call cap from
64 to 256 (Gemini caught the inconsistency with ipc's 128-subscription
quota). Matching the kernel-side hard ceiling (MAX_POLL_LIST) so a
capsule polling its full IPC quota plus stream pollables isn't
rejected at the kernel boundary while the WIT promises 256.

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
Replaces the StreamError::Closed stubs in host/io.rs with a thin
Astrid envelope around wasmtime-wasi-sync's stream implementations.
The kernel now does real byte movement on input-stream / output-stream
resources backed by DynInputStream / DynOutputStream — the same
storage type wasi-sync uses (re-shared via the bindgen 'with:' map).

What the envelope adds on top of wasi-sync:

- Cancellation: every method short-circuits with StreamError::Closed
  when the calling capsule's cancel-token has fired. Capsule unload
  always wins over a stuck future. Streams don't have a typed
  'cancelled' variant on the wire — the closed semantic ('no more
  bytes will be produced / accepted') is what the capsule should
  observe.
- Audit: each read / blocking-read / write / blocking-write-and-flush
  / splice / blocking-splice emits a tracing event under target =
  'astrid.audit.io' with capsule + principal + bytes + elapsed.
  High-volume non-fallible methods (check-write / flush / skip /
  subscribe / drop) intentionally skip per-call audit.
- Error conversion: convert_stream_error lowers the runtime
  StreamError (Closed / LastOperationFailed(wasmtime::Error) /
  Trap) into our bindgen-generated astrid:io/streams.stream-error
  wire variant via wasi-sync's conversion plus a rep re-tag on the
  Error resource handle (same underlying IoError storage type).

Bindgen config:

- 'trappable_error_type: { "astrid:io/streams.stream-error" =>
  wasmtime_wasi::p2::StreamError }' lowers the wire variant to the
  runtime enum for the Host trait signatures.
- 'imports: { "astrid:io/streams": trappable }' is what actually
  triggers the trait sig rewrite. Without that one-liner, bindgen
  still emits the bindgen-variant signatures and the delegation
  doesn't type-check.

Forward-compat: when Astrid ships as a hermit-rs unikernel, the
inner wasi-sync calls swap for native unikernel I/O. The audit /
cancel / conversion envelope is unchanged.

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
…table model

Largest of the host-impl ports. The legacy active_streams HashMap on
HostState is gone — the wasmtime ResourceTable is now the canonical
storage for NetStream (Unix accept / outbound TCP). Resource<TcpStream>
handles are just table reps; drop / lifetime / cross-capsule isolation
ride wasmtime's machinery.

Live surface (real impl, ported from the legacy code on origin/main):

- bind-unix + UnixListener.{accept, poll-accept}: kernel-pre-bound
  listener with peer-credential + session-token handshake. Returns
  Resource<TcpStream> backed by NetStream::Unix.
- connect-tcp: DNS resolve -> SSRF airlock -> tokio::TcpStream::connect
  under a bounded timeout. Capability gate (net_connect allowlist)
  checked before DNS so the resolver never sees denied hosts.
- lookup-host: airlocked DNS lookup, returns filtered "ip:port"
  strings (empty list = all results dropped through the airlock).
- TcpStream byte methods: read / write (length-prefixed),
  read-bytes / write-bytes (raw), peek (TCP only), shutdown.
- TcpStream socket options: peer-addr, local-addr, nodelay,
  read-timeout, write-timeout, hop-limit (TTL), keepalive, linger,
  reuseaddr — all backed by socket2::SockRef where tokio lacks the
  setter.

Stubbed (port-back follow-ups, all return CapabilityDenied so capsules
fail closed):

- bind-tcp / TcpListener inbound (planned alongside UDP).
- udp-bind / UdpSocket (connected + unconnected modes).
- tcp-stream.{read-stream, write-stream} (needs wasmtime-wasi-io
  InputStream/OutputStream adapter over our NetStream type — separate
  commit so the splice path lands with proper readiness wiring).
- subscribe-readable / subscribe-readiness pollables (same follow-up).

Other changes:

- HostState: removed active_streams + next_stream_id fields. All call
  sites (engine init, lifecycle init, test fixtures, hook handler)
  updated.
- net/mod.rs split into per-resource submodules (mod.rs / tcp_stream
  / unix_listener / tcp_listener / udp_socket) so no file crosses the
  1000-line CI threshold. stream.rs + handshake.rs unchanged.
- All errors now typed net::ErrorCode variants:
  WouldBlock, ConnectionRefused, ConnectionReset, Timeout,
  AddressInUse, AddressNotAvailable, NameUnresolvable,
  AirlockRejected, CapabilityDenied, Quota, NotTcp, InvalidHandle,
  Closed, Unknown(string).

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
Subscriptions are now first-class wasmtime resources. subscribe
allocates an EventReceiver against the kernel event bus, stores it
in the capsule's resource table as a SubscriptionEntry, and hands
back Resource<Subscription>. Drop / lifetime / cross-capsule
isolation rides wasmtime's resource machinery — the legacy
HostState.subscriptions HashMap is gone.

Live surface:

- publish / publish-as: audit envelope (target = astrid.audit.ipc,
  capsule + principal + topic + bytes).
- subscribe(topic_pattern) -> Resource<Subscription>: 256-byte cap,
  segment validation, mid-segment wildcard rejection, per-capsule
  MAX_SUBSCRIPTIONS = 128 cap, ipc_subscribe ACL enforced.
- Subscription.poll: non-blocking drain into IpcEnvelope with
  per-message principal context install + mixed-batch truncation.
- Subscription.recv(timeout_ms): blocks up to 60s, races cancel_token.
- get-interceptor-bindings: metadata only — handle-id informational.

Stubbed: Subscription.subscribe_readiness — pollable wiring lands
with the dedicated streams + pollables commit.

Engine cleanup: HostState.subscriptions and next_subscription_id
fields removed. Interceptor pre-registration no longer allocates an
EventReceiver per binding.

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
…handle pattern

http_stream.read_chunk now emits an audit event under target =
astrid.audit.http per call (capsule + principal + bytes + elapsed).
Matches the pattern net/streams use. status / headers / read_chunk
also switched from Resource::new_own (which conceptually moves) to
Resource::new_borrow when reading from the table — the original guest
handle stays valid.

body_stream and subscribe_readable remain todo!() — both depend on
the wasmtime-wasi-io InputStream/Pollable adapter work that comes in
a dedicated commit alongside net's stream halves (so the splice path
for HTTP-body-to-TCP-stream lands with proper readiness wiring on
both sides simultaneously).

cargo build --workspace ✓
cargo clippy --workspace -- -D warnings ✓
…dle stubbed

Seven most-used fs functions ported with typed errors and the new
FileStat shape: fs-exists, fs-mkdir, fs-readdir, fs-stat, fs-unlink,
read-file, write-file. Each goes through the per-principal security
gate + VFS resolution + audit envelope (target = astrid.audit.fs).

Module split mirrors net/: mod.rs (Host impl + glue), resolve.rs
(path scheme resolution + boundary canonicalize), file_handle.rs
(HostFileHandle stub).

Stubbed for follow-up: fs-open + FileHandle resource methods,
fs-mkdir-all, fs-stat-symlink, fs-append, fs-copy, fs-rename,
fs-remove-dir-all, fs-canonicalize, fs-read-link, fs-hard-link. All
return Unknown(port pending) so capsules can fall back to the
basic path-based API.

cargo build --workspace ok
cargo clippy --workspace -- -D warnings ok
Desktop-only package. The kernel port:

- spawn (synchronous): real impl with sandbox wrapping, capability gate,
  ProcessTracker call_id-scoped cancellation, stdout/stderr capture,
  ExitInfo mapping.
- spawn-background: real impl. Returns Resource<ProcessHandle> backed
  by ManagedProcess in the wasmtime ResourceTable. Per-capsule cap of 8
  plus per-principal sub-budget. Reader threads drain stdout/stderr
  into 1 MiB ring buffers.
- ProcessHandle.{read-logs, wait, kill, os-pid, signal}: real impls.
- ProcessHandle.{write-stdin, close-stdin, wait-with-output,
  subscribe-exit, subscribe-logs}: stubbed pending follow-ups.

Module split (mirrors net/, fs/): tracker / managed / handle / mod.
HostState.background_processes and next_process_id removed — the
ResourceTable is canonical storage.

cargo build --workspace ok
cargo clippy --workspace -- -D warnings ok
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request performs a major architectural migration of the Astrid kernel to a per-domain WIT host ABI. By decoupling host interfaces and removing reliance on vendored wasi:* interfaces, the change significantly hardens the security boundary between capsules and the host. The migration introduces typed error handling, standardized resource management via wasmtime::ResourceTable, and comprehensive audit logging, ensuring that all host operations are consistently monitored and cancellable.

Highlights

  • WIT ABI Migration: Migrated the kernel to a per-domain WIT host ABI, replacing the legacy bundled astrid:[email protected] world with split, domain-specific packages.
  • WASI Removal: Removed all wasi:* interface exposures to capsules, ensuring the host ABI is fully Astrid-owned for improved security and defense-in-depth.
  • Resource Management: Refactored HostState to use wasmtime::ResourceTable for canonical storage, eliminating parallel HashMap caches for streams, subscriptions, and processes.
  • Security & Audit: Introduced per-call audit envelopes and cancellation token races across all host interfaces to ensure robust security and lifecycle management.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


The ABI was messy and old, With secrets and logic untold. We split up the WIT, To make it all fit, And now it is secure and quite bold.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements a major refactor of the Astrid kernel's host ABI, migrating from a single monolithic WIT world to a sharded, per-domain package structure under the astrid:* namespace. Key changes include the introduction of a ResourceTable-based storage model for subscriptions, network streams, and processes, replacing manual HashMap tracking in HostState. The wasi:* dependency is removed in favor of Astrid-owned readiness multiplexing and system primitives. Feedback highlights several critical areas for improvement: the delete/push pattern in IPC receive calls invalidates guest resource handles, necessitating a move to Arc<Mutex<T>> for stable references; the O(N) iteration of the ResourceTable for connection counting should be replaced with an atomic counter for better scalability; IO errors in framed writes are currently swallowed and should be explicitly propagated; and the emulated CAS operation in the KV store lacks atomicity in multi-threaded environments and should be moved to the storage backend.

Comment thread crates/astrid-capsule/src/engine/wasm/host/ipc.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/host/net/mod.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/host/net/tcp_stream.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/host/kv.rs
…elpers

install_recv_invocation_context / clear_recv_invocation_context are now
called from Subscription.{poll, recv} in the ipc port (commit fb4e84c);
the dead_code allows were leftover from the stubbed-ipc state and are
no longer needed.

The remaining #[allow(dead_code)] tags in net/mod.rs (TcpListenerSlot,
UdpSocketSlot) and process/managed.rs (ManagedProcess.command) cover
legitimately-unused types that pair with the stubbed bind-tcp / udp-bind
trait impls and a future operator-facing diagnostics surface. Both are
documented at the call site as TODO-or-delete in their respective
follow-up commits.

cargo clippy --workspace -- -D warnings ok
Multi-agent review surfaced findings across the per-domain WIT split.
All critical, important, and nit-level items addressed; pure-function
regression tests added for the load-bearing fixes.

Critical:
- ipc::recv batches truncated at first publisher-principal boundary
  so tail messages can't be mis-stamped with the head's principal
  context (truncate_to_homogeneous_principal + 6 unit tests).
- TcpStream::write surfaces peer-disconnect IO kinds as
  ErrorCode::ConnectionReset instead of swallowing them as Ok(())
  (map_write_frame_err + 6 unit tests pinning the kind->code mapping).
- TcpStream::read cancellation returns NetReadStatus::Closed (not
  Pending) so cancelled run-loops terminate instead of busy-looping.
- spawn_background registers the spawned PID with ProcessTracker and
  ProcessHandle::drop unregisters; tool.v1.request.cancel can now
  reach backgrounded children (4 ProcessTracker contract tests).
- Subscription resource handles stay valid across multiple recv
  calls -- EventReceiver moved behind Arc<Mutex<...>> so the resource
  table entry is borrowed (not deleted-and-re-pushed) per call.
- read_file re-checks post-read payload size for TooLarge (eliminates
  pre-stat TOCTOU).

Important:
- ProcessHandle::wait now uses tokio::task::spawn_blocking(child.wait)
  raced against tokio::time::timeout instead of the 50ms try_wait
  busy-loop.
- unix_listener::accept retries credential-rejected connections with a
  100ms back-off (prevents CPU-pinned spin against hostile peers).
- spawn_background re-checks cancel_token after capability gate.
- read tcp-stream audits every call (previously omitted on cancel).

Nits:
- HTTP per-chunk timeout extracted to HTTP_STREAM_READ_TIMEOUT.
- build.rs invalidates wit-staging on .gitmodules changes.
- STUB_PRONE_EXPORTS doc flags risk if SDK ever adds astrid-hook-trigger
  to its mandatory stub set.
- host/mod.rs doc comment updated from Capsule::add_to_linker to
  Kernel::add_to_linker / configure_kernel_linker.
- Dead let export_fn = export_name alias removed in mod.rs lifecycle
  dispatch.
- Stale wasi_streams_imports placeholder removed from host/stubs.rs.
- Dead drain_receiver explicit-auto-deref clippy regressions fixed.

CHANGELOG.md updated with a per-fix breakdown.

Test summary: 21 new unit tests, all passing. Full astrid-capsule
suite: 272 passed, 0 failed.
CI was failing across check / clippy / test / msrv because the wit/
submodule was never checked out — astrid-capsule's build.rs panicked
with 'read wit/host: No such file or directory'. Added
'submodules: recursive' to every actions/checkout in ci.yml that
builds astrid-capsule.

Per Gemini's PR #752 review:

- MAX_ACTIVE_STREAMS / MAX_SUBSCRIPTIONS / MAX_BACKGROUND_PROCESSES
  quota gates now read O(1) counter fields on HostState rather than
  iterating the entire ResourceTable on every accept / connect /
  subscribe / spawn-background. New fields:
    net_stream_count: usize
    subscription_count: usize
    process_count_total: usize
    process_count_by_principal: HashMap<PrincipalId, usize>
  Each successful resource insert bumps the counter; the matching
  drop impl decrements via saturating_sub. Per-principal sub-budgets
  for spawn-background use the HashMap keyed on creator (entry stays
  alive while count > 0, removed when count hits 0).
- count_net_streams / count_subscriptions / count_processes helpers
  removed (no remaining callers).

The remaining Gemini finding — kv_cas non-atomicity across capsules
— is left as a documented TODO. The fix requires plumbing a real
compare_and_swap primitive through KvStore / ScopedKvStore in
astrid-storage; that's an astrid-storage scope change, separate
issue to follow.

Also: pre-existing test-only clippy regressions cleaned up
(field_reassign_with_default in two principal-enabled tests,
explicit_auto_deref in a fixture builder, io::Error::other in a
new test). CHANGELOG.md updated.

Tests: cargo test -p astrid-capsule --lib — 272 passed, 0 failed.
Clippy: cargo clippy --workspace --all-features -- -D warnings — clean.
…modules

Closes the kv_cas atomicity gap flagged by Gemini's PR #752 review.

KvStore trait
-------------
- New `async fn compare_and_swap(&self, namespace, key, expected, new)
  -> StorageResult<bool>`. Returns Ok(true) when the swap landed,
  Ok(false) when the predicate didn't match (now or by commit time),
  Err only for I/O / validation failures. `expected = None` means
  insert-if-absent.

MemoryKvStore
-------------
- Single write lock covers the entire read+conditional-write — other
  capsules calling set / compare_and_swap on the same store block
  until the CAS returns, so the compare cannot race a concurrent
  mutation.

SurrealKvStore
--------------
- Backend-level `tokio::sync::Mutex<()>` guards the full
  begin+read+set+commit sequence. SurrealKV's
  Transaction::validate_write_conflicts reads the memtable *before*
  Core::commit acquires its write mutex, so two concurrent commits
  can both pass validation and both succeed — observable as a
  multi-winner race in the new concurrent test.
- Conflict-detection variant matched against
  surrealkv::Error::TransactionWriteConflict /
  ::TransactionRetry (no more string scraping that a future error
  message rewording could silently break).

ScopedKvStore
-------------
- New `compare_and_swap(key, expected, new)` method delegating to
  the inner store under the bound namespace.

capsule host
------------
- kv_cas now calls kv.compare_and_swap directly; the get-then-set
  emulation + TODO comment removed.

Tests
-----
- Per-impl tests for the basic cases (insert-if-absent matches /
  doesn't match, replace when expected matches / differs, missing
  key + Some(expected) → false).
- Multi-thread tokio runtime concurrent test on both backends: 16-32
  tasks racing CAS(Some(b"0") → "winner-{i}") on the same key.
  Exactly one must win. (Required prefixing per-task values so a task
  writing "0" can't leave the store in its initial state.)

Refactor
--------
- kv.rs was 1226 lines after the CAS addition — over the project's
  1000-line CI ceiling — so split into kv/mod.rs (validators, helpers,
  trait, re-exports) + kv/memory.rs + kv/surreal.rs + kv/scoped.rs.
  Public API preserved verbatim via pub use.
The host fn now routes through the same VFS::mkdir call as fs-mkdir,
which every VFS impl (host, overlay, worktree) already implements via
std::fs::create_dir_all. The WIT contract distinguishes the two fns
only by idempotence semantics (fs-mkdir-all succeeds if the dir
already exists, fs-mkdir is strict); the recursive-creation behaviour
was already there for both, just not exposed through fs-mkdir-all.

Tightening fs-mkdir to true non-recursive / fail-if-exists semantics
is a separate behaviour change tracked as a follow-up.

Capability gating (gate_write), audit envelope
(astrid:fs/host.fs-mkdir-all), and error mapping match fs-mkdir.

Unblocks the system capsule's opportunistic improvement from the
post-PR#752 capsule migration batch (on_install can now use
fs::create_dir_all for the home://skills/capsule-development scaffold).

Closes one item of #753.
…apsules

Lands the core-side groundwork to make wasm32-unknown-unknown the
Astrid-canonical guest target. After this PR, the kernel exposes zero
wasi:* — capsules that historically targeted wasm32-wasip2 and relied
on auto-injected wasi:* imports will fail to load until they migrate
via the upcoming SDK + capsule sweep (separate PR cluster blocked on
this).

Breaking
--------
- KernelRequest / KernelResponse / CommandInfo / CapsuleMetadataEntry /
  DaemonStatus / SYSTEM_SESSION_UUID moved from astrid_types::kernel
  to astrid_core::kernel_api. astrid-types is the WASM-compatible
  shared-types crate intended to compile on wasm32-unknown-unknown
  for capsule SDK consumption; it cannot depend on astrid-core
  (which references PrincipalId, Quotas, etc.). The kernel-management
  RPC surface doesn't belong in a WASM-compatible crate to begin with.

  Migration: replace 'astrid_types::kernel::' with
  'astrid_core::kernel_api::'. astrid-cli, socket_client, admin_client,
  TUI, integration tests all updated. astrid-events keeps a
  pub-use re-export under 'astrid_events::kernel_api' for ergonomics.

- chrono workspace dep is now default-features=false, features=[serde].
  The 'clock' feature pulls wasm-bindgen + js-sys on
  wasm32-unknown-unknown, which inject __wbindgen_placeholder__
  imports that wit-component refuses to round-trip into a Component
  Model component. Records that need clock values get them from
  astrid_sdk::time (audited host fn); DateTime<Utc> remains as a
  serializable field shape.

Changed
-------
- configure_kernel_linker no longer registers
  wasmtime_wasi::p2::add_to_linker_sync. The kernel ABI is pure
  astrid:*. A wasm32-wasip2 capsule that auto-injected wasi:* imports
  will fail to load with 'interface not found'; that is the intended
  posture, not a bug.

Added
-----
- astrid-build is target-agnostic. Drops the hardcoded --target
  wasm32-wasip2 from cargo build; lets the capsule's own
  .cargo/config.toml select the target. Probes
  target/wasm32-unknown-unknown/release/ first, then
  wasm32-wasip2/release/, then the workspace root. When the produced
  artifact is a core wasm module (no Component Model layer; what
  wasm32-unknown-unknown produces), wit_component::ComponentEncoder
  wraps it into a Component Model component before packaging.

- astrid:fs/host.fs-mkdir-all unstubbed (already pushed in 297bd73,
  included for completeness). Closes one item of #753.

Notes
-----
- This commit intentionally leaves astrid:[email protected] in the canonical
  WIT contract. Dropping it cleanly is a follow-up — it requires the
  SDK to stop importing it AND the capsule sweep to land. Pending
  decision (rename vs delete) doesn't block this PR.

- Downstream PR sequence after this lands:
  - sdk-rust: getrandom backend cleanup, panic handler in prelude,
    document approved Rust subset for wasm32-unknown-unknown.
  - 17 capsule branches: switch .cargo/config.toml target to
    wasm32-unknown-unknown, rebuild, reinstall.
  - Final: drop astrid:[email protected] from canonical WIT once SDK + capsules
    have migrated.
End-to-end verification of the post-PR#752 'capsules speak only
astrid:*' invariant. Daemon up with 17 capsules loaded, every
component's WIT import list is pure astrid:*, zero wasi:* anywhere.

Changes (on top of 518cbd7's wasi-elimination foundation):

astrid-types: clock feature
- Gates IpcMessage::new() and the timestamp serde default behind a
  Cargo 'clock' feature. Default OFF so the crate compiles on
  wasm32-unknown-unknown for capsule SDK consumption (chrono's clock
  pulls wasm-bindgen). Kernel-side consumers enable it via their dep
  declaration; capsule-side gets a Unix-epoch default for absent
  timestamps — capsules read timestamps from kernel-published
  messages, never construct fresh ones.

workspace uuid:
- default-features=false, features=['v4','v5','serde','rng-getrandom'].
  Default features select a 'js' RNG on wasm32-unknown-unknown (via
  wasm-bindgen). rng-getrandom routes v4 generation through
  getrandom, satisfied by astrid-sys's custom backend on capsule
  builds. Same wiring needed in sdk-rust's astrid-sdk (separate repo
  PR, lands in parallel).

astrid-build: in-place component wrap
- ensure_component overwrites the original .wasm artifact instead of
  writing a sibling .component.wasm. Keeps Capsule.toml
  '[[component]] file = "<crate>.wasm"' directives resolving with no
  per-target conditional logic. The toolchain hides which target
  produced the artifact from the manifest layer.

System state after this commit:
- core/wit submodule: unchanged
- core kernel binary: zero wasmtime_wasi linker registration (see 518cbd7)
- All 17 capsules (agents, cli, context-engine, fs, hook-bridge,
  http, identity, memory, openai-compat, prompt-builder, react,
  registry, router, session, shell, skills, system) installed on
  wasm32-unknown-unknown via 'astrid capsule install'
- 'wasm-tools component wit' on each installed component: only
  astrid:* imports
- 'astrid status': 17 capsules loaded ready
- ~/.astrid/ state preserved across the kernel binary swap
@joshuajbouw

Copy link
Copy Markdown
Member Author

/gemini review

@joshuajbouw

Copy link
Copy Markdown
Member Author

@gemini-code-assist /review

Branch state has moved significantly since the original Gemini pass. The four prior comments were on earlier commit SHAs:

  • ipc.rs:413 (273f055) — delete/push invalidates resource handle → fixed in c62d590 (Arc<Mutex<EventReceiver>>)
  • net/mod.rs:78 (c62d590) — O(N) ResourceTable iteration for stream count → fixed in 784e825 (O(1) counter on HostState)
  • tcp_stream.rs:71 (273f055) — swallowed IO errors in framed write → fixed in c62d590 (map_io_err propagation, ConnectionReset surfaced)
  • kv.rs:125 (6824516) — non-atomic CAS → fixed in c67b6f8 (KvStore::compare_and_swap plumbed atomically through ScopedKvStore via storage-native primitive)

Fresh pass against HEAD welcome.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Astrid kernel's host ABI into per-domain WIT packages, removing all wasi:* dependencies to ensure a fully audited host surface. Key improvements include a target-agnostic build process, atomic KV compare-and-swap, and outbound TCP support for capsules. Review feedback identifies a security vulnerability in the SSRF protection logic where loopback addresses are not explicitly blocked and suggests that fs_mkdir should be updated to use a non-recursive method to maintain strictness as intended by the WIT contract.

Comment thread crates/astrid-capsule/src/engine/wasm/host/net/mod.rs
Comment thread crates/astrid-capsule/src/engine/wasm/host/fs/mod.rs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request executes a major architectural migration of the Astrid kernel's host ABI to a sharded, per-domain WIT structure, removing all wasi:* dependencies in favor of audited astrid:* interfaces and moving management API types to astrid-core. Key functional additions include outbound TCP support, idempotent directory creation, and atomic compare-and-swap operations for the KV store. Feedback identifies a critical issue in the process management logic where the wait operation consumes the child process handle without returning it on timeout or cancellation, which could lead to resource leaks. The reviewer recommends transitioning to tokio::process::Child to enable safe asynchronous waiting while ensuring that process handles remain available for subsequent cleanup or status checks.

Comment thread crates/astrid-capsule/src/engine/wasm/host/process/handle.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/host/process/managed.rs Outdated
Comment thread crates/astrid-capsule/src/engine/wasm/host/process/mod.rs
wasmtime-wasi 43.0.2 has a path_open(TRUNCATE) bypass of
FilePerms::WRITE host restriction (CVE filed as GHSA-2r75-cxrj-cmph).
Impact on Astrid is structurally bounded — capsules can't import
wasi:filesystem; the host ABI is fully astrid:* and routes file ops
through astrid:fs (VFS-mediated) — but the audit gate fails the
gate regardless of reachability.

wasmtime 45.0.0 is the latest stable major. Verified clean across
the full surface area:

- WasiCtx / WasiCtxBuilder / WasiView / WasiCtxView<'_> in host_state.rs
- DynPollable / DynInputStream / DynOutputStream / IoError /
  StreamError + bindings::sync::io::streams::Host in io.rs + ipc.rs
- Resource-type mappings in bindings.rs

No call-site changes needed — the 43→45 window is a clean bump
for the API surface astrid touches. cargo test --workspace passes;
cargo clippy --workspace --all-features -- -D warnings clean.

Bumping past 44.0.2 (also patched) directly to 45.0.0 keeps us on
the live release line rather than the patch backport.
…strict fs-mkdir

Addresses the two live Gemini review threads on #752 (the rest were
stale, referencing SHAs that pre-date later in-branch fixes).

ProcessHandle ownership (high-priority)
- ManagedProcess.child: std::process::Child → tokio::process::Child.
  Reason: std::Child::wait() consumes by value, forcing the previous
  impl to .take() the child into spawn_blocking. On timeout the
  blocking task kept the child alive, the resource slot was None,
  and Drop / kill / read-logs all saw Closed — zombie risk on cancel.
  tokio::Child::wait(&mut self) races against tokio::time::timeout
  without ownership transfer; on timeout the child stays in the slot.
- spawn-background converts the prepared std::Command to a
  tokio::Command via the From impl, sets kill_on_drop(true) so the
  runtime reaps zombies if ManagedProcess is dropped mid-flight.
- Reader threads → tokio tasks (AsyncReadExt::read).
- kill_and_reap: start_kill (sync) + try_wait.
- Tokio::Child::id() returns Option<u32> — every caller updated
  to surface ErrorCode::Closed on None.

fs-mkdir strict semantics (medium-priority)
- Every VFS impl routes mkdir() through create_dir_all, so fs-mkdir
  and fs-mkdir-all had identical behaviour despite the WIT contract
  distinction. Added a pre-check on fs-mkdir: if the relative parent
  is non-empty and doesn't exist, return ErrorCode::NotFound before
  invoking the VFS.

Stale findings (no changes needed):
- ipc.rs:413 → c62d590 (Arc<Mutex<EventReceiver>>)
- net/mod.rs:78 O(N) count → 784e825 (HostState::net_stream_count)
- tcp_stream.rs:71 swallowed errors → c62d590 (map_write_frame_err)
- kv.rs:125 non-atomic CAS → c67b6f8 (KvStore::compare_and_swap)
- net SSRF 127/8 → already covered by is_loopback() + octets[0]==127

Verified: cargo test --workspace, cargo clippy --workspace
--all-features -- -D warnings, end-to-end LLM round-trip.
@joshuajbouw
joshuajbouw merged commit be00b76 into main May 25, 2026
13 checks passed
@joshuajbouw
joshuajbouw deleted the feat/wit-per-domain-split branch May 25, 2026 11:31
joshuajbouw added a commit that referenced this pull request May 25, 2026
…s Closed (#760)

## Linked Issue

Closes #759

## Summary

Fix the `TcpStream::read_bytes` / `peek` cancellation hole flagged by
Gemini on #758 (`CHANGELOG.md:48` thread), plus a CHANGELOG
`[Unreleased]` cleanup that consolidates duplicate Added/Changed blocks
from the independent #752 + #746 roll-ups. Goes in before #758 so the
0.7.0 release inherits the corrected behaviour and cleaner CHANGELOG
structure.

## Changes

### `crates/astrid-capsule/src/engine/wasm/host/net/tcp_stream.rs`

- `read_bytes`: `None => Ok(Vec::new())` → `None =>
Err(ErrorCode::Closed)`
- `peek`: `result.unwrap_or(Ok(Vec::new()))` →
`result.unwrap_or(Err(ErrorCode::Closed))`

Both methods previously collapsed cancellation into an empty `Vec<u8>`,
which is indistinguishable from a clean EOF in byte-stream reads
(`std::io::Read::read` / `tokio::io::AsyncReadExt::read` convention).
The framed read path (`read_frame`) and the write side (`write_bytes`,
`shutdown`) already return `Err(ErrorCode::Closed)` on cancellation.
`read_bytes` / `peek` were the outliers.

Capsules with EOF-triggered finalizers (write trailers, send
last-message IPC, flush log, transition to "stream complete" state)
would execute those finalizers under a forced unload when the cancel
fires mid-read. The bug surface is narrow (the capsule has milliseconds
to live) but it's a real correctness hole. Now `Closed` distinguishes
cancellation from EOF; empty `Vec` keeps its "clean EOF" meaning (no
behaviour change for the EOF case).

### `CHANGELOG.md` (`[Unreleased]` consolidation)

- Merged the two `### Added` blocks (one from #752, one from #746) into
a single block. Same for `### Changed`.
- New `### Fixed` entry for the `read_bytes` / `peek` change above.
- No new entries removed — purely a structural cleanup.

## Test Plan

### Automated

- [x] `cargo test -p astrid-capsule --lib` — 272 passed, 0 failed
- [x] `cargo clippy --workspace --all-features -- -D warnings` clean

### Manual

- [x] N/A — the cancellation path only fires on capsule unload,
exercised in integration

## Checklist

- [x] Linked to an issue
- [x] CHANGELOG.md updated under `[Unreleased]`
joshuajbouw added a commit that referenced this pull request May 25, 2026
## Linked Issue

Closes #757

## Summary

Bump all workspace crates from 0.6.0 to 0.7.0. Big release rolling up the
per-domain WIT host ABI migration (#752 — wasi-elimination, every host
call routed through audited astrid:* interfaces), outbound TCP for
capsules (#746), wasmtime 43 → 45 (closes RUSTSEC-2026-0149), atomic
kv_cas, O(1) HostState quota counters, the Gemini review fixups for
#752, and the TcpStream cancellation hole closed in #760.

## Changes

- Workspace version 0.6.0 → 0.7.0
- All 20 workspace dependency versions updated to 0.7.0
- CHANGELOG [Unreleased] rolled into [0.7.0] (consolidation already
  done in #760 — Breaking, Added, Changed, Fixed all single-section)
joshuajbouw added a commit that referenced this pull request May 25, 2026
## Linked Issue

Closes #757

## Summary

Bump all workspace crates from 0.6.0 to 0.7.0. Big release rolling up
the per-domain WIT host ABI migration (#752 — wasi-elimination, every
host call routed through audited `astrid:*` interfaces), outbound TCP
for capsules (#746), wasmtime 43 → 45 (closes RUSTSEC-2026-0149), atomic
`kv_cas`, O(1) `HostState` quota counters, and the Gemini review fixups
landed since 0.6.0 was tagged.

## Changes

- Workspace version 0.6.0 → 0.7.0
- All 20 workspace dependency versions updated to 0.7.0
- CHANGELOG `[Unreleased]` rolled into `[0.7.0]` with consolidated
sections — duplicate Added/Changed blocks (from #752 + #746
accumulation) merged into a single canonical set, `wasi:*` elimination
moved from Changed to Breaking, three new entries for the Gemini #752
follow-up commits (tokio::process::Child, strict fs-mkdir, wasmtime 45
bump). Order: Breaking, Added, Changed, Fixed.

## Test Plan

### Automated

- [x] `cargo check --workspace` passes

### Manual

- [ ] Tag `v0.7.0` after merge
- [x] Release CI builds cross-platform binaries

## Checklist

- [x] Linked to an issue
- [x] CHANGELOG.md updated under `[0.7.0]`
joshuajbouw added a commit that referenced this pull request May 25, 2026
The release workflow's build matrix fails to compile astrid-capsule
because its build.rs reads from wit/host/ (the unicity-astrid/wit
submodule) which actions/checkout@v4 leaves uninitialised by default:

  error: failed to run custom build command for `astrid-capsule v0.7.0`
    read wit/host: Os { code: 2, kind: NotFound, message: "No such file or directory" }

ci.yml got the same fix in #752 (submodules: recursive on every job
that compiles the workspace). release.yml was missed because the
v0.6.0 release predated the per-domain WIT migration — first
release-build attempt that needed the submodule was v0.7.0, which
just failed exactly this way.

The github-release job (line 83) does not need the submodule — it
only downloads the build artifacts and creates the GitHub Release.
joshuajbouw added a commit that referenced this pull request May 25, 2026
…now() (#762)

## Linked Issue

Closes #761

## Summary

Two release-pipeline gaps surfaced by the 0.7.0 deploy attempt, both
metadata-only and grouped here because they both block the same publish:

1. **`cargo publish` fails for 10 workspace crates** that call
`Utc::now()` / `DateTime::format` — workspace feature unification masks
a missing chrono `clock` feature flag. Standalone publish verifier
doesn't get the unification, the crate compiles without `clock`, and
`Utc::now()` doesn't exist.
2. **The Release workflow's build matrix fails** to compile
`astrid-capsule` because `actions/checkout@v4` leaves the `wit/`
submodule uninitialised. Same root cause as the ci.yml fix in #752;
release.yml was missed because v0.6.0 predated the per-domain WIT
migration.

## Changes

### 10 crate Cargo.tomls — explicit `chrono` `clock` feature

Additive — keeps `serde` from the workspace baseline, adds `clock` on
top of the crate-local declaration:

```toml
chrono = { workspace = true, features = ["clock"] }
```

Affected crates: `astrid-approval`, `astrid-capabilities`, `astrid-cli`,
`astrid-core`, `astrid-events`, `astrid-hooks`, `astrid-openclaw`,
`astrid-storage`, `astrid-telemetry`, `astrid-workspace`.

`astrid-types` correctly gates `Utc::now()` behind `#[cfg(feature =
"clock")]` and does not need this fix.

### `.github/workflows/release.yml` — submodule checkout in build job

```yaml
- uses: actions/checkout@v4
  with:
    submodules: recursive
```

Only the `build` job needs it; `github-release` just downloads
artifacts.

## Release coordination

`v0.7.0` is already tagged but nothing reached crates.io (verifier
failed on `astrid-core`) and no release binaries shipped (build failed
on `astrid-capsule`). After this merges, force-update `v0.7.0` to the
merge commit. The Release workflow re-runs on the moved tag and produces
working binaries; `cargo workspaces publish --from-git` then succeeds.
No version bump — the published crate contents under 0.7.0 are
functionally identical (Cargo.toml + workflow metadata only).

## Test Plan

- [x] `cargo check --workspace` passes
- [x] `cargo publish -p astrid-core --dry-run --allow-dirty` succeeds
(previously failed E0599 on `Utc::now`)
- [ ] Release workflow re-runs successfully on the moved `v0.7.0` tag
(post-merge)

## Checklist

- [x] Linked to an issue
- [ ] CHANGELOG.md updated — N/A, no functional change
joshuajbouw added a commit to astrid-runtime/sdk-rust that referenced this pull request May 25, 2026
## Summary

Migrate `astrid-sdk` + `astrid-sys` to the per-domain WIT host ABI
introduced in `astrid-runtime/astrid#752`, and make
`wasm32-unknown-unknown` the canonical build target.

## Changes

### `astrid-sys` — bindings + entropy backend

- Per-domain WIT staging in `build.rs`: copies each
`host/<pkg>@<ver>.wit` into `wit-staging/deps/astrid-<pkg>/` so
`wit_bindgen::generate!` can resolve the layout.
- Synthetic `capsule` world supplied inline (no on-disk world file) —
imports every host package and includes every guest export world.
- `__getrandom_v03_custom` extern routes `getrandom 0.4` entropy through
`astrid:sys/host.random-bytes`, gated on `target_arch = "wasm32"` AND
`getrandom_backend = "custom"` (the rustflag every capsule's
`.cargo/config.toml` sets).
- Drops the vendored `astrid-capsule.wit` — contract lives in the
`contracts/` submodule, fully Astrid-owned (`astrid:*` only, no
`wasi:*`).

### `astrid-sdk` — typed wrappers + panic hook

- Every domain module (`fs`, `ipc`, `net`, `process`, `kv`, `sys`,
`time`, `http`, `approval`, `elicit`, `identity`, `uplink`) ported to
the new typed-`error-code` host ABI and resource-handle returns.
- Resource handles (`Subscription`, `ProcessHandle`, `HttpStream`,
`TcpStream`, `UnixListener`) carry Drop semantics — closing the handle
is automatic on scope exit.
- `install_panic_handler()` sets a process-wide `panic::set_hook`
(once-only) that routes Rust panics through `astrid:sys/host.log` at
`error` level so kernel-side audit captures them — `astrid-sdk-macros`
calls this at the entry of every Guest export method (`run`,
`astrid-hook-trigger`, `astrid-install`, `astrid-upgrade`).
- `astrid_sdk::time::monotonic()` exposes the audited host clock —
capsules MUST use this instead of `std::time::Instant::now()` (which
panics on `wasm32-unknown-unknown`).

### Build defaults

- `default-features = false` on `chrono` / `uuid` workspace deps; `uuid`
uses `rng-getrandom` so it picks up the SDK-provided custom backend
instead of pulling `wasm-bindgen`.
- `[patch.crates-io]` points `astrid-types` at
`../core/crates/astrid-types` for in-tree builds.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side per-domain WIT migration
- All `unicity-astrid/capsule-*` PRs landing the `feat/per-domain-wit`
branch

## Test Plan

- [x] `cargo build --workspace` clean
- [x] `cargo clippy --workspace --all-features -- -D warnings` clean
- [x] All 17 deployed capsules build against this SDK on
`wasm32-unknown-unknown`
- [x] End-to-end LLM round-trip verified: `astrid run "say hi"` → real
LM Studio response
joshuajbouw added a commit to astrid-runtime/sdk-js that referenced this pull request May 25, 2026
## Summary

Migrate `@astrid-os/sdk` (JS/TS) to the per-domain WIT host ABI
introduced in `astrid-runtime/astrid#752`, paired with
`astrid-runtime/sdk-rust#44`. Keeps the JS and Rust capsule contracts in
lockstep so a capsule author sees the same surface and the same type
names across both languages.

## Changes

### Per-domain bindings + wrappers

Every domain module (`fs`, `ipc`, `net`, `process`, `kv`, `sys`, `time`,
`http`, `approval`, `elicit`, `identity`, `uplink`) ported to the new
typed-`error-code` host ABI:

- WIT bindings (`wit-imports.d.ts`) regenerated against the per-domain
packages — `astrid:fs/[email protected]`, `astrid:ipc/[email protected]`,
`astrid:net/[email protected]`, `astrid:io/[email protected]`,
`astrid:io/[email protected]`, `astrid:io/[email protected]`, etc.
- Resource handles (`UnixListener`, `TcpListener`, `TcpStream`,
`UdpSocket`, `Subscription`, `BackgroundProcessHandle`, `HttpStream`)
are Component Model resources with `Symbol.dispose` for `using`
scope-bound cleanup.
- Typed `SysError` mirrors the Rust SDK's `SysError::HostError(String)`
shape; per-domain typed errors get formatted via the same Debug-style
conversion at the SDK boundary.

### WIT submodule + mirror

- `contracts/` is the `unicity-astrid/wit` submodule (shared with
sdk-rust).
- `scripts/sync-contracts-wit.sh` mirrors `contracts/interfaces/*.wit` →
`packages/astrid-sdk/wit-contracts/astrid-contracts.wit` (the
published-package physical-location requirement). CI gate runs
`--check`.

### Post-migration cleanups (mirroring sdk-rust)

- Drop `TcpStream.setTtl` / `TcpStream.ttl` back-compat aliases —
pre-migration names for `setHopLimit` / `hopLimit` with zero callers in
capsule code. Parallel to `unicity-astrid/sdk-rust@f90012c`.
- Route `TcpStream.recv`'s 50ms poll-interval through the audited
`astrid:sys/host.sleep-ns` (via `time.sleepMs`) instead of busy-spinning
on `clockMs`. The kernel can now cancel the wait when the capsule
unloads and account for the wait in audit. Parallel to
`unicity-astrid/sdk-rust@81c85c1` (`std::thread::sleep` →
`crate::time::sleep`).
- Refresh `astrid-contracts.wit` from canonical (same drift sdk-rust
corrected earlier in the branch).

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side per-domain WIT migration
- `astrid-runtime/sdk-rust#44` — Rust SDK migration
- All `unicity-astrid/capsule-*#feat/per-domain-wit` PRs (capsule layer)

## Test Plan

- [x] `npm run build` clean across the workspace (`packages/astrid-sdk`
+ `packages/astrid-build` + `examples/test-capsule`)
- [x] `examples/test-capsule` componentizes via `componentize-js`
against the new bindings — 12.44 MB / 154 host imports, all `astrid:*`
(zero `wasi:*` from capsule POV)
- [x] `scripts/sync-contracts-wit.sh --check` passes
- [ ] End-to-end smoke once an actual JS capsule lands against this SDK
(Rust-side `astrid run "say hi"` already verified for the cross-SDK
kernel)
joshuajbouw added a commit to unicity-aos/capsule-agents that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-cli that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-context-engine that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-fs that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-hook-bridge that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-http that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-identity that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-memory that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-openai that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-openai-compat that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-prompt-builder that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-react that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-registry that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-router that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-session that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-shell that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-skills that referenced this pull request May 26, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/capsule-system that referenced this pull request May 29, 2026
## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `astrid-runtime/astrid#752`
(paired with `astrid-runtime/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `astrid-runtime/astrid#752` — kernel-side migration
- `astrid-runtime/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`
joshuajbouw added a commit to unicity-aos/aos-ce that referenced this pull request Jul 13, 2026
* feat: native OpenAI LLM provider capsule

Talks directly to OpenAI's Chat Completions API with support for:
- Strict function calling (strict: true on tool definitions)
- Reasoning effort for o-series models (low/medium/high)
- Service tier routing (auto/default/flex/priority)
- max_completion_tokens (OpenAI's preferred field)
- Parallel tool calls

Separate from openai-compat which handles generic OpenAI-compatible
providers. This capsule is OpenAI-specific.

* feat: native OpenAI provider — Responses API, model registry, repo setup (#1)

## Summary

Complete native OpenAI LLM provider capsule using the **Responses API**
(`POST /v1/responses`), not the legacy Chat Completions endpoint.

## Changes

- **Responses API**: `input` + `instructions` schema, named SSE events
(`event: response.output_text.delta`), `reasoning.effort` nested object
- **Model registry**: Built-in lookup table for all current OpenAI
models (GPT-5.4/mini/nano/pro, GPT-5.3 Codex, GPT-5.2, GPT-4.1 series,
o-series, GPT-4o legacy). Selecting a model auto-resolves context
window, max output tokens, vision/tools/reasoning support. Env vars
override.
- **Strict function calling**: `strict: true` on all tool definitions
- **Reasoning-aware temperature**: Temperature only sent when reasoning
effort is `none` (GPT-5.4 constraint). Older reasoning models skip
temperature entirely.
- **Net capabilities**: Locked to `api.openai.com` hostname only
- **Repo setup**: README, dual MIT/Apache-2.0 licenses, GitHub Actions
release workflow with BLAKE3 hashes

## Test plan

- [ ] Build: `cargo build --target wasm32-wasip1 --release`
- [ ] Install and test with OpenAI API key against GPT-5.4
- [ ] Verify model registry resolves context_window correctly for each
model family
- [ ] Verify reasoning_effort only sent for reasoning models
- [ ] Verify temperature skipped when reasoning effort is non-none

* chore: add CI workflow (#2)

Add fmt + clippy + WASM build checks on PRs and main pushes. Matches
other capsule repos.

* feat: initial Telegram Bot uplink capsule

Bridges the Telegram Bot API to the Astrid kernel IPC bus.

Features:
- Streaming responses with throttled message editing
- Markdown to Telegram HTML conversion
- Approval/elicitation inline keyboards
- Session management with KV persistence
- Access control via user ID allowlist
- Bot commands: /start, /help, /reset, /cancel

* fix: align with astrid-sdk 0.5.3 API

- Use Response.json()/text() instead of .status/.body fields
- Add description parameter to elicit::secret/text_with_default
- Fix formatting (rustfmt --edition 2024)
- Suppress expected dead_code warnings on deserialized fields

* fix: target wasm32-wasip1 to match astrid build system

Add .cargo/config.toml and rust-toolchain.toml matching other capsules.

* fix: remove install hook to avoid double-prompting

Capsule.toml [env] section already handles prompting for bot_token and
allowed_user_ids during install. The #[astrid::install] hook was
duplicating the same prompts.

* fix: use wildcard net capability to match kernel security gate

The kernel's Extism-based security gate doesn't support URL-pattern
net capabilities like 'https://api.telegram.org/*'. Use '*' wildcard
matching the pattern used by capsule-openai-compat.

* fix: add net capability to component section

The kernel's Extism-based security gate checks per-component capabilities,
not the top-level [capabilities] section. Without capabilities on the
[[component]], network access is denied at runtime.

* fix: unwrap SDK HTTP response envelope before parsing Telegram JSON

http::send() returns a JSON envelope {status, headers, body} where body
is the actual HTTP response as a string. Parse the envelope first, then
deserialize the Telegram API response from the body field.

* docs: fix WASM target in README (wasip1, not wasip2)

* fix: multi-user approval/elicitation routing (#11)

* fix: use session_id from payload for multi-user approval/elicitation routing

Remove find_chat_for_event which silently dropped approval and elicitation
events when multiple users had active turns. Instead, extract session_id
directly from the IPC payload and resolve the target chat via session_to_chat.
Falls back to single-active-turn heuristic only when no session_id is present.

Closes #1

* fix: drop events with unresolvable session_id instead of misrouting

Extract resolve_chat_from_payload helper to deduplicate chat resolution
logic between approval_required and elicit_request handlers.

The previous or_else fallback would use the single-active-turn heuristic
even when session_id was present but unresolvable (stale or unknown),
which could misroute events. Now we only fall back to the heuristic when
session_id is truly absent from the payload; if it is present but cannot
be resolved, the event is dropped.

* fix: propagate uplink::register error instead of silently discarding it (#15)

Closes #2

* fix: exponential backoff on Telegram API failures (#12)

* fix: add exponential backoff on persistent Telegram API poll failures

Replace the fixed 2-second sleep on Telegram poll errors with
exponential backoff (2s, 4s, 8s, ... up to 60s). The counter resets
on every successful poll, preventing runaway retry storms during
prolonged outages.

Closes #3

* fix: use non-blocking backoff to avoid stalling IPC during Telegram errors

Replace std::thread::sleep backoff with a next_poll_at timestamp so the
main loop continues processing IPC events while Telegram polling is
deferred. Adds a 50ms tick at the end of the loop to prevent
busy-spinning when polls are skipped.

* fix: TTL-based cleanup for turns and pending_approvals memory leak (#17)

Add created_at and last_activity timestamps to TurnState and created_at
to PendingApproval. Introduce TURN_TIMEOUT and APPROVAL_TTL constants
(300s each) and a Phase C cleanup pass in the main loop that evicts
stale entries, preventing unbounded HashMap growth.

Expired turns collect their IDs first, then retain removes them, and
finally Telegram API calls notify users — avoiding mutable borrow
conflicts during I/O (per Copilot review feedback). Turn timeout is
based on last_activity (updated on stream deltas and approval events)
so long-running turns with ongoing activity are not prematurely reaped.

Closes #4

* fix: check HTTP status codes and monitor IPC dropped messages (#14)

* fix: check HTTP status codes and monitor IPC dropped messages

Check envelope.status in parse_response before parsing Telegram JSON:
429 returns a clear rate-limit error, >=500 surfaces the server error
with a truncated body, and >=400 attempts to extract the Telegram error
description. Also log a warning when the IPC poll envelope reports
dropped messages so stale responses are visible in logs.

Closes #5, closes #7

* fix: UTF-8 safe truncation and shared unwrap_envelope helper

- Replace &envelope.body[..200] byte-slicing with chars().take(200)
  to avoid panicking on multi-byte UTF-8 boundaries.
- Extract unwrap_envelope() helper that checks HTTP status (429, 5xx,
  4xx) and returns the body string on success.
- Refactor both parse_response and edit_message_text to use the shared
  helper, so edit_message_text now properly checks HTTP status too.

* fix: enum parsing, Capsule.toml bloat, link double-escape, dead code (#18)

- #6: Fix handle_elicitation_request — look for field_type as an object
  with "Enum" key containing array instead of broken string equality
  check; add !buttons.is_empty() guard
- #8: Trim Capsule.toml ipc_publish and ipc_subscribe to only the
  topics the code actually uses (removed 11 unused topic patterns)
- #9: Add html_unescape helper in format.rs; unescape URL before
  re-escaping for href to prevent double-escape; add tests
- #10: Remove unused session_id field from PendingApproval; remove
  dead code in handle_final_response; add unit tests for
  parse_allowed_users, is_user_allowed, new_session_id

* fix: address all findings from second code review (#19)

* fix: address all 18 findings from second code review

High:
- #17: Text elicitations now tracked in pending_elicitations map;
  user text replies routed as elicit_response instead of new turn

Medium:
- #1: getUpdates switched from GET with unencoded query to POST with JSON body
- #4: Elicitation callbacks validated against pending_elicitations state
- #5: KV errors now logged as warnings instead of silently discarded
- #9: Graceful exit after 50 consecutive IPC poll errors
- #14: Callback data truncated to respect Telegram's 64-byte limit
- #16: Unconditional 50ms sleep now only runs during backoff

Low-Medium:
- #11: text_buffer capped at 256KB to prevent WASM OOM
- #7: uplink::register kept (kernel bookkeeping) with comment
- #13: net=[*] kept (kernel rejects URL patterns) with comment

Low:
- #10: Turn timeout uses collect+remove instead of double retain (TOCTOU fix)
- #18: Approval decision text now HTML-escaped
- Pending elicitation TTL cleanup added alongside approvals

* fix: address Copilot review feedback on PR #19

- Approval request_id truncated with floor_char_boundary (UTF-8 safe),
  and pending_approvals keyed by the truncated id so lookups match
- IPC error counter reset on any Ok (including empty polls)
- Text buffer cap enforced strictly: partial append up to MAX_TEXT_BUFFER

* fix: address second Copilot review round

- Elicitation check moved before command parsing so /path replies work
- IPC publish failure re-inserts pending elicitation for retry
- Approval callback uses short token in callback_data but stores full
  request_id in PendingApproval for correct IPC routing
- IPC error counter tracks per full pass, not per handle
- (Tests for new state machine deferred — needs IPC/KV mocking)

* fix: address third Copilot review round

- Approval/elicitation IPC publish failures now re-insert pending state
  and notify user to retry (no more silent loss)
- Approval callback_data uses FNV hash token for long request_ids to
  avoid prefix-collision risk; full request_id stored in PendingApproval
- IPC error counter tracks per full pass (all handles), not per handle
- Elicitation comment fixed to match skip behavior (not truncation)
- Added 4 tests for callback_token: short passthrough, long hashing,
  distinct ids, and 64-byte callback_data fit

* fix: clone request_id before use in elicitation handler

Bind request_id before building payload/topic to avoid potential
ownership confusion (the json! macro borrows, but cloning makes
intent explicit and prevents future refactoring surprises).

* fix: use callback_token for elicitation callback_data too

Elicitation enum options now use the same FNV hash tokenization as
approvals, maximizing space for option values within the 64-byte
callback_data limit. Validation matches against both full id and token.

* fix: address round 6 Copilot feedback

- Log full_request_id (not callback token) in approval TTL cleanup
- Log full_id in elicitation publish failure
- Bump turn last_activity on elicit_request to prevent timeout during input
- Clarify FNV hash comment: not crypto-resistant, sufficient for transient tokens

* fix: hash request_ids containing colons, update callback format docs

Colons in request_ids would break splitn(3, ':') parsing. callback_token
now always hashes ids containing ':'. Updated format comment to reflect
token-based callback_data structure.

* fix: detect and log callback token hash collisions on insert

* refactor: rename APPROVAL_TTL to PENDING_INTERACTION_TTL

Now governs both approvals and elicitations, name should reflect that.

* docs: fix install command and add reinstall/purge notes

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#7)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#13)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#9)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#11)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#15)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#15)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#3)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#14)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#17)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#12)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#9)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#5)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* feat: migrate to SDK 0.6.0 (wasmtime Component Model + WIT-driven types) (#10)

## Summary

Migrate to astrid-sdk 0.6.0 targeting wasm32-wasip2 (Component Model).

- Remove `extism-pdk` dependency
- Update `astrid-sdk` to 0.6.0 (from crates.io)
- Migrate IPC APIs: `recv_bytes`/`poll_bytes` → typed `PollResult`
- Migrate `hooks::trigger(&[u8])` → `hooks::trigger(&str)`
- Migrate `log::log("level", msg)` → typed `log::info(msg)` etc.
- Add `[[topic]]` declarations to Capsule.toml where applicable
- Build target: wasm32-wasip2

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#12)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#13)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#11)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#11)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#18)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#16)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#11)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#11)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#10)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#14)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
cargo-like-manifest in the rfcs repo, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs where
applicable.

Other capabilities (sandboxing, fs/net allowlists) preserved verbatim.
No src changes — manifest-only, behaviour unchanged.

## Dependencies

* RFC: `unicity-astrid/rfcs#26` (cargo-like manifest schema).
* Parser: `unicity-astrid/astrid#713` (cargo-like manifest schema parser
in core). The kernel reads both legacy and new forms equivalently during
the migration window.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#12)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
`unicity-astrid/rfcs#26`, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Canonicalise bare-name wit refs to the full `@unicity-astrid/wit/...`
form.
* Drop remaining TODOs onto newly-added canonical WIT refs.

Other capabilities preserved verbatim. No src changes — manifest-only,
behaviour unchanged.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema + restore on_before_prompt_build binding (#15)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
`unicity-astrid/rfcs#26`, parser support in
`unicity-astrid/astrid#713`), and restores the `on_before_prompt_build`
interceptor binding that was lost in a prior refactor.

* `chore: convert to Cargo-like [publish]/[subscribe] schema` — same
conversion as the rest of the capsule fleet.
* `fix(manifest): restore lost handler binding for
on_before_prompt_build` — the new schema does not auto-derive
interceptor bindings from a `subscribe` entry; this commit makes the
binding explicit so the handler keeps firing under the new schema.
* `chore: replace TODO wit refs with canonical @unicity-astrid/wit/...`
— canonical refs.

No src changes. The handler-binding restoration is critical: without it,
memory capsule silently stops participating in the prompt-build hook
chain after the manifest migration.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Manual: `on_before_prompt_build` fires for memory capsule under a
daemon running the cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#17)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
`unicity-astrid/rfcs#26`, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.
* Drop remaining TODOs onto newly-added canonical WIT refs.

Manifest-only, behaviour unchanged. Note: the existing PR #16
(`fix/payload-data-unwrap`) covers a separate src-side fix for the
`Custom { data }` unwrap pattern; this PR is purely manifest.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(manifest): convert to Cargo-like [publish]/[subscribe] schema with canonical wit refs (#13)

## Summary

Converts `Capsule.toml` to the new Cargo-like manifest schema (RFC:
`unicity-astrid/rfcs#26`, parser support in
`unicity-astrid/astrid#713`):

* Move legacy `[capabilities].ipc_publish` / `ipc_subscribe` arrays into
typed `[publish]` and `[subscribe]` tables.
* Replace `"TODO"` wit refs on resolvable topics with the canonical
`@unicity-astrid/wit/<noun>/<record>` path.

Manifest-only, behaviour unchanged. The src-side `Custom { data }`
unwrap fix is covered by the existing PR #11
(`fix/payload-data-unwrap`); this PR is purely manifest and
intentionally drops the duplicate src commit so review stays focused.

## Test plan

- [x] `cargo build --target wasm32-wasip1 --release`
- [x] Capsule loads cleanly under a daemon running the
cargo-like-manifest parser PR.

* chore(deps): bump astrid-sdk to 0.6.1 (#18)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#14)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#13)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#14)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#12)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#12)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#19)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#16)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#17)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#14)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#12)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#12)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#11)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#15)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* chore(deps): bump astrid-sdk to 0.6.1 (#13)

Bumps `astrid-sdk` dependency from 0.6.0 to 0.6.1.

Picks up:
- `ipc::publish_as` / `ipc::publish_json_as` (uplink principal
propagation, sdk-rust #37)
- Canonical-WIT-sourced contracts with per-interface `pub mod <iface>`
namespacing (sdk-rust #39)
- `cargo publish -p astrid-sys` fix (sdk-rust #36)

No source change required — `astrid-sdk` 0.6.1 is API-compatible at
every site this capsule touches.

* feat: initial scaffold of the users capsule

Implements astrid:[email protected] over IPC RPC. Capsule subscribes to
users.v1.<op>.request and publishes users.v1.<op>.response for the
eight operations: resolve, link, unlink, create, links, get, delete,
list. Each request carries a source envelope (channel, user-id,
correlation-id) so multi-tenant uplinks (sphere, discord, telegram)
can route responses back to the originating end-user by correlation.

KV key layout mirrors the legacy kernel astrid-storage::identity
store byte-for-byte (user/{uuid}, link/{platform}/{id},
name/{display_name}) so the future kernel-side cutover
(unicity-astrid/astrid#747) reads existing records unchanged.

Internal layout:
  - lib.rs      — capsule entrypoint + IPC dispatchers
  - types.rs    — domain records (AstridUser, FrontendLink, Source)
  - store.rs    — KV-backed store + Backend substitution seam
  - requests.rs — inbound payload structs (kebab + snake alias)
  - responses.rs — outbound JSON projection (kebab-case)
  - time.rs     — RFC 3339 formatting via Hinnant civil_from_days

36 unit tests cover key validation (path traversal, null bytes),
upsert semantics, cascade delete, name-index last-writer-wins,
request envelope deserialization, and timestamp formatting.

Closes unicity-astrid/astrid#747 (capsule side; the kernel-side
deletions ship in a follow-up once SDK wrappers cut over).

* self-review: clarify storage-shape divergence + tidy list_users projection

- Document that the on-disk JSON shape diverges from the legacy kernel
  store in three places (public_key as list<u8> vs base64; ms-precision
  timestamps vs chrono's us-precision; AstridUser drops the redundant
  principal field). Pre-launch with no records to migrate, the
  divergences are deliberate — value layout follows the WIT contract,
  not the kernel's Rust serialization. README + types.rs module
  docstring carry the explanation; the earlier 'byte-for-byte' claim
  was overstated.

- Replace the misleading filter_map on list_users with a direct map
  via a new user_value helper. user_to_json(Some(u)) always returns
  Some, so the filtering semantics never fired — latent bug if anyone
  later changes user_to_json to drop partially-invalid records.

* fix: drop [patch.crates-io] — use published astrid-sdk 0.6.1

The local-worktree patch was inherited from peer capsules and pinned
astrid-sdk/astrid-sdk-macros/astrid-sys to absolute paths that only
exist on the original author's machine. CI, contributors, and anyone
else cloning the repo hit:

    error: failed to load source for dependency `astrid-sdk`
    Caused by: Unable to update /…/sdk-rust/astrid-sdk

Removing the patch makes Cargo resolve astrid-sdk = "0.6.1" from
crates.io (already the base dep). Verified: cargo build --release
--target wasm32-wasip1 pulls 0.6.1 cleanly; 36/36 unit tests pass;
clippy and fmt are clean.

* feat: implement expanded astrid:[email protected] surface

Catches the capsule up to the merged WIT (unicity-astrid/wit#6):

  - source.uplink (was source.channel) — disambiguates from
    frontend-link.platform.
  - frontend-link.platform_instance for Slack workspaces, IRC
    networks, XMPP servers. KV key is now
    link/{platform}/{instance|_}/{platform_user_id}, with _ as the
    reserved sentinel for None.
  - frontend-link.display_name — platform-side global name at link time.
  - set_display_name + set_public_key topics for mutating AstridUser
    fields without rotating the UUID.
  - cursor/limit pagination on list_users and the two new context
    list topics.
  - Per-context display-name overlay (two-layer identity model):
    ContextIdentity record + five users.v1.context.* topics
    (set/clear/get/list_for_user/list_in_context). KV prefix is
    context/{platform}/{instance|_}/{context_id}/{platform_user_id}.
  - resolve becomes context-aware and returns a layered display name
    in one round-trip (context > link > canonical).
  - Cascade on unlink: drops every context overlay tied to the link.
  - Cascade on delete_user: drops every link and overlay for that user.

src/store.rs grew to 734 lines; store tests moved to src/store_tests.rs
to stay under CI's 1000-line cap. 42 unit tests on the host target
cover: identity CRUD with instance scoping, mutation, layered resolve
fallback chain, context overlay CRUD, two cascade paths, pagination
across list_users / list_context_for_user, sentinel-reserved
validation. Wasm release: 255 KB, fmt + clippy clean on both wasm
and host.

* test: platform scenarios — Discord/Slack/Telegram/Matrix/X/IRC/Mastodon/Email/SMS/Nostr/Passkey/GitHub

Real-shape end-to-end tests against the data models of every platform
from the audit. Each test uses platform-realistic identifiers and
exercises the link → resolve → list-links path plus the
platform's distinguishing characteristic:

  - Discord: 18-digit snowflakes + per-guild nickname layering
  - Slack: workspace-scoped IDs (T/U pairs); same U-id in different
    workspaces resolves to different humans; per-channel context
  - Telegram: int64 user-ids; @username refresh via re-link
  - Matrix: '@alice:server.org' federated IDs with no instance;
    per-room display-name overlays
  - X: numeric stable id, handle change via re-link
  - IRC: per-network scoping ('alice' on libera vs oftc)
  - Mastodon: '@[email protected]' federated; no instance
  - Email: globally-unique address
  - SMS: E.164 phone numbers
  - Nostr: npub-as-identity with public_key on AstridUser
  - Passkey: credential-id link with public_key on AstridUser
  - GitHub: numeric id stable, login change via re-link

Plus three group-setting cases:

  - One human linked across six platforms; resolve from any returns
    the same AstridUserId.
  - Five-member Discord guild member roster via context.list_in_context,
    paginated, every row resolves to a user.
  - Bot-vs-human attribution via method='bot' audit string.

20 new tests, total now 62. Catches contract-level data-model
mismatches before any uplink consumes the WIT — closes the largest
gap in the earlier 'will this work 100%' caveats list.

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#6)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#15)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#14)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#15)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#13)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#13)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#20)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#17)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#4)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#18)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#15)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#19)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unknown-unknown"` with
`--cfg=getrandom_backend="custom"` rustflag so `getrandom 0.4` picks up
the SDK's `__getrandom_v03_custom` extern (routes entropy through
`astrid:sys/host.random-bytes`).
- `rust-toolchain.toml` — `targets = ["wasm32-unknown-unknown"]`.
- `[patch.crates-io]` — points `astrid-sdk*` / `astrid-sys` /
`astrid-types` at the in-tree workspaces so the polyrepo cross-cuts
resolve.
- Call-site updates for the new typed-`error-code` SDK surface where
touched.

## Pairs with

- `unicity-astrid/astrid#752` — kernel-side migration
- `unicity-astrid/sdk-rust#44` — SDK migration
- All other `unicity-astrid/capsule-*` PRs on `feat/per-domain-wit`

## Test Plan

- [x] `cargo build --target wasm32-unknown-unknown --release` clean
- [x] `cargo clippy --release -- -D warnings` clean
- [x] Loads + runs under the migrated kernel — verified end-to-end via
`astrid run "say hi"`

* feat!: migrate to per-domain WIT + wasm32-unknown-unknown (#14)

## Summary

Migrate this capsule to:

1. The per-domain WIT host ABI introduced in `unicity-astrid/astrid#752`
(paired with `unicity-astrid/sdk-rust#44`).
2. `wasm32-unknown-unknown` as the canonical build target — zero
`wasi:*` imports, every host call routed through audited `astrid:*`
interfaces.

## Changes

- `.cargo/config.toml` — `target = "wasm32-unkn…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(kernel)!: migrate to per-domain WIT host ABI

1 participant