atlibs-iroh
The iroh transport for atlibs
(knotless://johnandersen777.bsky.social/atlibs) hosts: it serves com.fedfork.exec.iroh.connect, so a
module can reach a VM over an iroh ticket. It dials with dumbpipe: an ssh command on the guest, or a
raw ALPN exchange; with subscribe: true it is a session (output arrives as messages, send writes
stdin). It also provides the iroh: scheme fetcher behind com.fedfork.exec.http.fetch: HTTP/1.1 to a unix
socket on the guest (the guest agent's shell MCP service) over ssh root@guest socialweb-guest-agent relay <socket>.
iroh://<ticket>/<path> (a market endpoint, e.g. a bidder's offering endpoints) is HTTP/1.1 over dumbpipe connect <ticket> itself: no ssh, no key; atlibs hosts send module calls with endpoints there with service auth.
The host's ssh key never enters wasm (ATLIBS_IROH_KEY, else <state>/iroh_ed25519).
src/lib.rs: the Rust transport (atlibs_iroh::transport), on atlibs-host's seamjs/index.js: the same transport for Node hosts;js/fake-bin/: stand-indumbpipe,ssh, guest relay and HTTP service for testsmodules/iroh-user-data: an atlibs module, cloud-init for a guest that serves ssh over iroh (dumbpipe in front of sshd);computer.socialweb.vm.irohUserDataexamples/iroh.mjs: reach socialweb.computer guests and atirc from a shelltests/: both atlibs CLIs through this transportlexicons/com/fedfork/exec/iroh/connect.json: the connect call's schema
Set up
Clone atlibs next to this repo; the JS side and the tests import it from ../atlibs:
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs && git -C atlibs checkout -q 88b2ba8
brew install dumbpipe # for real dials and tests/loopback.test.mjs
Build
The crate depends on atlibs-host by git (Cargo.lock pins the commit):
cargo build --release -p atlibs-iroh # the transport
cargo wasm # modules/* -> target/wasm32-unknown-unknown/release/*.wasm
Link it into a Rust host
atlibs-host at the rev this crate takes it at (the same full rev, so cargo builds one copy):
[dependencies]
atlibs-host = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs", rev = "88b2ba88a22946a104c28c283bd0e9ecf26dec1f" }
atlibs-iroh = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh", rev = "0aae189c258e0e72ef1b170cf73b79ef4c6228c9" }
Register it with the host's other transports, as the atlibs CLI does behind its iroh feature:
transports.push(atlibs_iroh::transport(state_dir.join("iroh_ed25519")));
A host that takes atlibs-host from a path (atlibs itself) must point this crate's copy at it too, or
cargo builds two atlibs-hosts:
[patch."https://knotless.dev/johnandersen777.bsky.social/atlibs"]
atlibs-host = { path = "host" }
Use it from Node
npm install @publicdomainrelay/atlibs-iroh @publicdomainrelay/atlibs (Node >= 25, and dumbpipe on
PATH), or use a checkout. The atlibs Node CLI loads transports named in ATLIBS_TRANSPORTS:
cd ../atlibs && cargo wasm
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>"}'
Fetch from a guest
iroh:///<ref>/<socket>/<path...>, where ref is base64url of the JSON {"ticket", "hostKeys": [...]} and
socket is shell (/run/socialweb-guest-agent/shell.sock, the guest agent's MCP service: POST .../shell/mcp)
or a percent-encoded absolute unix path. Nothing in the URL is a secret. The request goes as HTTP/1.1 over
ssh root@guest socialweb-guest-agent relay <socket> through dumbpipe, with the host keys pinned as for connect.
Each relay carries one request at a time and stays open (HTTP keep-alive) for the next request to the same
(ref, socket); concurrent requests open more relays (a long-poll read holds one). A request whose relay fails
before any response byte arrives is retried once on a fresh relay. Responses come back whole, chunked bodies
decoded, at most 16 MiB.
- Node:
fetchers({ env, stateDir })gives{ "iroh:": async ({ method, url, headers, body, signal }) => ({ status, headers, body }) }, ready to spread into runModule'sfetchers. Idle relays never keep the process up;fetchers()["iroh:"].close()ends them. - Rust: the transport's call answers
Call::schemes()(["iroh"]) andCall::fetch(), soatlibs_host::transport::fetchersputs it inrun::Http::schemes(the atlibs CLI does, foriroh://*inATLIBS_HTTP). Its relays end with the transport.
A module reaches it through com.fedfork.exec.http.fetch once its http list and the host allowlist both
accept iroh://*.
Run the example
atlibs run bobvmbuilder.bsky.social/iroh-user-data iroh_user_data "{\"ssh_key\":\"$(cat ~/.ssh/id_ed25519.pub)\"}" | jq -r .user_data # cloud-init
node examples/iroh.mjs ssh guest.json ~/.ssh/id_ed25519 uname -a
Test
(cd ../atlibs && cargo wasm && cargo host) # echo.wasm and the atlibs CLI (or set ATLIBS_BIN)
cargo test --release -p atlibs-iroh && cargo wasm
node --test js/*.test.mjs examples/*.test.mjs tests/*.test.mjs modules/*/test.mjs
tests/conformance.test.mjs runs both CLIs with js/fake-bin on PATH, and http.fetch to an iroh:/// URL on both engines; tests/loopback.test.mjs dials a
throwaway sshd through real dumbpipe (it skips, saying why, without dumbpipe, sshd or ssh-keygen).
The Rust CLI in these tests is atlibs' cargo host build, which links this crate.
Publish
js/ is @publicdomainrelay/atlibs-iroh on npm (npm publish --access public; the tests and
js/fake-bin/ stay out of the tarball). It shells out to dumbpipe (and ssh), which must be on the
host's PATH. The crate is not on crates.io: hosts link it by git. modules/*: atlibs publish --dir modules.
