Skip to content

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 25 May 18:57
· 76 commits to main since this release
v0.7.0
403c4a7

Breaking

  • KernelRequest / KernelResponse / CommandInfo / CapsuleMetadataEntry / DaemonStatus / SYSTEM_SESSION_UUID moved from astrid_types::kernel to astrid_core::kernel_api. Re-exports under astrid_events::kernel_api are preserved for migration ergonomics. The reason: 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, and the rest of the kernel-only type universe). The kernel-management RPC surface (CLI ↔ daemon) doesn't belong in a WASM-compatible crate to begin with. CLI commands, socket_client, admin_client, the TUI, and the integration tests have all been updated to import from astrid_core::kernel_api.

Added

  • astrid:fs/host.fs-mkdir-all — unstubbed. Idempotent recursive directory creation via the existing VFS mkdir call (every VFS impl already routes through std::fs::create_dir_all under the hood). Capability gating, audit envelope (astrid:fs/host.fs-mkdir-all), and error mapping match fs-mkdir. Unblocks capsule code that wants the std::fs::create_dir_all-style idempotent variant instead of the strict fs-mkdir. (Closes one item of #753.)
  • Outbound TCP for capsules — net.connect-tcp host fn + net_connect capability. Capsules can now open persistent TCP connections via the new astrid:capsule/net.net-connect-tcp(host, port) -> stream-handle host fn, gated by a per-capsule net_connect = ["host:port", "host:*"] allowlist in Capsule.toml. The returned handle flows through the existing net-read / net-write / net-close-stream plumbing, and the kernel reuses the same is_safe_ip airlock that gates http-request to reject loopback / private / link-local / multicast IPs after DNS resolution. Connect timeout bounded to 10s; per-capsule active-stream cap (MAX_ACTIVE_STREAMS = 8) shared with inbound net-accept. Unblocks WebSocket clients, MQTT, Discord/Telegram gateways, postgres/redis, and the immediate motivator: a Unicity-network capsule wrapping Sphere SDK (Fulcrum + Nostr WebSocket transports). Tracking issue: #745. RFC: rfcs#27. WIT contract: wit#5.
  • NetStream enum (Unix + Tcp) in engine::wasm::host_state — replaces the bare Arc<Mutex<UnixStream>> value type in active_streams. The net_read / net_write dispatchers match on the variant; the inner framing (read_frame / write_frame generic helpers) is shared via tokio::io::AsyncRead + AsyncWrite trait bounds. Single-variant capsules see no behavior change.
  • CapsuleSecurityGate::check_net_connect(capsule_id, host, port) — new trait method, default-deny. ManifestSecurityGate implements it by matching the requested host:port against the manifest's net_connect allowlist (case-insensitive host, exact-or-* port).
  • astrid-build is target-agnostic now. Drops the hardcoded --target wasm32-wasip2 flag on cargo build; instead lets the capsule's own .cargo/config.toml select the target. After compilation, probes target/wasm32-unknown-unknown/release/ first, target/wasm32-wasip2/release/ second (and the workspace root) for the produced .wasm. When the produced artifact is a core wasm module (no Component Model layer = 1 in the magic bytes) — which is what wasm32-unknown-unknown produces — wit_component::ComponentEncoder::default().validate(true).module(&core).encode() wraps it into a Component Model component. Required because the Astrid-canonical guest target is wasm32-unknown-unknown (zero wasi:* imports), and cargo does not produce a component directly for that target.
  • astrid-build's ensure_component overwrites the original .wasm artifact instead of writing a sibling .component.wasm. Capsule Capsule.toml [[component]] file = "<crate>.wasm" directives keep resolving without per-target conditional logic — the toolchain hides which target produced the artifact.
  • astrid-types clock feature. Default OFF — gates IpcMessage::new() and the #[serde(default = "Utc::now")] timestamp default behind a Cargo feature. Kernel-side consumers (astrid-events, the daemon) enable it via their dep declaration. Capsule-side consumers (via astrid-sdk on wasm32-unknown-unknown) leave it off; absent timestamps fall back to the Unix epoch via a from_timestamp(0,0) default. Capsule code reads timestamps from kernel-published messages, never constructs them.
  • chrono = { default-features = false, features = ["serde"] } workspace-wide. The clock feature is what pulls wasm-bindgen + js-sys on wasm32-unknown-unknown, which then inject __wbindgen_placeholder__ imports that wit-component's encoder refuses to round-trip. Records that need a clock value receive it from astrid_sdk::time (audited host fn); DateTime<Utc> is only used as a serializable field shape, never constructed via Utc::now() in capsule code.
  • uuid = { default-features = false, features = ["v4", "v5", "serde", "rng-getrandom"] } workspace-wide. The default features pull a js-based RNG on wasm32-unknown-unknown (via wasm-bindgen). rng-getrandom routes v4 generation through getrandom, which astrid-sys configures with a custom backend (astrid:sys.random-bytes) on capsule builds. Same dep wiring in sdk-rust's astrid-sdk.

Changed

  • Kernel exposes zero wasi:*. configure_kernel_linker no longer registers wasmtime_wasi::p2::add_to_linker_sync. The Astrid-canonical guest target (wasm32-unknown-unknown) produces wasm with zero wasi:* imports; a capsule that somehow ships with a wasi:* import fails to instantiate at load time with a clear "interface not found" error — the intended posture, not a bug. Capsules that historically targeted wasm32-wasip2 and relied on auto-injected wasi:* imports will fail to load until they migrate to wasm32-unknown-unknown via the upcoming SDK + capsule sweep (a separate PR cluster, blocked on this one landing first).
  • crates/astrid-capsule/src/manifest.rs split into a manifest/ submodule. The 1000-line single file became manifest/mod.rs + manifest/capabilities.rs + manifest/topics.rs. CapabilitiesDef, PublishDef, SubscribeDef (with their custom deserializers and TOML parsing tests) live in dedicated submodules; the top-level manifest::* public API is preserved via pub use re-exports — no consumer-side change required.
  • astrid-storage::kv split into a submodule directory. kv.rs (now ~1200 lines after the CAS addition) became kv/mod.rs (validators, helpers, trait, re-exports) + kv/memory.rs (MemoryKvStore) + kv/surreal.rs (SurrealKvStore, behind the kv feature) + kv/scoped.rs (ScopedKvStore), each well under the 1000-line CI ceiling. Public API preserved verbatim via pub use.
  • wit/astrid-capsule.wit resynced from canonical unicity-astrid/wit to pick up the new net-connect-tcp fn and the ipc-message.principal field (canonical PR #4 from May; was missing from the in-tree copy). Internal IpcMessage → WitIpcMessage conversion now forwards principal.

Fixed

  • TcpStream::read_bytes / peek return Err(ErrorCode::Closed) on cancellation. Previously both methods collapsed cancel to Ok(Vec::new()), which is indistinguishable from a clean EOF in byte-stream reads. Capsules with EOF finalizers (write trailers, send last-message IPC, flush logs) would execute those finalizers under a forced unload. Now matches read_frame / write_bytes / shutdown — Closed surfaces cancellation as its own signal. Empty Vec retains its "clean EOF" meaning.
  • Per-domain WIT review fixups (PR #752). A multi-agent review surfaced fixes addressed in-branch before merge:
    • ipc::recv mixed-principal batches are now truncated at the first publisher boundary so tail messages can't be silently mis-stamped with the head's principal context; new truncate_to_homogeneous_principal unit tests cover the boundary cases.
    • TcpStream::write surfaces peer-disconnect IO kinds as ErrorCode::ConnectionReset instead of swallowing them as Ok(()); pure-function tests pin the BrokenPipe / ConnectionReset / ConnectionAborted / UnexpectedEof → ConnectionReset mapping and use tokio::io::duplex to reproduce the live close.
    • TcpStream::read cancellation now returns NetReadStatus::Closed (not Pending) so a cancelled run-loop terminates instead of busy-looping.
    • spawn_background registers the spawned PID in the ProcessTracker, and the ProcessHandle drop path unregisters; previously a tool.v1.request.cancel could never reach backgrounded children.
    • Subscription recv keeps the resource handle valid across calls — the EventReceiver lives behind an Arc<Mutex<...>> so the wasmtime resource is no longer deleted-and-re-pushed each blocking wait.
    • read_file re-checks the post-read payload size for TooLarge rather than relying on a pre-stat TOCTOU.
    • ProcessHandle::wait uses tokio::task::spawn_blocking(child.wait) racing a tokio::time::timeout instead of the 50ms try_wait busy-poll.
    • unix_listener::accept retries credential-rejected connections with a 100ms back-off to avoid a CPU-pinned spin against a hostile peer.
    • http-stream per-chunk timeout extracted to HTTP_STREAM_READ_TIMEOUT named constant.
    • Build script now invalidates the WIT staging dir on .gitmodules changes so CI runners that lazily git submodule update don't compile against a stale tree.
  • Per-domain WIT review fixups round 2 (Gemini, PR #752).
    • MAX_ACTIVE_STREAMS / MAX_SUBSCRIPTIONS / MAX_BACKGROUND_PROCESSES quota gates now read O(1) counter fields on HostState (net_stream_count, subscription_count, process_count_total, process_count_by_principal) instead of walking the entire ResourceTable. Each successful resource insert bumps the counter; the matching drop impl decrements. Per-principal sub-budgets for spawn-background use a HashMap<PrincipalId, usize> keyed on the creator. Single-threaded: wasmtime stores are owned by exactly one OS thread.
    • CI workflows (ci.yml) now check out the wit/ submodule recursively so astrid-capsule's build script can stage the per-domain WIT packages. Previously every job that touched the build (check, clippy, test, msrv) panicked with read wit/host: No such file or directory.
  • Atomic kv_cas across capsules. The host fn used to emulate compare-and-swap with a get followed by a set, which raced across capsules running concurrently in the same kernel. KvStore now exposes an atomic compare_and_swap(namespace, key, expected, new) primitive; MemoryKvStore serializes read+conditional-write under its existing RwLock, and SurrealKvStore runs the comparison inside a single MVCC transaction guarded by an internal mutex that closes the TOCTOU between surrealkv's validate_write_conflicts and its actual write phase. Two concurrent-spawn regression tests (memory + surreal) assert exactly one of 16–32 racing tasks succeeds the swap. The capsule kv_cas host fn delegates straight to the new primitive — no more emulation.

Install

From source (requires Rust 1.94+):

cargo install astrid

Pre-built binaries:
Download the archive for your platform, extract, and add to PATH:

tar xzf astrid-*-$(uname -m)-*.tar.gz
sudo mv astrid-*/astrid astrid-*/astrid-daemon astrid-*/astrid-build /usr/local/bin/

Then run astrid init to set up capsules.


With many thanks from the following Astrinauts 🚀

  • Joshua J. Bouw