RFC: net.connect-tcp outbound TCP host function#27
Open
joshuajbouw wants to merge 2 commits into
Open
Conversation
Adds one host fn — `net-connect-tcp(host, port) -> stream-handle` — to the existing `astrid:capsule/net` interface, capability-gated against a new `net_connect` allowlist in `Capsule.toml`. Reuses the existing stream-handle plumbing (`net-read` / `net-write` / `net-close-stream`) and the existing SSRF airlock from `http-request`. Unblocks WebSocket, MQTT, Discord/Telegram bridges, postgres/redis, and the immediate motivator: a Unicity-network capsule wrapping Sphere SDK that needs persistent Fulcrum + Nostr WebSocket connections. Closes #745 (tracking issue).
joshuajbouw
added a commit
to astrid-runtime/wit
that referenced
this pull request
May 19, 2026
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/[email protected]): - 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
Summary, Reference-level WIT block, and Drawbacks updated to reflect the broader scope decision — capsules now get every method std::net::TcpStream exposes (byte-stream read/write, peek, shutdown, peer/local-addr, nodelay, read/write timeouts, ttl), not just connect. The old 'framing is wrong for raw TCP' drawback is resolved by the new net-read-bytes / net-write-bytes pair. Replaced with a 'dual framing in one interface' drawback acknowledging that the legacy framed pair and the byte-stream pair share a stream-handle type — a deliberate choice, not a bug.
This was referenced May 19, 2026
joshuajbouw
added a commit
to astrid-runtime/wit
that referenced
this pull request
May 19, 2026
## Summary Adds one host function — `net-connect-tcp(host, port) -> stream-handle` — to the existing `astrid:capsule/net` interface. Returns the same handle type as `net-accept`, so the existing `net-read` / `net-write` / `net-close-stream` already operate on it without further WIT changes. ## Motivation The host ABI today has `http-request` / `http-stream-start` for one-shot fetch and `net-accept` for 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` / npm `ws`); the host gives capsules what `std::net::TcpStream` gives a Rust binary. ## Design - **Wire shape**: `net-connect-tcp(host: string, port: u16) -> result<u64, string>`. Returns a `stream-handle` (u64), same type as `net-accept`. Existing read/write/close already accept this handle. - **No new types, no new interface.** Strictly additive to `astrid:capsule/net`. Capsules that don't call this fn are byte-identical at the wit-bindgen layer. - **Security gates** (all kernel-side, documented in WIT doc comment): - Per-capsule `net_connect = ["host:port"]` allowlist in `Capsule.toml` (kernel-side, separate PR). Fail-closed when missing or empty. - SSRF airlock on resolved IP — reuses the `is_safe_ip` already gating `http-request` / `http-stream-start`. - Per-capsule active-stream cap (`MAX_ACTIVE_STREAMS = 8`) shared with `net-accept`. - Connect timeout (10s default) to avoid indefinite host-fn holds. ## RFC Full design rationale, alternatives, and SDK shape in [astrid-runtime/rfcs#27](astrid-runtime/rfcs#27). Tracking issue: [astrid-runtime/astrid#745](astrid-runtime/astrid#745). ## After merge - Bump submodule in `sdk-rust` + regenerate bindings + add `astrid_sdk::net::connect` + `TcpStream` facade - Bump submodule in `sdk-js` + add `connect()` + `Stream` type - Add kernel impl in `astrid-capsule/src/engine/wasm/host/net.rs` + manifest parser `net_connect` field
joshuajbouw
added a commit
to astrid-runtime/sdk-js
that referenced
this pull request
May 19, 2026
Adds `connect(host, port)` to the JS SDK's net module with the exact shape RFC astrid-runtime/rfcs#27 specifies — returns a StreamHandle that flows through the existing recv / tryRecv / send / close surface, same type as the accept path returns. The host fn (`astrid:capsule/net.net-connect-tcp`) hasn't landed yet (tracking astrid-runtime/astrid#745). The body throws SysError.api with the tracking URLs so capsules attempting to use it before the host side ships get a clear, actionable error. Why land the stub now: capsules being built against the final unicity-astrid/wit contract (Unicity / Sphere SDK port, Discord bridges, MQTT, etc.) can import `connect` and write against the final API today. When astrid#745 merges, this commit becomes a one-line swap: replace the `throw SysError.api(...)` with the commented-in `callHost(...)` line above it. No callers change.
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/[email protected] 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/[email protected]): - 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/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/[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.
Adds one host fn —
net-connect-tcp(host, port) -> stream-handle— to the existingastrid:capsule/netinterface, capability-gated against a newnet_connectallowlist inCapsule.toml. Reuses the existing stream-handle plumbing (net-read/net-write/net-close-stream) and the existing SSRF airlock fromhttp-request.Unblocks WebSocket, MQTT, Discord/Telegram bridges, postgres/redis, and the immediate motivator: a Unicity-network capsule wrapping Sphere SDK that needs persistent Fulcrum + Nostr WebSocket connections.
Closes #745 (tracking issue).