~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-cocore

0
0
main
138 lines · 8.0 KB Raw History
# 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.