atlibs-cocore
atlibs wasm modules (knotless://johnandersen777.bsky.social/atlibs) for co/core, the AI cooperative (graze-social/cocore), and a page that runs them: what co/core documents for apps, its OpenAI-compatible inference API, run as the user with no API key.
Inference as the user, no key
co/core's API is https://cocore.dev/api/v1 (/chat/completions, /models, and the private,
verified and probono routes). It takes a co/core API key, or, for an atproto app, a service-auth token
from the caller's PDS: com.atproto.server.getServiceAuth with aud did:web:cocore.dev and lxm
dev.cocore.inference.dispatch; the completion then runs on behalf of the token's issuer and is billed to
their co/core balance. (co/core's quickstart says aud did:web:console.cocore.dev; the API answers that
401 BadJwtAudience: aud does not match this service and accepts did:web:cocore.dev, the DID whose
document names https://cocore.dev. A permission's aud must be a DID with a service fragment or *, and a
PDS drops aud=did:web:cocore.dev from an OAuth grant, so the session scope is aud=*.)
cocore-inference sends no credentials. It calls com.fedfork.exec.http.fetch, and the host adds them
for that origin (atlibs service auth instead of a key):
{ "https://cocore.dev": { "serviceAuth": { "aud": "did:web:cocore.dev", "lxm": "dev.cocore.inference.dispatch" } } }
Before each request the host mints the token with the run's own session (an OAuth session or an app
password) and sends it as Authorization: Bearer; the wasm never sees it. Under OAuth the PDS issues it
only for a session whose scope has rpc:dev.cocore.inference.dispatch?aud=*, which
is the module's session record (records/cocore-inference.json). With an API key instead (from
cocore-keys, or the console), the same module runs with the host header
{ "https://cocore.dev": { "headers": { "authorization": "Bearer cocore-..." } } }.
The DID must have connected co/core once (signed in at cocore.dev), else co/core
answers 401 onboarding_required, which the module reports as co/core: this account has not connected co/core yet: sign in at https://cocore.dev once, then try again (onboarding_required). Other errors keep
co/core's code and message (co/core HTTP 401 invalid_api_key: Invalid API key, 402 insufficient_credits, 400 tool_calls_not_supported, 503 no_providers_for_version, ...).
Modules
| module | actions | session scope |
|---|---|---|
cocore-inference | chat (one chat completion; stub is the test model), models (the public directory) | rpc:dev.cocore.inference.dispatch?aud=* (the service-auth token) |
cocore-keys | list_keys, create_key, revoke_key, delete_key: API keys, for the key path | rpc:dev.cocore.account.{list,create,revoke,delete}ApiKey?aud=did:web:cocore.dev%23cocore_console |
chat input: model (default stub), prompt, system, messages (OpenAI chat messages, before the
prompt; images as image_url parts), maxTokens (1024), stream (true: the event stream is collected
whole), route (open, private, verified, probono), country, minProviderVersion, minTrust
(verified), and extra (JSON merged over the body: tools, tool_choice, ...). Result:
{ text, finishReason, toolCalls, model, usage, cocore } (cocore: the response's x_cocore, the
provider and the receipt).
An agent loop over co/core, with MCP tools and compaction, is
atlibs-asyncai with its co/core preset (provider openai, endpoint
https://cocore.dev/api/v1, the same serviceAuth grant).
App registration
co/core's Connect your app reads a
dev.cocore.app.registration record (rkey self) from the app's own repo, and sends the browser back only
to a return host that serves /.well-known/cocore-app.json naming the app's DID. This app declares both in
its session record (records/cocore-inference.json, app), and atlibs serves them as the deployment's own
identity, did:web:<host>, with a read-only repo at that origin (atlibs
App identity):
atlibs serve modules/cocore-inference --origin https://<host>(or a Node server withATLIBS_ORIGIN) serves/.well-known/did.json,/.well-known/cocore-app.jsonandcom.atproto.repo.getRecordfor it;atlibs stage --target spa --origin https://<host>/ modules/cocore-inference modules/cocore-keyswrites them as static files beside the page (one record, sogetRecordis a file too); serve them from the host's root, as JSON.
The template's https://atlibs.example becomes the deployment's origin: the registration's website and
returnUrls (https://<host>/), and did:web:atlibs.example the deployment's DID.
Device pairing (dev.cocore.devicePair.start with appDid, then poll for a key bound to the user) is
not wired up: its deviceId and the key it returns must stay server-side, and atlibs has no host-side
secret store for a module's results (a module's state and result pass through the wasm and back to the
caller). This app does not need it: service auth runs inference as the signed-in user with no key at all.
The registration is what a pairing would need once there is a place to keep the key.
Dropped
The earlier modules went through the AppView (did:web:appview.cocore.dev) with XRPC: cocore-explore
(providers, jobs, receipts, settlements, latency), cocore-account (profile and friends records) and
cocore-inference's dispatch (dev.cocore.inference.dispatch proxied to the AppView) and status
(receipts and settlements). They are gone: an app needs the documented inference API, not the AppView's
indexes. The receipt for a completion is in its x_cocore. cocore-app (writing a
dev.cocore.app.registration to the signed-in user's own repo) is replaced by the app registration above:
the registration belongs on the app's identity, not the user's.
Set up
atlibs is a sibling checkout: Cargo fetches the abi crate from git, and the tests and the page import
its JavaScript from ../atlibs.
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8
Build and test
cargo wasm
node --test modules/*/test.mjs web/*/*.test.mjs
(cd ../atlibs && cargo host) # the atlibs CLI, for the Rust host
node --import ../atlibs/rust/host/swap.mjs --test modules/*/test.mjs # the same, on the Rust host
Each module's test fails if it makes a call outside its calls or its session's scope (the service-auth
mint included).
Run
ATLIBS_HTTP='{"https://cocore.dev":{"serviceAuth":{"aud":"did:web:cocore.dev","lxm":"dev.cocore.inference.dispatch"}}}' \
atlibs run modules/cocore-inference chat '{"model":"stub","prompt":"Say hi."}' --app-password you.bsky.social --password-env APP_PASSWORD
python3 -m http.server 8000 --bind 127.0.0.1 --directory web/cocore # http://127.0.0.1:8000/
The page signs in once with the union of the modules' scopes; each run is held to its own, and inference gets its token from the page's host (cocore.dev must answer the page's origin: CORS, see Open).
Live test
test/live.sh runs the inference and key actions against cocore.dev as a test account with an app
password (the stub model; the API key it creates is revoked and deleted). An account that never
connected co/core gets onboarding_required from inference, which the script reports as the expected
answer (REQUIRE_ONBOARDED=1 makes it a failure).
HANDLE=you.bsky.social PASSWORD_ENV=APP_PASSWORD test/live.sh
Open
co/core's API (https://cocore.dev/api/v1) sends CORS headers only to localhost origins, so inference from a
page on a public origin fails in the browser. Use a custom endpoint there until co/core changes that.
