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.bindingrecord, filled-in parameters, pinned by CID. - The owner signs in once at the runner, granting the composition’s scopes.
knotless-runreads the binding, loads the pinned composition withatlibs-host, and runs each job through it, on the owner’s session.
atlibs today
Section titled “atlibs today”atlibs run bobvmbuilder.bsky.social/requester-policies # lists instances and entriesatlibs add bobvmbuilder.bsky.social/requester-policies # composition + modules, pinned, in atlibs.lock.jsonatlibs publish composition policy-composition.json --rkey requester-policiescom.fedfork.exec.composition:instances(module,pin,input,session),handlers,edges(from,nsid,toorhandler,policies; noentry),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
configtypes an instance’sinput(keys, types,required); not part of the pin. - Load checks (both hosts): pins, required hooks, implements, input
against
config, transports provided;atlibs compose checkruns 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).
atlibs changes
Section titled “atlibs changes”| Change | Where |
|---|---|
parameters on a composition: module.json field types + default, enum, minimum/maximum, secret | lexicon, both hosts |
{"$param": "<key>"} and {"$host": "<name>"} in instance input, resolved and validated at load | both hosts |
exports: [{nsid, instance, entry}]: which entry serves a contract | lexicon, both hosts |
Published-composition resolution (module keys against the publisher, @cid, pins) moved from rust/cli into atlibs-host | rust/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 lexicon | lexicons/ |
| Per-composition client metadata and sessions (Sessions, below) | rust/host |
| Net denylist for workloads (below) | both hosts’ http.fetch |
Namespaces
Section titled “Namespaces”com.fedfork.exec.*atlibs’ sandboxed wasm: compositions, their
parameters,com.fedfork.exec.bindingcomputer.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.releasecomputer.socialweb.cicd.process.exec,.start,.read,.write,.kill,.listcomputer.socialweb.cicd.workload.run
The process contract
Section titled “The process contract”Generic; not MCP. computer.socialweb.cicd.process.*:
| Procedure | Input | Output |
|---|---|---|
exec | command, stdin?, cwd?, timeoutMs? | exitCode, stdout, stderr, truncated, timedOut? |
start | command, stdin?, cwd? | id |
read | id, cursor?, maxBytes?, waitMs? | output, cursor, done, exitCode?, dropped? |
write | id, data, close? | {} |
kill | id, signal? | {} |
list | jobs |
- 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/mcpas the adapter. - A workload module calls it over a transport URL
(
iroh:///<ref>/shell/...) throughhttp.fetch, or through a composed exec-client instance (a hook routed by an edge).
Workloads
Section titled “Workloads”A composition’s workload module runs the job:
computer.socialweb.machine.provision/release(exports): the requester (or an adapter) rents and releases. Output: an opaqueleaseand the transport URL.computer.socialweb.cicd.workload.run(exports): input{lease, url, job}. It fetches the checkout, runs each step withstart/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.refand 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.
Records
Section titled “Records”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:
getRecordonly, neverlistRecords. - Precedence: a workflow’s
executor: <rkey>, then the repository’s, thenself. secretsnames 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"Bind a repository
Section titled “Bind a repository”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-testPUBLISHER=did:plc:ocnuqjlzgohypj3zoznermms COMPOSITION=socialweb-computer-cicdget() { 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/$COMPOSITIONCID=$(get $PUBLISHER com.fedfork.exec.composition $COMPOSITION | jq -r .cid)mkdir -p recordsjq -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 namerecords/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-hostandatlibs-irohas Rust crates, pinned byrev.
The runner
Section titled “The runner”- Rust, in process:
atlibs-hostandatlibs-irohas Cargo git dependencies pinned byrev, 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 theExecutor/Sessionseam therunner-socialweb-computerexperiment 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,
$hostfrom 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/projectSessions
Section titled “Sessions”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.
Security
Section titled “Security”- 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-timeoutcap the binding’s.
knotless: Executor settings
Section titled “knotless: Executor settings”web/src/routes/[authority]/[repo]/settings/executor, and the same in
account settings for self:
- Paste
<handle>/<composition>or anat://URI. - It resolves and shows instances (module, publisher, pin), edges and policies, the scope union, and the parameters.
- A form from
parameters:stringtext (enuma select),integer/numberwith bounds,booleana checkbox,object/arrayJSON;secretlinks to the repository’s secrets. - Authorize the composition at the runner (its own CIMD, its scope).
- Save writes the binding, pinned by CID. A newer version shows as “update available”; it never runs on its own.
atlibs-web-editor-ui
Section titled “atlibs-web-editor-ui”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
hooksto instances thatimplementsthem; attach policies. - Declare parameters,
$paramthem into instance inputs. - Validate as both hosts load (pins, required hooks, parameters).
- Publish to the signed-in user’s repo
(
com.fedfork.exec.composition), asatlibs publish compositiondoes. - A static page on atlibs’ browser host and its OAuth.
- atlibs:
parameters,$param/$host,exportsand the binding lexicon; resolution and per-composition server OAuth inatlibs-host;publishsweeps compositions; net denylist. Conformance tests on both hosts. 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; publishsocialweb-computer-vm(requester, policies, user_data, process client, workload).- kcp (dependency): the guest agent’s shell service serves
computer.socialweb.cicd.process.*,/mcpas an adapter. - knotless-run:
--executor compositionin process, sessions, trusted publishers, leases. Tests with a fake transport. - knotless UI: Executor pages. Shots and e2e.
- atlibs-web-editor-ui.
- 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.
Decided
Section titled “Decided”- 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.
Decisions made building it
Section titled “Decisions made building it”- Machine contracts:
computer.socialweb.vm.provision/.release(beside the existingcomputer.socialweb.vm.userData), pluscomputer.socialweb.vm.prepare, run once per owner (the requester’sbind_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’
defaultandenumlive invalues: {default, enum}, an object, so the record stays a lexiconunknown;minimum/maximumare integers (atproto has no floats). - Bind before run: both hosts’ run refuses a placeholder left unbound;
atlibs run --params {inputs, secrets, host}. atlibs-hostgainedregistry(published compositions by CID),signin(per-composition client routes,scope_of, a store per CID) andregistry::module_call; rusqlite 0.40, so one sqlite links beside knotless-serve.- knotless-run takes
atlibs-hostandatlibs-irohbyrev, atlibs-host’s the one atlibs-iroh pins (one copy);atlibs_host::tls()picks rustls’ provider. - Runner seam:
Session::stepruns 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-publishertakes DIDs. Workloads’http.fetchreaches 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-workloadandprocess-mcp(atlibs-socialweb-computer-requester), composed inbobvmbuilder.bsky.social/socialweb-computer-cicd, every instance pinned. - Pages: Settings › Executor, for a repository (its key) and for the account (
self), throughdev.knotless.getBinding,getCompositionandputBinding; knotless’s OAuth scope gainsrepo: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 itsdev.knotless.reporecord, then its owner’s binding at it, else theirself(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 byimplementsneeded no runner change. The Executor page shows each route’s NSID and target, and each instance’s input.