whiskers.git / crates / whiskersd / README.md

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.

RouteInOut
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/v2the sameJSON: the MP3 and the time of every character, in Whiskers' own shape (service README)
POST /v1/messagesan Anthropic Messages requestthe 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

FilePortHow
src/hubs.rsthe memory, household and chat hubsone JSON file each behind a mutex, replaced atomically; the merge rules are the core's
src/journal.rsJournaljournal.jsonl, one mutex over count-then-append, flushed per append
src/pictures.rsPictureShelfa directory; written to a temporary name and linked into place, never replacing
src/allowance.rsAllowancetokens.json and speak.json behind one mutex; the rules are the ports'
src/judge.rsJudgethe guard's questions asked of Jev on its own thread, with a bounded wait
src/embed.rsEmbeddera local llama.cpp embeddings server
src/speak.rsVoiceElevenLabs 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.rsThinkerforwards the vetted body to the operator's gateway
src/secrets.rs, src/clock.rsSecrets, Clockthe environment; the system clock
src/native.rsBackendall 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.