whiskers.git / crates / whiskersd / README.md
1# whiskersd
2
3Whiskers' service, native. It holds the keys so the child's tablet never does, and runs on a
4machine you operate that the tablet reaches over a private network.
5
6It is the **native adapter** of Whiskers' backend: [`whiskers-service`](../whiskers-service/) is what the
7routes do, [`whiskers-ports`](../whiskers-ports/) is what they run on, and this crate is that interface
8implemented with files, `ureq`, a Jev thread, the environment and the system clock. `src/main.rs` is only
9HTTP (`tiny_http`): it turns a request into the service's plain `Request` and the `Response` back. The same
10routes run on any other adapter that passes [`whiskers-conformance`](../whiskers-conformance/); this one is
11held to it in `tests/conformance.rs`.
12
13| Route | In | Out |
14|---|---|---|
15| `POST /check` | `{"direction": "FromChild" or "ToChild", "age": 3..12 (optional; absent is judged as 3), "text": "..."}` | `{"Verdict": ...}`, or `{"Unavailable": "why"}` (Jev) |
16| `POST /speak` | `{"text": "..."}` | `audio/mpeg` (ElevenLabs), or an error status |
17| `POST /speak/v2` | the same | JSON: the MP3 and the time of every character, in Whiskers' own shape (service README) |
18| `POST /v1/messages` | an Anthropic Messages request | the gateway's answer, unchanged (see below) |
19
20The sync, journal, picture, embed, rerank and usage routes are listed in `src/main.rs` and implemented in
21`whiskers-service`.
22
23    WHISKERS_MODEL_URL=<gateway> whiskersd <private-ip>:47900
24
25The app is told this address at run time (a grown-up types `<private-ip>` or `<private-ip>:47900` into its parents' side). Its
26**Check** button asks `POST /usage`, the service's cheapest side-effect-free route, and expects a JSON reply with a `voice`
27section; there is no separate health route.
28
29`/v1/messages` is a narrow proxy to your model gateway (`WHISKERS_MODEL_URL`, required: the service
30exits at start without it), so the gateway itself is never exposed on the private network. It lets
31through one model (`WHISKERS_MODEL`), a reply of at most 1,000 tokens, no streaming, no tools, and a
32body of at most 8 MB; anything else is a 400 before it reaches the gateway.
33
34Bind it to a private-network address, not to `0.0.0.0`. `/check` is given the child's age and
35never the name.
36Environment: `TYPESAFE_API_KEY` for `/check`; `ELEVENLABS_API_KEY` and
37`ELEVENLABS_VOICE_ID` for `/speak`. The voice's daily cap is not an environment setting: it is the
38grown-ups' choice (`voice_daily_chars`, 1,000 until changed in the app), the same on every device. The
39free plan's 10,000 monthly credits spread over a month is about 330 a day.
40
41With a key missing, Jev unreachable or the spend ledger refusing, `/check`
42answers `Unavailable` (the pipeline treats that as a refusal) and `/speak` answers
43an error status (the tablet speaks in its own voice instead). It is not yet run as
44a service; `tools/run-whiskersd.sh` runs it as a transient user service.
45
46## The adapters
47
48| File | Port | How |
49|---|---|---|
50| `src/hubs.rs` | the memory, household and chat hubs | one JSON file each behind a mutex, replaced atomically; the merge rules are the core's |
51| `src/journal.rs` | `Journal` | `journal.jsonl`, one mutex over count-then-append, flushed per append |
52| `src/pictures.rs` | `PictureShelf` | a directory; written to a temporary name and linked into place, never replacing |
53| `src/allowance.rs` | `Allowance` | `tokens.json` and `speak.json` behind one mutex; the rules are the ports' |
54| `src/judge.rs` | `Judge` | the guard's questions asked of Jev on its own thread, with a bounded wait |
55| `src/embed.rs` | `Embedder` | a local llama.cpp embeddings server |
56| `src/speak.rs` | `Voice` | ElevenLabs over `ureq`, with a stand-in voice for a refused one; `timed_from_vendor` is the only code that knows ElevenLabs' alignment and turns it into a `TimedAudio` |
57| `src/model.rs` | `Thinker` | forwards the vetted body to the operator's gateway |
58| `src/secrets.rs`, `src/clock.rs` | `Secrets`, `Clock` | the environment; the system clock |
59| `src/native.rs` | `Backend` | all of the above, built from the environment; it implements every `Has…` capability, which `Service::complete` requires |
60
61Where credentials end up: in the service's environment, put there by whatever starts it (`op-env-run`);
62nothing here writes one down.