feat(sdk,net): full std::net::TcpStream surface#42
Merged
Conversation
Adds astrid_sdk::net::connect(host, port) — the outbound-TCP analogue of the existing accept — and a std::net::TcpStream-shaped facade with std::io::Read + Write impls. RAII close-on-drop. Generic code over Read + Write (tungstenite for WebSocket, rustls, postgres drivers) works unmodified. Submodule bump pulls astrid-runtime/wit#5 (the host-side net-connect-tcp fn). Capability gate (per-capsule `net_connect` allowlist in Capsule.toml) and SSRF airlock are kernel-side and land in a separate core PR. The SDK wrapper passes host/port straight through. RFC: astrid-runtime/rfcs#27 Tracking issue: astrid-runtime/astrid#745
Extends astrid_sdk::net to mirror std::net::TcpStream completely. Wraps the 14 new host fns added in astrid-runtime/wit#5 amendment: Free fns: - read_bytes / write_bytes (byte-stream, no framing) - peek - shutdown(Shutdown::{Read,Write,Both}) — std::net::Shutdown analogue - peer_addr / local_addr - set_nodelay / nodelay - set_read_timeout / read_timeout (Duration) - set_write_timeout / write_timeout (Duration) - set_ttl / ttl TcpStream methods: - All of the above, returning std::io::Error - std::io::Read uses read_bytes (no length-prefix surplus buffering) - std::io::Write uses write_bytes (no implicit length prefix) - Drop calls close() Generic Read+Write consumers (tungstenite for WebSocket, rustls, postgres drivers) work unmodified now that the SDK's Read/Write operate on byte-stream semantics. RFC: astrid-runtime/rfcs#27 WIT: astrid-runtime/wit#5 Tracking issue: astrid-runtime/astrid#745
This was referenced May 19, 2026
There was a problem hiding this comment.
Code Review
This pull request introduces outbound TCP support to the astrid-sdk, adding a low-level connect function and a high-level TcpStream facade that implements std::io::Read and std::io::Write. The WIT interface has been expanded to include new functions for byte-stream operations, peeking, and socket options like TTL and Nagle's algorithm. Feedback identifies a critical issue where the Read and peek implementations incorrectly return Ok(0) when no data is ready, which is ambiguous with EOF. Other recommendations include refactoring timeout validation into a shared helper function and improving IPv6 address parsing to handle square brackets.
joshuajbouw
added a commit
to astrid-runtime/astrid
that referenced
this pull request
May 19, 2026
Addresses Gemini code-review feedback on astrid-runtime/sdk-rust#42. Before: - timeout=None falls through to a 50ms internal timeout. - Timeout expiry with no data returned Ok(empty Vec). - Caller can't distinguish 'transient no data' from 'peer disconnected'. After: - timeout=None drops the tokio::time::timeout wrapper entirely. Reads block until data, EOF, or capsule unload (via the outer bounded_block_on_cancellable cancellation token). - Empty Vec is now ALWAYS EOF — matches std::io::Read's Ok(0) contract. - 'read would block' error only surfaces when the caller has explicitly set a read timeout that expired. SDK-side, that maps to std::io::ErrorKind::WouldBlock so std::Read callers can distinguish. The peek and write_bytes paths already had the right shape; only net_read_bytes carried the bug.
- to_host_timeout helper rejects Some(Duration::ZERO) to match
std::net::TcpStream::set_{read,write}_timeout. set_read_timeout
and set_write_timeout now go through this helper.
- parse_host_port strips IPv6 brackets — `[::1]:443` correctly
yields ("::1", 443). Handles bracket-less ambiguous v6 (multiple
colons, no port) as a clean error.
- io_error_from_net_op maps the host's 'read would block' /
'write would block' / 'peek would block' sentinel to
std::io::ErrorKind::WouldBlock so std::Read / Write / peek callers
can distinguish timeout-fired from EOF. Combined with the kernel
fix that empty Vec now unambiguously means EOF, std::Read::read
returning Ok(0) is no longer a false EOF.
- 9 new unit tests cover parse_host_port (v4, v6, malformed),
to_host_timeout (None / Zero / passthrough), and
io_error_from_net_op (WouldBlock mapping).
astrid-runtime/wit#5 (full std::net::TcpStream surface) merged to main as commit 9244c74 (squash). Submodule was pinned at the pre-squash PR head 215ba1c — content-identical but referencing a non-canonical SHA. Bump to main keeps the reference auditable. astrid-sys/wit/astrid-capsule.wit is unchanged — the squash didn't modify the file content, and sync-host-wit.sh produced an empty diff.
joshuajbouw
added a commit
to astrid-runtime/astrid
that referenced
this pull request
May 19, 2026
) ## Linked Issue Closes #745 ## Summary Adds outbound TCP to the host ABI: `net-connect-tcp` plus 14 more host fns that give capsules full `std::net::TcpStream` parity. Unblocks every capsule that needs persistent TCP — WebSocket clients (Fulcrum, Nostr), MQTT, Discord/Telegram gateways, postgres / redis. Strictly additive to the existing `net` interface; **TLS is not in scope** (capsules ship their own crate, matching the `std::net` model where TLS lives in user-space). ## Changes - **WIT**: syncs canonical [astrid-runtime/wit#5](astrid-runtime/wit#5) into in-tree `wit/astrid-capsule.wit`. Adds the `shutdown-how` enum + 15 new `net-*` fns (`connect-tcp`, `read-bytes`, `write-bytes`, `peek`, `shutdown`, `peer-addr`, `local-addr`, `set-nodelay` / `nodelay`, `set-read-timeout` / `read-timeout`, `set-write-timeout` / `write-timeout`, `set-ttl` / `ttl`) and brings in `ipc-message.principal` (canonical PR #4) which had drifted from the in-tree copy. - **Manifest**: `CapabilitiesDef.net_connect: Vec<String>` — per-capsule allowlist for outbound TCP destinations. Each entry is `"host:port"` (exact) or `"host:*"` (any port). Empty / missing denies all outbound TCP (fail-closed). Independent of `net_bind`, `http`, `host_process`. - **Kernel** (`engine/wasm/host/net/`): `net_connect_tcp` runs capability check → host string validation → DNS resolve → SSRF airlock (`is_safe_ip`, same gate as `http-request`) → bounded 10s connect. `read_bytes` / `write_bytes` are byte-stream variants honouring per-stream timeouts (empty Vec = EOF unambiguously; timeout fires as `would block` for SDK-side `ErrorKind::WouldBlock` mapping). `peek` uses tokio's native `TcpStream::peek`. `shutdown` uses `socket2::SockRef` for full `Read|Write|Both` direction support. - **Security caps**: per-call read/peek buffer capped at `MAX_BYTES_PER_CALL = 10 MB` (guards against `u32::MAX` OOM). Host string validation (non-empty, ≤255 bytes per RFC 1035, no null bytes) at the host-fn boundary. - **Security gate**: new `CapsuleSecurityGate::check_net_connect` trait method, default-deny. `ManifestSecurityGate` impl matches `host:port` against the manifest allowlist (case-insensitive host, exact-or-`*` port). - **Refactors**: `net.rs` split into `net/{mod,handshake,stream}.rs` and `manifest.rs` split into `manifest/{mod,capabilities,topics}.rs` to stay under the 1000-line CI cap. `pub use` re-exports preserve the public API. ## Test Plan ### Automated - [x] `cargo test --workspace` passes (296 lib tests in astrid-capsule — was 285, + 11 new covering net_connect pattern matching, socket2 shutdown round-trip, validate_host gates) - [x] No new clippy warnings (`cargo clippy --workspace -- -D warnings` clean) - [x] `cargo +nightly fmt --all --check` clean ### Manual - [ ] Tag a SDK release (separate PR per CLAUDE.md) so capsule authors can bump - [ ] Smoke-test a capsule against a public `ws://` endpoint over the new surface (deferred to a follow-up capsule PR) ## Refs - RFC: [astrid-runtime/rfcs#27](astrid-runtime/rfcs#27) - WIT: [astrid-runtime/wit#5](astrid-runtime/wit#5) - SDK (rust): [astrid-runtime/sdk-rust#42](astrid-runtime/sdk-rust#42) - SDK (js): [astrid-runtime/sdk-js#3](astrid-runtime/sdk-js#3) ## Checklist - [x] Linked to an issue - [x] CHANGELOG.md updated under `[Unreleased]`
joshuajbouw
added a commit
to astrid-runtime/sdk-js
that referenced
this pull request
May 19, 2026
## Summary JS/TS mirror of [astrid-runtime/sdk-rust#42](astrid-runtime/sdk-rust#42). Adds outbound TCP for capsules — `connect()` plus the full set of methods a Rust binary's `std::net::TcpStream` exposes — on the `StreamHandle` class. ## Changes ### Ambient module (`wit-imports.d.ts`) Declares the 14 new host fns + `ShutdownHow` type in the `astrid:capsule/[email protected]` module, matching the canonical WIT in [astrid-runtime/wit#5](astrid-runtime/wit#5). ### `net.ts` - `connect(host, port)` — outbound TCP (already in this branch's tip, now upgraded from a stub). - New `StreamHandle` methods: - `readBytes(maxBytes)` / `writeBytes(data)` — byte-stream read/write (no length-prefix framing). - `peek(maxBytes)` — non-destructive read. - `shutdown(how)` — half-close (`'read' | 'write' | 'both'`). - `peerAddr()` / `localAddr()` — `ip:port` strings. - `setNodelay(b)` / `nodelay()` — TCP_NODELAY. - `setReadTimeout(ms)` / `readTimeout()`. - `setWriteTimeout(ms)` / `writeTimeout()`. - `setTtl(n)` / `ttl()` — IP TTL. ### Submodule `contracts/` bumped to `feat/net-connect-tcp` tip (matches WIT PR head). ### Unchanged The existing framed surface (`recv` / `tryRecv` / `send` / `close` + `AsyncIterable<Uint8Array>`) keeps its semantics — that's the right shape for the inbound CLI proxy. Outbound TCP capsules use the byte-stream pair (`readBytes` / `writeBytes`). ## TLS Out of scope. JS-side capsules ship their own WebSocket / TLS library over `readBytes` / `writeBytes`, same as native Rust binaries do — std::net::TcpStream doesn't ship TLS, and we're holding that line. ## Verification - `npm run -w packages/astrid-sdk build` clean (tsc -b) - 64 contract types regenerate cleanly via the prebuild script ## Refs - RFC: [astrid-runtime/rfcs#27](astrid-runtime/rfcs#27) - WIT: [astrid-runtime/wit#5](astrid-runtime/wit#5) - SDK (rust): [astrid-runtime/sdk-rust#42](astrid-runtime/sdk-rust#42) - Core: [astrid-runtime/astrid#746](astrid-runtime/astrid#746) - Tracking issue: [astrid-runtime/astrid#745](astrid-runtime/astrid#745)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Outbound TCP for capsules. Mirrors
std::net::TcpStreamend-to-end — every method a Rust binary expects on a connected TCP stream is available on the SDK side. WebSocket libraries, TLS wrappers, postgres drivers — anything generic overRead + Write— drops in unmodified.Added
Free functions (low-level, one per WIT host fn)
connect(host, port) -> StreamHandle— opens an outbound TCP connection. Capability-gated againstCapsule.tomlnet_connectallowlist; SSRF airlock on resolved IP; 10s connect timeout.read_bytes(stream, max) -> Vec<u8>/write_bytes(stream, data) -> u32— byte-stream read/write (no length-prefix framing).peek(stream, max) -> Vec<u8>— non-destructive read.shutdown(stream, Shutdown::{Read,Write,Both})— half-close. Backed bysocket2::SockReffor full direction support.peer_addr/local_addr—ip:portstrings.set_nodelay/nodelay— TCP_NODELAY.set_read_timeout/read_timeout—Option<Duration>.set_write_timeout/write_timeout.set_ttl/ttl— IP TTL.TcpStreamfacadestd::net::TcpStream-shaped wrapper aroundStreamHandlewith std::io::Read + Write impls, RAII close-on-drop, and matching inherent methods (set_nodelay,peer_addr,shutdown(Shutdown::*),peek, timeouts,set_ttl).ShutdownenumLocal mirror of
std::net::Shutdown(Read / Write / Both), wired to the WITshutdown-howenum.Submodule
contracts/bumped tofeat/net-connect-tcptip on canonical unicity-astrid/wit#5.TLS
Out of scope — std::net::TcpStream doesn't ship TLS either. Capsules that need TLS bring their own crate (
rustls, etc.) over this byte-stream surface, matching how native Rust binaries do it.Refs
Verification
cargo build/test/clippy -p astrid-sdkcleanTcpStreamcompiles