Companion bridge
A thin, additive layer so a LAN client (e.g. a phone) can discover what an Odysseus server offers and pair to it, without duplicating any LLM logic.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/companion/ping |
session or token | cheap, auth-validated health check |
| GET | /api/companion/info |
session or token | server identity + capability flags |
| GET | /api/companion/models |
session or token | the caller's own model endpoints |
| GET | /api/companion/pair |
admin cookie | pairing page (a form; never mints) |
| POST | /api/companion/pair |
admin cookie | mint a one-time pairing token (?format=json for an in-app screen) |
/models scopes to the caller's real owner plus legacy null-owner shared rows
(same rule as owner_filter) and never returns API-key material.
Pairing CSRF posture
Minting happens only on POST. The session cookie is SameSite=Lax
(routes/auth_routes.py), so a browser will not send it on a cross-site POST —
the same protection POST /api/tokens relies on. A GET would be unsafe (Lax
cookies ride top-level GET navigations), so GET /pair only renders a form.
Minting invalidates the auth middleware's token cache, so a freshly minted token
works on the next request without a restart.
The pairing/scoping rules live in small, tested units (token_owner,
owner_can_see, mint_pairing_token, pairing.*) — see
tests/test_companion_readonly.py and tests/test_companion_pairing.py.