Skip to content

Runs on a chosen composition

Proposal, not built. A repository picks an atlibs composition; knotless-run runs the repository’s checks on it, as the repository owner, in process.

  • The owner publishes or picks a composition: modules, wiring, policies, parameters. Its workload modules run the steps.
  • The owner binds it to a repository (or the account) with a com.fedfork.exec.binding record, filled-in parameters, pinned by CID.
  • The owner signs in once at the runner, granting the composition’s scopes.
  • knotless-run reads the binding, loads the pinned composition with atlibs-host, and runs each job through it, on the owner’s session.
uri + cidowner's OAuth sessionexec contractdev.knotless.repocom.fedfork.exec.bindingcom.fedfork.exec.compositionpinned modulesknotless-run + atlibs-hostrented VM / any machine
atlibs run bobvmbuilder.bsky.social/requester-policies # lists instances and entries
atlibs add bobvmbuilder.bsky.social/requester-policies # composition + modules, pinned, in atlibs.lock.json
atlibs publish composition policy-composition.json --rkey requester-policies
  • com.fedfork.exec.composition: instances (module, pin, input, session), handlers, edges (from, nsid, to or handler,
    • policies; no entry), limits.
  • Hooks: required, default, combine: all-allow; implements: an edge reaches the entry its target implements for its NSID, and a target that implements none is refused at load.
  • module.json config types an instance’s input (keys, types, required); not part of the pin.
  • Load checks (both hosts): pins, required hooks, implements, input against config, transports provided; atlibs compose check runs them with no wasm. Compositions are written in TS (compose.ts) or Rust (atlibs-compose) and the composition.json they print is published.
  • Each instance runs on its own module’s session scope.
  • Action fields: type, required, description; the MCP adapter makes JSON Schema of them.
  • iroh: / webrtc: URL fetchers (atlibs-iroh, atlibs-webrtc).
ChangeWhere
parameters on a composition: module.json field types + default, enum, minimum/maximum, secretlexicon, both hosts
{"$param": "<key>"} and {"$host": "<name>"} in instance input, resolved and validated at loadboth hosts
exports: [{nsid, instance, entry}]: which entry serves a contractlexicon, both hosts
Published-composition resolution (module keys against the publisher, @cid, pins) moved from rust/cli into atlibs-hostrust/host
Server OAuth confidential client usable as a library (login, callback, sealed per-DID sessions, single-flight refresh, revoke)rust/host
atlibs publish sweeps *.composition.json, checks each instance’s module is published at its pin, writes records/rust/cli
com.fedfork.exec.binding lexiconlexicons/
Per-composition client metadata and sessions (Sessions, below)rust/host
Net denylist for workloads (below)both hosts’ http.fetch
com.fedfork.exec.*

atlibs’ sandboxed wasm: compositions, their parameters, com.fedfork.exec.binding

computer.socialweb.*

what runs on external compute, which wasm cannot sandbox: machines, processes, CI workloads

dev.knotless.*

knotless runner state only

Contracts on external compute:

  • computer.socialweb.machine.provision, computer.socialweb.machine.release
  • computer.socialweb.cicd.process.exec, .start, .read, .write, .kill, .list
  • computer.socialweb.cicd.workload.run

Generic; not MCP. computer.socialweb.cicd.process.*:

ProcedureInputOutput
execcommand, stdin?, cwd?, timeoutMs?exitCode, stdout, stderr, truncated, timedOut?
startcommand, stdin?, cwd?id
readid, cursor?, maxBytes?, waitMs?output, cursor, done, exitCode?, dropped?
writeid, data, close?{}
killid, signal?{}
listjobs
  • The same tools the guest agent’s shell serves today, minus MCP.
  • MCP is an adapter (tools named as the procedures) for agents.
  • Dependency (kcp): the guest agent’s shell service serves this contract as plain HTTP (POST /<procedure>), with /mcp as the adapter.
  • A workload module calls it over a transport URL (iroh:///<ref>/shell/...) through http.fetch, or through a composed exec-client instance (a hook routed by an edge).

A composition’s workload module runs the job:

  • computer.socialweb.machine.provision / release (exports): the requester (or an adapter) rents and releases. Output: an opaque lease and the transport URL.
  • computer.socialweb.cicd.workload.run (exports): input {lease, url, job}. It fetches the checkout, runs each step with start / write / read, streams output, returns per-step exit codes.
  • job: the expanded workflow (steps, variables, the steps’ granted secrets) and what to check out: {repo: {did, key}, commit, base?, shared: [{did, key, commit}]} (base: a pull’s, merged on the machine as the runner merges today; shared: the lock’s shared steps).
  • Log streaming: the workload emits events ({step, output}); the runner writes them to the run’s log as they come.
  • Checkout: the workload fetches the commit itself from the owner’s PDS (the repository’s dev.knotless.ref and pack records and blobs, public reads) and writes the packs into the machine (git index-pack, git checkout). No tarball from the runner.
  • A private repository: the runner passes the workload a narrower grant, read-only, for that repository’s space alone (job.read); the workload never holds the owner’s broader scope.

com.fedfork.exec.binding, in the owner’s repo:

{
"$type": "com.fedfork.exec.binding",
"composition": { "uri": "at://did:plc:…/com.fedfork.exec.composition/socialweb-computer-vm", "cid": "bafyrei…" },
"inputs": { "bidders": ["did:plc:4vcig7s43z3nyqrnepmcfzqt"], "cpus": 2, "mem": 4 },
"secrets": ["API_TOKEN"],
"createdAt": "2026-11-10T00:00:00Z"
}
  • rkey: the repository’s key, or self (account default).
  • Lookup: getRecord only, never listRecords.
  • Precedence: a workflow’s executor: <rkey>, then the repository’s, then self.
  • secrets names parameters filled from the repository’s sealed secrets; no value is ever in a record.
# what the runner reads, for repo key <key>
curl -s "$PDS/xrpc/com.atproto.repo.getRecord?repo=$OWNER&collection=com.fedfork.exec.binding&rkey=<key>"
curl -s "$PDS/xrpc/com.atproto.repo.getRecord?repo=$OWNER&collection=com.fedfork.exec.binding&rkey=self"

The binding goes in the repository owner’s account at the repository’s key (an organization’s repository: the organization’s account). Its key is the key of its dev.knotless.repo record, whose rkey is its name:

OWNER=did:plc:lpfuqerea3deuoyrn7ojser4 NAME=kc-ci-test
PUBLISHER=did:plc:ocnuqjlzgohypj3zoznermms COMPOSITION=socialweb-computer-cicd
get() { pds=$(curl -s "https://plc.directory/$1" | jq -r '.service[] | select(.id == "#atproto_pds").serviceEndpoint')
curl -s "$pds/xrpc/com.atproto.repo.getRecord?repo=$1&collection=$2&rkey=$3"; }
KEY=$(get $OWNER dev.knotless.repo $NAME | jq -r .value.key)
COMP=at://$PUBLISHER/com.fedfork.exec.composition/$COMPOSITION
CID=$(get $PUBLISHER com.fedfork.exec.composition $COMPOSITION | jq -r .cid)
mkdir -p records
jq -n --arg uri "$COMP" --arg cid "$CID" '{"$type": "com.fedfork.exec.binding", composition: {uri: $uri, cid: $cid},
inputs: {bidders: ["did:plc:4vcig7s43z3nyqrnepmcfzqt"]}, createdAt: (now | todate)}' > "records/$KEY.json"
export HANDLE=you.bsky.social PASSWORD=<app password>
atlibs publish records "records/$KEY.json" --app-password "$HANDLE" --password-env PASSWORD # rkey = the file name

records/self.json instead binds every repository of the account. An organization’s admin binds from Settings › Executor: the organization’s gateway writes it as the organization.

  • Between modules: composition first (hooks, hook defaults, edges). Workload modules, the exec contract and its MCP adapter, user_data, policies: composed instances, not crates linked into one wasm.
  • Cargo git deps pinned by rev: only where composition cannot do it.
  • A host (knotless-run) is not a module: it depends on atlibs-host and atlibs-iroh as Rust crates, pinned by rev.
  • Rust, in process: atlibs-host and atlibs-iroh as Cargo git dependencies pinned by rev, behind a feature (a host, see Reuse). No Node, no CLI, no extra binary.
[dependencies]
atlibs-host = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs", rev = "<sha>", optional = true }
atlibs-iroh = { git = "https://knotless.dev/johnandersen777.bsky.social/atlibs-iroh", rev = "<sha>", optional = true }
[features]
compositions = ["dep:atlibs-host", "dep:atlibs-iroh"]
  • One executor, --executor composition, behind the Executor / Session seam the runner-socialweb-computer experiment used.
  • A run: read the binding → check the publisher is trusted → load the composition at its CID → resolve parameters (secrets from the repository’s sealed secrets, $host from the runner) → provision → workload.run → release, always.
  • Leases kept in the runner’s state; a start releases any of a run not going.
  • Flags:
knotless-run --executor composition \
--trust-publisher did:plc:ocnuqjlzgohypj3zoznermms \
--origin https://runner.example \
alice.example/project

OAuth is per composition, not per runner. The repository owner’s own session for that composition rents and runs: the owner pays, the owner’s associations and vouches count.

  • The runner serves one client metadata document (CIMD) per bound composition, keyed by its CID; scope: the union of its modules’ session scopes, nothing more. A confidential client (private_key_jwt; the runner’s key at /oauth/jwks.json).
curl -s https://runner.example/oauth/composition/<cid>/client-metadata.json | jq -c '{client_id, scope, token_endpoint_auth_method}'
  • Sign-in, from the Executor page: GET https://runner.example/oauth/composition/<cid>/login?handle=<owner>.
  • Session: sealed in the runner’s state, keyed by (DID, composition CID); one refresh at a time per key.
  • Use: every run of a binding that pins that CID, wherever it is bound. A binding moved to a new CID needs its sign-in.
  • Revoke: the Executor page’s “Sign out” (POST https://runner.example/oauth/composition/<cid>/logout), or the owner’s PDS.
  • Each instance still runs only within its own module’s scope.
  • Trusted publishers: --trust-publisher <did|handle>, repeatable; default the runner’s own DID. A composition, its modules and hook defaults must all be by trusted publishers, at their pins.
  • Net denylist for workloads’ http.fetch, on by default: loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), instance metadata (169.254.169.254, fd00:ec2::254, metadata.google.internal, metadata.azure.com), and private ranges unless --allow-net <cidr>.
  • Secrets: from the repository’s sealed secrets only; named in the binding, never valued; redacted from logs; none for pulls and batches.
  • Pulls run on the base’s binding, never one the head adds.
  • Limits: the runner’s --cpus, --memory, --run-timeout cap the binding’s.

web/src/routes/[authority]/[repo]/settings/executor, and the same in account settings for self:

  1. Paste <handle>/<composition> or an at:// URI.
  2. It resolves and shows instances (module, publisher, pin), edges and policies, the scope union, and the parameters.
  3. A form from parameters: string text (enum a select), integer/number with bounds, boolean a checkbox, object/array JSON; secret links to the repository’s secrets.
  4. Authorize the composition at the runner (its own CIMD, its scope).
  5. Save writes the binding, pinned by CID. A newer version shows as “update available”; it never runs on its own.

A new repository (not made yet), served at atlibs.socialweb.computer: author and publish compositions.

  • Add instances by module ref; draw edges from each module’s hooks to instances that implements them; attach policies.
  • Declare parameters, $param them into instance inputs.
  • Validate as both hosts load (pins, required hooks, parameters).
  • Publish to the signed-in user’s repo (com.fedfork.exec.composition), as atlibs publish composition does.
  • A static page on atlibs’ browser host and its OAuth.
  1. atlibs: parameters, $param/$host, exports and the binding lexicon; resolution and per-composition server OAuth in atlibs-host; publish sweeps compositions; net denylist. Conformance tests on both hosts.
  2. computer.socialweb.* lexicons (machine, process, workload); the process-client and its MCP adapter as composable modules; a workload module (workload.run: fetch the commit, run the steps); user_data in wasm; publish socialweb-computer-vm (requester, policies, user_data, process client, workload).
  3. kcp (dependency): the guest agent’s shell service serves computer.socialweb.cicd.process.*, /mcp as an adapter.
  4. knotless-run: --executor composition in process, sessions, trusted publishers, leases. Tests with a fake transport.
  5. knotless UI: Executor pages. Shots and e2e.
  6. atlibs-web-editor-ui.
  7. One live run: a test repository bound to socialweb-computer-vm, a push runs on hat-9, the VM released, test records deleted.

Risks: the parameter syntax and contracts are hard to change once bindings use them; a runner holding owners’ sessions is a target (sealed storage, minimal scope, revocation); atlibs-host in knotless-run adds wasmtime to its build; a bidder that accepts and never boots (seen on hat-7) needs provision to time out and release to work without guest reports.

  • Namespaces as above; OAuth per composition (CIMD by CID); the workload fetches the commit; the net denylist denies private ranges (RFC 1918, ULA) unless --allow-net <cidr>; a private repository’s checkout through a read-only per-repository grant from the runner.
  • Machine contracts: computer.socialweb.vm.provision / .release (beside the existing computer.socialweb.vm.userData), plus computer.socialweb.vm.prepare, run once per owner (the requester’s bind_key). The owner’s attestation key, did:keys and records ledger are kept per owner, whichever composition runs; only the OAuth session is per composition.
  • A rent bounds its own waits; one that fails or never boots gives the machine back and deletes its records itself, so the runner puts no timeout of its own on it.
  • Parameters’ default and enum live in values: {default, enum}, an object, so the record stays a lexicon unknown; minimum/maximum are integers (atproto has no floats).
  • Bind before run: both hosts’ run refuses a placeholder left unbound; atlibs run --params {inputs, secrets, host}.
  • atlibs-host gained registry (published compositions by CID), signin (per-composition client routes, scope_of, a store per CID) and registry::module_call; rusqlite 0.40, so one sqlite links beside knotless-serve.
  • knotless-run takes atlibs-host and atlibs-iroh by rev, atlibs-host’s the one atlibs-iroh pins (one copy); atlibs_host::tls() picks rustls’ provider.
  • Runner seam: Session::step runs a step as a process, or on the composition’s workload; a composition’s run checks out once (workload.run {checkout}), then runs one step a call.
  • Checkout: the machine clones <appview>/<did>/<name> (knotless serves git over https from the owner’s PDS) at the commit. Pulls and the queue are refused for now: their merged commit is on no remote.
  • --trust-publisher takes DIDs. Workloads’ http.fetch reaches only the machine’s transport (iroh://*): no http(s) at all, which covers the net denylist.
  • --app-password <did>=<file>: an owner who runs as an app password (tests, the operator’s own accounts); an OAuth session for the composition wins.
  • Modules: cicd-workload and process-mcp (atlibs-socialweb-computer-requester), composed in bobvmbuilder.bsky.social/socialweb-computer-cicd, every instance pinned.
  • Pages: Settings › Executor, for a repository (its key) and for the account (self), through dev.knotless.getBinding, getComposition and putBinding; knotless’s OAuth scope gains repo:com.fedfork.exec.binding. The sign-in link is built from the runner’s address, typed in.
  • An organization’s repository binds in the organization’s account, where the runner reads it: an admin who joined writes it from the pages as its other settings are, as the organization through the gateway it delegated to, whose grant gains repo:com.fedfork.exec.binding (a delegation from before it is refused, saying to delegate again). No binding in the admin’s account, no listing.
  • The repository page reads its binding by getRecord, no index: getBinding?repo=<at:// URI> finds its key from its dev.knotless.repo record, then its owner’s binding at it, else their self (inherited). putBinding {repo} takes the key from the record too, never from the page.
  • atlibs 0.3.0’s model: knotless-run pins atlibs-host at v0.3.0 (and atlibs-iroh at 0aae189, which pins the same); the runner reads only exports (which still name an entry), so routing by implements needed no runner change. The Executor page shows each route’s NSID and target, and each instance’s input.