whiskersd
Whiskers' service, native. It holds the keys so the child's tablet never does, and runs on a machine you operate that the tablet reaches over a private network.
It is the native adapter of Whiskers' backend: whiskers-service is what the
routes do, whiskers-ports is what they run on, and this crate is that interface
implemented with files, ureq, a Jev thread, the environment and the system clock. src/main.rs is only
HTTP (tiny_http): it turns a request into the service's plain Request and the Response back. The same
routes run on any other adapter that passes whiskers-conformance; this one is
held to it in tests/conformance.rs.
| Route | In | Out |
|---|---|---|
POST /check | {"direction": "FromChild" or "ToChild", "age": 3..12 (optional; absent is judged as 3), "text": "..."} | {"Verdict": ...}, or {"Unavailable": "why"} (Jev) |
POST /speak | {"text": "..."} | audio/mpeg (ElevenLabs), or an error status |
POST /speak/v2 | the same | JSON: the MP3 and the time of every character, in Whiskers' own shape (service README) |
POST /v1/messages | an Anthropic Messages request | the gateway's answer, unchanged (see below) |
The sync, journal, picture, embed, rerank and usage routes are listed in src/main.rs and implemented in
whiskers-service.
WHISKERS_MODEL_URL=<gateway> whiskersd <private-ip>:47900
The app is told this address at run time (a grown-up types <private-ip> or <private-ip>:47900 into its parents' side). Its
Check button asks POST /usage, the service's cheapest side-effect-free route, and expects a JSON reply with a voice
section; there is no separate health route.
/v1/messages is a narrow proxy to your model gateway (WHISKERS_MODEL_URL, required: the service
exits at start without it), so the gateway itself is never exposed on the private network. It lets
through one model (WHISKERS_MODEL), a reply of at most 1,000 tokens, no streaming, no tools, and a
body of at most 8 MB; anything else is a 400 before it reaches the gateway.
Bind it to a private-network address, not to 0.0.0.0. /check is given the child's age and
never the name.
Environment: TYPESAFE_API_KEY for /check; ELEVENLABS_API_KEY and
ELEVENLABS_VOICE_ID for /speak. The voice's daily cap is not an environment setting: it is the
grown-ups' choice (voice_daily_chars, 1,000 until changed in the app), the same on every device. The
free plan's 10,000 monthly credits spread over a month is about 330 a day.
With a key missing, Jev unreachable or the spend ledger refusing, /check
answers Unavailable (the pipeline treats that as a refusal) and /speak answers
an error status (the tablet speaks in its own voice instead). It is not yet run as
a service; tools/run-whiskersd.sh runs it as a transient user service.
The adapters
| File | Port | How |
|---|---|---|
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 |
src/journal.rs | Journal | journal.jsonl, one mutex over count-then-append, flushed per append |
src/pictures.rs | PictureShelf | a directory; written to a temporary name and linked into place, never replacing |
src/allowance.rs | Allowance | tokens.json and speak.json behind one mutex; the rules are the ports' |
src/judge.rs | Judge | the guard's questions asked of Jev on its own thread, with a bounded wait |
src/embed.rs | Embedder | a local llama.cpp embeddings server |
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 |
src/model.rs | Thinker | forwards the vetted body to the operator's gateway |
src/secrets.rs, src/clock.rs | Secrets, Clock | the environment; the system clock |
src/native.rs | Backend | all of the above, built from the environment; it implements every Has… capability, which Service::complete requires |
Where credentials end up: in the service's environment, put there by whatever starts it (op-env-run);
nothing here writes one down.