1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# 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.<name>.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>/<tag>/<path>`: ref `{ "receipt": { "uri", "cid" }, "bidder"? }`, tag the guest's
datachannel tag (`shell`), e.g. `webrtc:///<ref>/shell/mcp`
- `iroh:///<ref>/<socket>/<path>`: 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":"<an iroh 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<dyn Call>,
}
```
`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<Vec<u8>>) -> BoxFut<'_, Result<crate::host::Response, String>> {
```
`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
`<state>/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)