# Transports Some modules need more than XRPC: a shell on a VM over iroh, or a WebRTC datachannel from a page. A transport serves `com.fedfork.exec..connect` calls for a host. Hosts take transports as plugins; the core never imports one, and a host without it refuses its calls. | transport | NSID, URLs | hosts | repo | |---|---|---|---| | iroh | `com.fedfork.exec.iroh.connect`, `iroh:` | Node, Rust (cargo feature `iroh`, on by default) | [atlibs-iroh](knotless://johnandersen777.bsky.social/atlibs-iroh) | | WebRTC | `com.fedfork.exec.webrtc.connect`, `webrtc:` | browser | [atlibs-webrtc](knotless://johnandersen777.bsky.social/atlibs-webrtc) | ## Fetch a URL A transport can also carry plain HTTP: `com.fedfork.exec.http.fetch` sends a URL of its scheme to it ([other URL schemes](modules/calls.md#other-url-schemes)), so a module talks to a service in a guest VM, the socialweb.computer guest agent's MCP server (Streamable HTTP on a unix socket), as it would to any HTTP server. The URL is self-contained; its ref is base64url JSON of what the transport needs, nothing secret: - `webrtc://///`: ref `{ "receipt": { "uri", "cid" }, "bidder"? }`, tag the guest's datachannel tag (`shell`), e.g. `webrtc:////shell/mcp` - `iroh://///`: ref `{ "ticket", "hostKeys": [...] }`, socket `shell` (`/run/socialweb-guest-agent/shell.sock`) or a percent-encoded absolute unix path The module's `http` lists `webrtc://*` or `iroh://*`, and so must the host's allowlist. ## Require a transport A module that needs one lists the NSID in `calls` and declares it as a required hook, so a host without the transport refuses it at load rather than mid-run: `examples/rust/echo/module.json` ```json "hooks": [ { "nsid": "com.fedfork.exec.iroh.connect", "required": true, "entries": ["session", "connect"], "reason": "session and connect dial the guest over iroh: a host built without iroh refuses them at load" } ] ``` The Node CLI loads no transport unless asked, so `echo`'s iroh entries are refused: ```bash run node javascript/src/server/cli.mjs run target/wasm32-unknown-unknown/release/echo.wasm examples/rust/echo/module.json session '{"ticket":"x"}' | jq -r .error ``` ```output Echo needs hook com.fedfork.exec.iroh.connect (…): this host was built without iroh ``` ## Plug one in **JS**: a transport is a map of NSID to call, passed as `hostCall`'s `calls`, and its `fetchers()`, a map of scheme to fetch, `{ "webrtc:": async ({ method, url, headers, body, signal }) => ({ status, headers, body }) }`, spread into `runModule`'s (or `runCompose`'s) `fetchers`. The Node CLI loads modules named in `ATLIBS_TRANSPORTS`, each exporting `transport` and/or `fetchers({ env, stateDir })` (atlibs-iroh's is `js/index.js`): ```bash run=gate ATLIBS_TRANSPORTS=../atlibs-iroh/js/index.js node javascript/src/server/cli.mjs run target/wasm32-unknown-unknown/release/echo.wasm examples/rust/echo/module.json session '{"ticket":""}' ``` **Rust**: a `Transport` says which NSIDs it provides and the `Call` that serves them; the CLI registers the ones it was built with. Its `Call` may also fetch URL schemes of its own, gathered by `transport::fetchers()` into the http allowlist (`ATLIBS_HTTP` names them, `{"iroh://*": {}}`): `rust/host/src/transport.rs` ```rust /// A transport: the NSIDs it provides (request and subscribe) and the call that serves them. #[derive(Clone)] pub struct Transport { pub provides: &'static [&'static str], pub call: Arc, } ``` `rust/host/src/run.rs` ```rust /// The URL schemes (no colon: "iroh") whose HTTP_NSID requests this call serves with fetch(): a /// transport's (transport::fetchers). None by default. fn schemes(&self) -> &'static [&'static str] { &[] } /// One HTTP_NSID request to a URL of one of schemes() (JS: a fetchers entry), as host::Fetch::fetch. fn fetch(&self, _method: &str, url: &str, _headers: Vec<(String, String)>, _body: Option>) -> BoxFut<'_, Result> { ``` `rust/cli/src/main.rs` ```rust pub fn transports(state_dir: &Path) -> Transports { #[allow(unused_mut)] let mut transports: Transports = vec![]; #[cfg(feature = "iroh")] transports.push(atlibs_iroh::transport(env("ATLIBS_IROH_KEY").map(PathBuf::from).unwrap_or_else(|| state_dir.join("iroh_ed25519")))); transports } ``` The CLI's `iroh` feature takes `atlibs-iroh` by git; `rust/Cargo.toml` patches that crate's `atlibs-host` to this checkout, so one `atlibs-host` is built. `cargo loopback` builds `atlibs` without iroh, for checking that refusal. ## iroh `atlibs-iroh` dials an iroh ticket with `dumbpipe` (`brew install dumbpipe`): an ssh command on the guest, or a raw ALPN exchange. With `subscribe: true` it is a session: output arrives as messages, and `send` writes stdin. The host's ssh key never enters wasm (`ATLIBS_IROH_KEY`, else `/iroh_ed25519`). Its tests run both CLIs from an atlibs checkout next to it: ```bash run=gate cd ../atlibs-iroh && node --test js/*.test.mjs examples/*.test.mjs tests/*.test.mjs # tests/loopback: real dumbpipe into a throwaway sshd ``` ## WebRTC `atlibs-webrtc` gives a page `WebrtcHost` (`js/index.js`): it finds the guest's offer, answers, opens a datachannel and can run ssh over it. Signalling goes through the module (sealed with age); connectivity is STUN only. Its loopback test runs headless Chrome against its `webrtc-peer` into a throwaway sshd: ```bash run=gate cd ../atlibs-webrtc && cargo build --release && node --test js/*.test.mjs tests/*.test.mjs ``` > **STUN only.** There is no TURN relay: two peers behind symmetric NATs will not connect. Previous: [Policies](policies.md) | Next: [Examples](examples.md)