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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# Rust servers
`rust/host` (crate `atlibs-host`) runs the same modules, step protocol, compositions and state dir as
the JS hosts, with wasmtime. The `atlibs` CLI (`rust/cli`) is built on it, and `atlibs serve` is a
ready-made server; this page shows the pieces to embed in your own.
## Serve modules without writing a server
```bash run
atlibs serve examples/rust/compose --port 8791 &
```
```bash run
curl -s -X POST localhost:8791/run/lookup -H 'content-type: application/json' -d '{"n":5}'; echo
```
```output
"doubled": 10
```
Sign-in, origins and reverse proxies: [Server sessions](sessions.md).
## Add the crate
`rust/cli/Cargo.toml`
```toml
[dependencies]
atlibs-host = { path = "../host" }
atlibs-iroh = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh", optional = true }
```
`atlibs-host` has no transport in it; `atlibs-iroh` is one, from its own repo behind a cargo feature
([Transports](../transports.md)). Outside this repo, take both by git:
```toml norun
[dependencies]
atlibs-host = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs" }
atlibs-iroh = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh" }
```
Build and test everything in `rust/`:
```bash run=gate
cargo host # the CLI, into rust/host/target
cargo host-test # atlibs-host's and the CLI's tests
```
## Choose who runs act as
`SessionConfig` is explicit: an app password or a saved OAuth session. Nothing is read from the
environment, and no config means signed out.
`rust/host/src/host.rs`
```rust
/// Who a session signs in as: an explicit choice, never read from the environment.
/// No config, no session.
#[derive(Clone, Debug)]
pub enum SessionConfig {
/// The simple / testing case: an app password at `service` (an entryway or PDS). The password may
/// be None when the state dir holds this identifier's saved session.
AppPassword { identifier: String, password: Option<String>, service: String },
/// A user's OAuth session, saved in the state dir by `atlibs login` or a server's /oauth/callback.
OAuth { did: String },
}
```
## Run a module
A module's `call` is `HostCall` (XRPC with the session) wrapped in `ScopeCall` when signed in, so the
run makes only the calls its own session record's scope allows.
`rust/host/src/registry.rs`
```rust
let mut call: Arc<dyn Call> = Arc::new(HostCall { fetch: fetch.clone(), session: session.clone(), attest });
if session.is_some() {
let key = module.key.clone().unwrap_or_else(|| name.to_string());
call = Arc::new(ScopeCall { module: key, session: module.scope.clone(), inner: call });
}
```
`Ctx` holds the wasm runner and the records ledger; `run_module` drives the step loop and returns the
same `{ entry, result, error?, events, calls }` JSON as the JS host.
`rust/cli/src/run.rs`
```rust
let call = self.call_for(module, module.id.as_deref().unwrap_or_default()).await?;
let ctx = Ctx { runner: Arc::new(Bound { wasm: self.wasm.clone(), bytes: module.bytes.clone(), sha256: module.sha256.clone() }), ledger, now: None };
let options = Options { load: Some(self.hook_loader(module, pinned.clone())), ..options };
Ok(run_module(&ctx, module, entry, &with_did, call, options).await)
```
Compositions go through `compose::run_compose` the same way, with a `call_for` per instance;
`compose::check_specs` runs the load checks from module.json fields alone (`atlibs compose check`).
## Serve client metadata
`ClientMetadataHandler::from_stage(&stage)` takes an `atlibs stage --target server` stage.json and
answers `GET /oauth/<session>/client-metadata.json` with any `http::Request`; anything else is `None`.
`rust/host/src/oauth.rs`
```rust
/// Serves `<path><session>/client-metadata.json` for each record, the client_id being that URL on
/// `origin` (on a loopback Host, the request's origin when `origin` is unset). With `name`,
/// `<path><name>/client-metadata.json` is the composite of all of them.
#[derive(Clone, Debug, Default)]
pub struct ClientMetadataHandler {
```
`rust/cli/src/serve.rs`
```rust
if let Some(res) = self.oauth.handle(&req) {
```
## One session per signed-in user
Each request carries its user's session handle; the server opens that user's session and runs with
it alone.
`rust/cli/src/serve.rs`
```rust
// who this run acts as: the request's signed-in user (their session alone), else the server's own (--as / --app-password)
let host = match handle_of(&req) {
None => self.host.clone(),
Some(handle) => {
let did = self.cli.store().and_then(|store| oauth_client::did_of_handle(store, handle));
let session = match did {
Ok(Some(did)) => self.session(&did).await,
Ok(None) => Err("unknown or expired session: sign in again (GET /oauth/login?handle=...)".into()),
Err(e) => Err(e),
};
match session {
Ok(session) => Host { session: Some(session), ..self.host.clone() },
Err(e) => return json_reply(401, &json!({ "error": "AuthRequired", "message": e })),
}
}
};
```
## Same results as the JS host
A conformance suite runs the same modules, inputs and call stubs through both hosts and compares
results, call logs and ledgers. It runs JS tests against the Rust host too:
```bash run=gate
node --test rust/host/conformance.test.mjs
node --import ./rust/host/swap.mjs --test javascript/src/run-module/*.test.mjs examples/rust/*/test.mjs javascript/src/compose/compose.test.mjs javascript/src/server/server.test.mjs
```
Previous: [Node servers](node.md) | Next: [Server sessions](sessions.md)