# 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 `-`: 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 ` 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) -> Result { 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) -> Result { 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 `` and at `.`; 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 { ``` `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::("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)