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
# atlibs-cocore
[atlibs](https://knotless.dev/johnandersen777.bsky.social/atlibs) wasm modules
(knotless://johnandersen777.bsky.social/atlibs) for [co/core](https://cocore.dev), the AI cooperative
([graze-social/cocore](https://github.com/graze-social/cocore)), and a page that runs them: what co/core
[documents for apps](https://cocore.dev/docs/inference), 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](../atlibs/docs/modules/calls.md#service-auth-instead-of-a-key)):
```json
{ "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](https://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](../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](https://cocore.dev/docs/inference/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/docs/server/app-identity.md)):
- `atlibs serve modules/cocore-inference --origin https://<host>` (or a Node server with `ATLIBS_ORIGIN`)
serves `/.well-known/did.json`, `/.well-known/cocore-app.json` and `com.atproto.repo.getRecord` for it;
- `atlibs stage --target spa --origin https://<host>/ modules/cocore-inference modules/cocore-keys` writes
them as static files beside the page (one record, so `getRecord` is 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`.
```bash
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8
```
## Build and test
```bash
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
```bash
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).
```bash
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.