feat(net): add net-connect-tcp for outbound TCP#5
Merged
Conversation
Adds one host fn — net-connect-tcp(host, port) -> stream-handle — to the astrid:capsule/net interface. Returns the same handle type as net-accept, so net-read / net-write / net-close-stream already work with it. Capability gate: per-capsule net_connect = ["host:port"] allowlist in Capsule.toml (defined kernel-side, this RFC describes the wire shape only). SSRF airlock runs on the resolved IP, matching the gate on http-request. Connect timeout bounded to 10s default. Unblocks WebSocket, MQTT, Discord/Telegram gateways, postgres/redis, and the immediate forcing function: a Unicity-network capsule wrapping Sphere SDK (Fulcrum + Nostr persistent WebSocket). RFC: astrid-runtime/rfcs#27 Tracking issue: astrid-runtime/astrid#745
Adds 14 functions + shutdown-how enum to astrid:capsule/net for parity with std::net::TcpStream: - net-read-bytes / net-write-bytes: byte-stream read/write (no length-prefix framing — distinct from the legacy framed net-read / net-write used by the inbound Unix-accept proxy) - net-peek: peek without consuming - net-shutdown(read|write|both): half-close - net-peer-addr / net-local-addr: address introspection - net-set-nodelay / net-nodelay: TCP_NODELAY - net-set-read-timeout / net-read-timeout - net-set-write-timeout / net-write-timeout - net-set-ttl / net-ttl: IP TTL Existing net-read / net-write keep their framed semantics (the CLI proxy uplink remains structurally framed). Both pairs operate on the same stream-handle type — capsule code picks the matching pair for its protocol. Unblocks generic Read+Write consumers — tungstenite, rustls, postgres drivers — to drop in unmodified. RFC: astrid-runtime/rfcs#27
joshuajbouw
added a commit
to astrid-runtime/sdk-rust
that referenced
this pull request
May 19, 2026
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
joshuajbouw
added a commit
to astrid-runtime/astrid
that referenced
this pull request
May 19, 2026
Implements 14 new host fns to give capsules complete std::net::TcpStream
parity. All are kernel-side; the SDK wraps them in unicity-astrid/sdk-rust.
New fns on net::Host:
- net_read_bytes / net_write_bytes: byte-stream read/write (no
length-prefix framing). Read returns empty Vec on EOF; timeout
surfaces as 'read would block' / 'write would block' to match
std::io::ErrorKind::WouldBlock.
- net_peek: peek without consuming
- net_shutdown(read|write|both): half-close (write supported; read/both
return clear errors pending socket2 integration)
- net_peer_addr / net_local_addr: address introspection
- net_set_nodelay / net_nodelay: TCP_NODELAY toggle
- net_set_read_timeout / net_read_timeout (stored per-stream)
- net_set_write_timeout / net_write_timeout (stored per-stream)
- net_set_ttl / net_ttl: IP TTL
Refactor:
- NetStream::Tcp now holds TcpStreamSlot { stream, read_timeout,
write_timeout } instead of a bare Arc<Mutex<TcpStream>>. Per-stream
timeout state lives next to the socket so the byte-stream
read/write fns can apply tokio::time::timeout per call without
global state.
- net.rs split into net/ submodule (handshake.rs + stream.rs +
mod.rs) to stay under the 1000-line CI cap.
Unix variants reject TCP-only ops (nodelay, ttl, addresses, timeouts)
with clear errors — those are TCP socket options that don't apply.
RFC: astrid-runtime/rfcs#27
WIT: astrid-runtime/wit#5
Tracking issue: closes #745
joshuajbouw
added a commit
to astrid-runtime/sdk-js
that referenced
this pull request
May 19, 2026
Mirrors the kernel + sdk-rust expansion in unicity-astrid/sdk-rust
feat/net-connect-tcp.
Ambient module additions (astrid:capsule/net@0.1.0):
- ShutdownHow type ('read' | 'write' | 'both')
- 14 new functions: netReadBytes, netWriteBytes, netPeek,
netShutdown, netPeerAddr, netLocalAddr, netSetNodelay, netNodelay,
netSetReadTimeout, netReadTimeout, netSetWriteTimeout,
netWriteTimeout, netSetTtl, netTtl
StreamHandle methods:
- readBytes(maxBytes), writeBytes(data), peek(maxBytes)
- shutdown(how)
- peerAddr(), localAddr()
- setNodelay(b), nodelay()
- setReadTimeout(ms), readTimeout()
- setWriteTimeout(ms), writeTimeout()
- setTtl(n), ttl()
The existing recv / send / close framed surface is unchanged — it's
the right shape for the inbound CLI proxy. Outbound TCP capsules use
the byte-stream pair.
RFC: astrid-runtime/rfcs#27
WIT: astrid-runtime/wit#5
SDK (rust): unicity-astrid/sdk-rust feat/net-connect-tcp
Tracking issue: astrid-runtime/astrid#745
This was referenced May 19, 2026
joshuajbouw
added a commit
to astrid-runtime/sdk-js
that referenced
this pull request
May 19, 2026
Replaces the stub from 5a12d7d with the real impl now that the host side has landed (astrid-runtime/wit#5 + astrid#745 feat/net-connect-tcp). Changes: - packages/astrid-sdk/src/wit-imports.d.ts: declare netConnectTcp in the astrid:capsule/net@0.1.0 ambient module. - packages/astrid-sdk/src/net.ts: connect() now calls hostConnectTcp through the existing callHost wrapper; throws SysError on capability denial, SSRF rejection, DNS failure, or connect timeout. JSDoc lists the three kernel-side checks operators care about (allowlist, SSRF airlock, active-stream cap) so capsule authors don't have to read the host source to understand failure modes. - contracts/: submodule bumped to feat/net-connect-tcp tip (matches the WIT PR head). - packages/astrid-sdk/wit-contracts/astrid-contracts.wit: regenerated by scripts/sync-contracts-wit.sh (no diff vs main beyond canonical updates). Returns the same StreamHandle type as accept() — recv / tryRecv / send / close all Just Work without runtime branching. RFC: astrid-runtime/rfcs#27 WIT: astrid-runtime/wit#5 SDK (rust): unicity-astrid/sdk-rust feat/net-connect-tcp Tracking issue: astrid-runtime/astrid#745
joshuajbouw
added a commit
to astrid-runtime/sdk-js
that referenced
this pull request
May 19, 2026
Mirrors the kernel + sdk-rust expansion in unicity-astrid/sdk-rust
feat/net-connect-tcp.
Ambient module additions (astrid:capsule/net@0.1.0):
- ShutdownHow type ('read' | 'write' | 'both')
- 14 new functions: netReadBytes, netWriteBytes, netPeek,
netShutdown, netPeerAddr, netLocalAddr, netSetNodelay, netNodelay,
netSetReadTimeout, netReadTimeout, netSetWriteTimeout,
netWriteTimeout, netSetTtl, netTtl
StreamHandle methods:
- readBytes(maxBytes), writeBytes(data), peek(maxBytes)
- shutdown(how)
- peerAddr(), localAddr()
- setNodelay(b), nodelay()
- setReadTimeout(ms), readTimeout()
- setWriteTimeout(ms), writeTimeout()
- setTtl(n), ttl()
The existing recv / send / close framed surface is unchanged — it's
the right shape for the inbound CLI proxy. Outbound TCP capsules use
the byte-stream pair.
RFC: astrid-runtime/rfcs#27
WIT: astrid-runtime/wit#5
SDK (rust): unicity-astrid/sdk-rust feat/net-connect-tcp
Tracking issue: astrid-runtime/astrid#745
joshuajbouw
added a commit
to astrid-runtime/sdk-rust
that referenced
this pull request
May 19, 2026
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-rust
that referenced
this pull request
May 19, 2026
## Summary
Outbound TCP for capsules. Mirrors `std::net::TcpStream` end-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 over `Read + 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 against `Capsule.toml` `net_connect`
allowlist; 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
by `socket2::SockRef` for full direction support.
- `peer_addr` / `local_addr` — `ip:port` strings.
- `set_nodelay` / `nodelay` — TCP_NODELAY.
- `set_read_timeout` / `read_timeout` — `Option<Duration>`.
- `set_write_timeout` / `write_timeout`.
- `set_ttl` / `ttl` — IP TTL.
### `TcpStream` facade
`std::net::TcpStream`-shaped wrapper around `StreamHandle` with
std::io::Read + Write impls, RAII close-on-drop, and matching inherent
methods (`set_nodelay`, `peer_addr`, `shutdown(Shutdown::*)`, `peek`,
timeouts, `set_ttl`).
```rust
use astrid_sdk::net::TcpStream;
use std::io::{Read, Write};
let mut sock = TcpStream::connect("fulcrum.unicity.network:443")?;
sock.set_nodelay(true)?;
sock.write_all(b"GET / HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n")?;
```
### `Shutdown` enum
Local mirror of `std::net::Shutdown` (Read / Write / Both), wired to the
WIT `shutdown-how` enum.
## Submodule
`contracts/` bumped to `feat/net-connect-tcp` tip on canonical
[astrid-runtime/wit#5](astrid-runtime/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
- RFC:
[astrid-runtime/rfcs#27](astrid-runtime/rfcs#27)
- WIT:
[astrid-runtime/wit#5](astrid-runtime/wit#5)
- Tracking issue:
[astrid-runtime/astrid#745](astrid-runtime/astrid#745)
## Verification
- `cargo build/test/clippy -p astrid-sdk` clean
- Doctest on `TcpStream` compiles
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/net@0.1.0` 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)
5 tasks
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
Adds one host function —
net-connect-tcp(host, port) -> stream-handle— to the existingastrid:capsule/netinterface. Returns the same handle type asnet-accept, so the existingnet-read/net-write/net-close-streamalready operate on it without further WIT changes.Motivation
The host ABI today has
http-request/http-stream-startfor one-shot fetch andnet-acceptfor inbound Unix sockets. There is no path for outbound persistent TCP — blocking WebSocket clients, MQTT, Discord/Telegram gateways, postgres/redis, and the immediate forcing function: a Unicity-network capsule wrapping Sphere SDK that needs Fulcrum + Nostr WebSocket transports.This adds the missing primitive. WebSocket and every higher protocol stay in user-space (
tungstenite/ npmws); the host gives capsules whatstd::net::TcpStreamgives a Rust binary.Design
net-connect-tcp(host: string, port: u16) -> result<u64, string>. Returns astream-handle(u64), same type asnet-accept. Existing read/write/close already accept this handle.astrid:capsule/net. Capsules that don't call this fn are byte-identical at the wit-bindgen layer.net_connect = ["host:port"]allowlist inCapsule.toml(kernel-side, separate PR). Fail-closed when missing or empty.is_safe_ipalready gatinghttp-request/http-stream-start.MAX_ACTIVE_STREAMS = 8) shared withnet-accept.RFC
Full design rationale, alternatives, and SDK shape in unicity-astrid/rfcs#27.
Tracking issue: unicity-astrid/astrid#745.
After merge
sdk-rust+ regenerate bindings + addastrid_sdk::net::connect+TcpStreamfacadesdk-js+ addconnect()+Streamtypeastrid-capsule/src/engine/wasm/host/net.rs+ manifest parsernet_connectfield