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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# Compositions
A composition runs several modules as one program. A module calls an NSID; the composition routes that
call to a module that implements it, or to a handler the page or server supplies. Callers can pass
callbacks, and policies can veto a call before it is made.
## Write one
In TypeScript (or [Rust](#in-rust)), from bindings of the modules' module.json; the composition.json it
writes is the file hosts load, published and reviewed. Commit both.
`examples/rust/compose/compose.ts`
```ts
const ui = c.instance("ui", compose);
const lookup = c.instance("lookup", compose, { input: { n: 0 } });
const even = c.instance("even", compose);
const confirm = c.handler("confirm", "The page asks the user.");
c.route(ui, "com.example.compose.lookup", lookup, "ui needs n doubled", { policies: [policy(even, "even")] });
c.route(ui, "com.example.compose.confirm", confirm, "ui asks the user before acting");
```
```bash run
mkdir b && atlibs compose bindings --dir b examples/rust/compose 2>/dev/null
diff b/atlibs.modules.js examples/rust/compose/atlibs.modules.js && echo bindings unchanged
node examples/rust/compose/compose.ts | diff - examples/rust/compose/composition.json && echo composition unchanged
```
```output
bindings unchanged
composition unchanged
```
`atlibs compose bindings` writes `atlibs.modules.js` and `.d.ts` (`atlibs add` does too, for what it
vendors): a route's NSID must be a hook of the caller and its target must implement it, an input key
must be in the module's `config`, of its type. `build()` runs the hosts' load checks and names your line:
```text norun
composition: edge ui com.example.compose.confirm: x (compose) does not implement com.example.compose.confirm
at compose.ts:9:3
```
- **instances**: a module under a name, with optional fixed `input` (keys from the module's `config`).
The same module may appear twice.
- **handlers**: names the caller (page, server, CLI) must supply.
- **edges** (`route`): `from` calling `nsid` goes `to` an instance, at the entry its module
`implements` for `nsid`, or to a `handler`. `reason` is shown wherever the call is logged (default: the
hook's). `policies` must allow it first.
- **limits**: call depth and total routed calls (defaults 8 and 100).
## Declare what a module needs: hooks
A module lists the routed NSIDs it calls in `hooks`, so a composition that forgets one fails at load,
not halfway through a run:
`examples/rust/compose/module.json`
```json
"hooks": [
{ "nsid": "com.example.compose.lookup", "reason": "the example's lookup: another instance doubles n" },
{ "nsid": "com.example.compose.confirm", "reason": "the example's confirm: the caller's handler" }
]
```
| hook field | what |
|---|---|
| `nsid` | the contract: a lexicon procedure |
| `reason` | why the module needs it |
| `required` | no route and no default: refuse the composition at load (default: the call answers `null`) |
| `default` | a module that serves it when nothing routes it (the same publisher's, at the entry its `implements` names), as instance `<instance>-<default>`: one per module, shared by the hooks that name it |
| `combine: "all-allow"` | any number of routes, each answering a policy verdict; every one must allow ([Policies](policies.md)) |
| `entries` | the entries that call it (default: all) |
A module that serves a contract says so in `implements`, and the input an instance may give it in
`config` (fields as in `actions`):
`examples/rust/compose/module.json`
```json
"config": [{ "key": "n", "label": "n", "type": "integer", "description": "The n lookup doubles when a call gives none." }],
"calls": [],
"implements": [{ "nsid": "com.example.compose.lookup", "entry": "lookup" }],
```
An instance's `input` key outside `config`, a value of another type, or a missing `required` field fails
the load. `atlibs compose check <file>` runs these checks without the wasm ([CLI](cli.md#compose));
JS: `checkComposition(manifest, specOf)`.
## Run it
```bash run
atlibs run examples/rust/compose/composition.json ui caller '{"nsid":"com.example.compose.lookup","input":{"n":4}}' \
--handler confirm=wasm:examples/rust/compose#lookup | jq -c .result.q.output.doubled
```
```output
8
```
`ui` asked `lookup` to double 4; the `even` policy allowed it. With 3 the policy denies, and `ui` gets a
failed result instead:
```bash run
atlibs run examples/rust/compose/composition.json ui caller '{"nsid":"com.example.compose.lookup","input":{"n":3}}' \
--handler confirm=wasm:examples/rust/compose#lookup | jq -c .result.q
```
```output
{"error":"policy even denied: n 3 is not even","ok":false}
```
The run also prints a `graph`: every routed call, handler call, callback and policy run.
## Pass a callback
A caller puts `cap("name")` in a call's input. The callee sees a capability token and can call it; the
caller gets `Event::Callback` and answers by calling `reply`:
`examples/rust/compose/src/lib.rs`
```rust
fn caller_handler(run: Run<Value>) -> Result<Handle, String> {
let mut state = if run.state.is_null() { json!({ "progress": [] }) } else { run.state };
Ok(match run.event {
Event::Start => {
let nsid = run.args["nsid"].as_str().ok_or("nsid is required")?;
let mut input = run.args.get("input").cloned().unwrap_or_else(|| json!({}));
input["progress"] = cap("progress");
let call = Call::new("q", nsid).input(input);
let call = match run.args["proxy"].as_str() {
Some(proxy) => call.proxy(proxy),
None => call,
};
Handle::new().call(call).state(state)
}
Event::Callback { cap, input, reply, .. } if cap == "progress" => {
let list = state["progress"].as_array_mut().ok_or("bad state")?;
list.push(input.clone());
let id = format!("reply-{}", list.len());
Handle::new().call(Call::new(id, reply).input(json!({ "seen": input }))).state(state)
}
Event::Result { id, result, .. } if id == "q" => {
let outcome = match result.output() {
Ok(output) => json!({ "ok": true, "output": output }),
Err(error) => json!({ "ok": false, "error": error }),
};
Handle::new().result(json!({ "q": outcome, "progress": state["progress"] }))
}
_ => Handle::new(),
})
}
```
`examples/rust/compose/src/lib.rs`
```rust
fn lookup_handler(run: Run<Value>) -> Result<Handle, String> {
let n = run.args["n"].as_i64().ok_or("n is required")?;
let answer = |ack: Value| Handle::new().result(json!({ "doubled": n * 2, "ack": ack, "caller": run.args["caller"] }));
Ok(match (run.event, cap_token(&run.args["progress"])) {
(Event::Start, Some(token)) => Handle::new().call(Call::new("p", token).input(json!({ "step": 1 }))),
(Event::Start, None) => answer(Value::Null),
(Event::Result { id, result, .. }, _) if id == "p" => answer(result.output().cloned().unwrap_or_else(|e| json!({ "error": e }))),
_ => Handle::new(),
})
}
```
Capabilities are unforgeable per run and die with it. A policy never receives the caller's
capabilities.
## Handlers from the caller
On the command line a handler is another module's entry, a URL, or a command
([CLI: run](cli.md#run)). In JS, `runCompose` takes functions:
```js run
import { readFile } from "node:fs/promises";
import { loadComposition, runCompose } from "./javascript/src/compose/index.js";
import { localModule } from "./javascript/src/run-module/index.js";
import { createNodeContext, fileFetch } from "./javascript/src/server/index.js";
const ctx = createNodeContext({ id: "compose-demo", stateDir: ".state/compose-demo" });
const root = `file://${process.cwd()}/`;
const manifest = JSON.parse(await readFile("examples/rust/compose/composition.json", "utf8"));
// every instance here is the module "compose": load the local build for it
const load = () => localModule(new URL("target/wasm32-unknown-unknown/release/compose.wasm", root), new URL("examples/rust/compose/module.json", root), fileFetch);
const composition = await loadComposition(ctx, manifest, { load });
// the handler the manifest declares; a page would ask the user here
const confirm = async (input, { caller }) => ({ confirmed: input.n, by: caller });
const ran = await runCompose(ctx, composition, "ui", "caller", { nsid: "com.example.compose.confirm", input: { n: 1 } }, { handlers: { confirm } });
console.log(JSON.stringify(ran.result.q.output));
```
```output
{"by":"ui","confirmed":1}
```
In a page, pass `callFor: (instance) => policyCall(stage, instance.module, calls)` so each instance runs
under its own module's session ([Sign in from a page](browser/oauth.md)).
## Parameters
Inputs a binding fills. Types as module.json fields; `values` holds `default` and `enum`; a `secret`
comes from secrets only. `{"$param": key}` in an instance's input takes the value; `{"$host": name}`,
one only the host gives.
```bash run
cat > params.json <<'JSON'
{
"$type": "com.fedfork.exec.composition",
"name": "Doubler",
"parameters": [{ "key": "n", "type": "integer", "maximum": 100, "values": { "default": 4 } }],
"instances": [{ "name": "lookup", "module": "compose", "input": { "n": { "$param": "n" } } }],
"exports": [{ "nsid": "com.example.compose.lookup", "instance": "lookup", "entry": "lookup" }]
}
JSON
atlibs run params.json lookup lookup | jq -c .result.doubled
atlibs run params.json lookup lookup --params '{"inputs":{"n":21}}' | jq -c .result.doubled
atlibs run params.json lookup lookup --params '{"inputs":{"n":200}}' 2>&1 | tail -1
```
```output
8
42
composition: parameter n must be at most 100
```
`--params` is `{inputs, secrets, host}`. JS: `bindParameters(manifest, { inputs, secrets, host })`
before `runCompose`, which refuses a placeholder left unbound. `exports` names the entry serving a
contract: `exportOf(manifest, nsid)`.
A `com.fedfork.exec.binding` record is a composition pinned by CID, its `inputs`, and the names of
its `secrets`. Publishing writes the composition at `<name>` and at `<name>.<its CID>`; the CID loads the
latter, so a republish changes no binding.
## Run one from your own host
A Rust host loads a published composition pinned by CID, and signs its users in once per composition,
with only the scopes its modules need:
`rust/host/src/registry.rs`
```rust
pub async fn published(net: &Net, uri: &str, cid: Option<&str>) -> Result<(String, Composition, Vec<(String, Lib)>), String> {
```
`rust/host/src/signin.rs`
```rust
pub fn scope_of(comp: &Composition) -> String {
…
pub fn composition_prefix(cid: &str) -> String {
…
pub fn composition_store(state_dir: &Path, cid: &str) -> Result<Store, String> {
```
`registry::module_call` gives each instance its own call (its session's scope, attest, did:key,
transports); `params::bind`, then `compose::run_compose`, runs an export (`params::export_of`).
## In Rust
`atlibs-compose` (`rust/compose`, by git like atlibs-host): a build.rs generates the same bindings as
types, so a wrong route or input does not compile; `check(dir)` runs `atlibs compose check`.
`examples/rust-compose/src/lib.rs`
```rust
let lookup = c.instance::<Compose>("lookup").input(ComposeInput::default().n(0));
…
c.route(&ui, compose::Lookup, &lookup, "ui needs n doubled").policy(&even, Compose::even);
```
```bash run=gate
cargo run -q --release --manifest-path rust/Cargo.toml -p rust-compose --target host-tuple --target-dir rust/host/target | diff - examples/rust/compose/composition.json
```
```text norun
error[E0277]: module `Compose` does not implement `Confirm`
= note: route it to an instance whose module implements it
```
## Stage and publish it
`atlibs stage` copies a composition and every module it names; `atlibs add` fetches a published one
with its modules.
```bash run
atlibs stage --target spa --origin https://you.example/ --out site examples/rust/compose/composition.json > /dev/null
find site -type f | LC_ALL=C sort
```
```output
site/composition.json
site/modules/compose.json
site/modules/compose.wasm
site/stage.json
```
```bash run=account
atlibs publish composition examples/rust/compose/composition.json --as you.bsky.social
```
A published composition's instance `module` keys become your own module records; an instance may also
pin a module with `pin`: it loads that version, so the module's republish changes nothing.
Previous: [Publish a module](modules/publish.md) | Next: [Policies](policies.md)