~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-iroh

iroh transport for atlibs hosts: com.fedfork.exec.iroh.connect in Rust (atlibs-host) and JavaScript

0
0
main
116 lines · 6.0 KB Raw History
# atlibs-iroh

The iroh transport for [atlibs](https://knotless.dev/johnandersen777.bsky.social/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 seam
- `js/index.js`: the same transport for Node hosts; `js/fake-bin/`: stand-in `dumbpipe`, `ssh`, guest relay and HTTP service for tests
- `modules/iroh-user-data`: an atlibs module, cloud-init for a guest that serves ssh over iroh (dumbpipe in front of sshd); `computer.socialweb.vm.irohUserData`
- `examples/iroh.mjs`: reach socialweb.computer guests and atirc from a shell
- `tests/`: both atlibs CLIs through this transport
- `lexicons/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`:

```bash
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs && git -C atlibs checkout -q 7df7c6a
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):

```bash
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):

```toml
[dependencies]
atlibs-host = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs", rev = "7df7c6a4eab8b72a77ac5392d9be2fcb5141b4ad" }
atlibs-iroh = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh", rev = "be11ce95acfaa0cf057cc059f955af6be5263596" }
```

Register it with the host's other transports, as the `atlibs` CLI does behind its `iroh` feature:

```rust
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-host`s:

```toml
[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`:

```bash
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's `fetchers`. Idle relays never keep the process up; `fetchers()["iroh:"].close()` ends them.
- Rust: the transport's call answers `Call::schemes()` (`["iroh"]`) and `Call::fetch()`, so `atlibs_host::transport::fetchers`
  puts it in `run::Http::schemes` (the atlibs CLI does, for `iroh://*` in `ATLIBS_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

```bash
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

```bash
(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`.