Files
FamReynaBrain/projects/tactility_voice_messages_2026.md
T
2026-09-14 22:38:47 -04:00

243 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tactility Voice Messages — Backend Implementation Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Status:** planning draft — no service, database, public route, device app, or deployment has been created.
**Goal:** Build a private, family-only voice-message backend, initially hosted on the Mac mini, that links a Tactility device to one family member at a time; persists messages until and after recipient-device delivery; and can securely serve an internet-connected device in Mexico.
**Architecture:** A dedicated API on the Mac mini owns the SQLite metadata database and a private immutable audio archive. A public HTTPS reverse-proxy route forwards only to that API. Tactility devices hold a per-device credential and their selected person in app data on the SD card; every API request is authenticated as a device, then authorized only for the device’s active person. The device app is a separate `Voice Messages` app that reuses the existing local `Voice Recorder` WAV capture/playback patterns without reusing the unrelated Hermes/Pipecat conversation gateway.
**Proposed stack:** Python 3.9-compatible FastAPI service; SQLAlchemy + Alembic; SQLite; private filesystem audio storage; launchd service on the Mac mini; HTTPS/WSS reverse proxy; C/C++ Tactility app. Containerization and a FamReynaServer migration are deferred but the service must keep configuration, database, and audio storage portable.
---
## Confirmed product decisions
| Area | Decision |
|---|---|
| Initial authoritative host | Mac mini. Design for a later migration to FamReynaServer. |
| First-run identity | Family people are prepopulated on the backend. A device displays the roster and stores its selected active person on SD card. |
| Device relationship | One person may have only one linked device at a time. |
| Reassignment | The device may switch people freely from Settings, but the server refuses assignment to a person already linked to another device. The device then clears its local inbox cache. |
| Remote reachability | Must support a stable internet URL for a device in Mexico. |
| Inbox | Private: a person can access only messages they sent or received. Newest first; sender filter applies inside this private view. |
| Audio/metadata | Audio-first v1. Each list row shows sender, recipient(s), timestamp, duration, and delivery state. No transcript or text search. |
| Delivery state | Track delivery only. A message becomes delivered only after the recipient device has durably saved its audio locally and acknowledges that fact. Do not track listening/playback. |
| Server retention | Keep audio and metadata indefinitely after delivery as a family archive. |
| Deletion | No deletion or hiding in v1. The archive is append-only. |
## Provisional defaults made after interview timeouts
These are intentionally isolated so they can be changed without changing the confirmed requirements.
| Area | Default | Rationale |
|---|---|---|
| Recipients | One or more people per message | Supports a message to both grandparents; delivery remains per recipient. |
| Maximum message length | 2 minutes | Bounds transfer and storage while remaining useful for family updates. |
| Administration | Protected server-side seed/management command | Smallest v1 surface; no public admin UI. |
| Public connection | HTTPS/WSS subdomain through the existing family reverse proxy | Provides a stable URL without publishing a raw Mac mini listener. |
| Client boundary | New `Voice Messages` app | Preserves the existing personal local `Voice Recorder` behavior. |
| Audio archival format | Retain validated source WAV | Device code already records/plays WAV. Add transcoding only after v1 delivery works. |
## Source discovery evidence
- Existing application repository: `/Users/adolforeyna/Projects/electronics/tactility/tactility_apps`.
- Existing recorder: `Apps/VoiceRecorder/manifest.properties` identifies `one.tactility.voicerecorder`, visible name `Voice Recorder`.
- `Apps/VoiceRecorder/main/Source/main.c` records WAV files under `/sdcard/memos/` and has no network/upload implementation.
- Recorder source actually sets `SAMPLE_RATE` to 44100 and writes that rate into `WavHeader`; the top comment calling it 16 kHz is stale. Two minutes of 44.1 kHz mono PCM16 is approximately 10.09 MiB. Do not rely on the stale comment.
- Existing `Apps/PipecatVoice` is a separate live conversational client with a private-LAN raw-WebSocket protocol and a hard `ws://`-only endpoint parser. It must not be repurposed for the public message service.
- An older PocketBase voice-memo prototype exists on FamReynaServer, but its documented collection rules were public and its model is device-based/`played`-based. It is reference material only, not a production starting point.
## Security model
1. Before a device can connect, an administrator creates an approved device record with an opaque device ID and a high-entropy bootstrap secret. Never use a display name, MAC address, or SD-card person selection as authentication.
2. The installer places only the public server URL, device ID, and bootstrap secret in the device’s private app data on the SD card. Nothing is compiled into firmware/source.
3. Over TLS, the device exchanges its bootstrap secret once for a rotatable device credential. The server stores only a secure verifier/hash, not a plaintext reusable secret.
4. Each request authenticates the physical device. Server authorization derives the active person from the server’s device-assignment record; a client-supplied person ID is never trusted for read/write access.
5. A `switch active person` request requires authenticated device credentials, rejects people who are assigned elsewhere, creates an append-only assignment audit event, and invalidates cached authorization state.
6. The public reverse proxy permits only the message-service host/path and forwards to a narrowly bound Mac-mini listener. No Hermes `:8642`, Pipecat, or internal API key is exposed. TLS certificate validation on the ESP32 is mandatory; the new client must support `https://`/`wss://`, unlike the old Pipecat app.
7. Logs contain IDs, status codes, and bounded error categories—not credentials, full URLs with credentials, or audio/transcript content.
## Data model
### `people`
`id`, unique `slug`, display name, active flag, created/updated timestamps.
### `devices`
`id`, unique opaque device ID, human-readable device label, credential verifier/version, active-person foreign key nullable, revoked timestamp nullable, created/updated timestamps. A unique constraint on non-null `active_person_id` enforces one active device per person.
### `device_assignment_events`
`id`, device ID, prior person nullable, new person nullable, actor type (`device` or `admin`), timestamp, reason. Append-only audit trail.
### `messages`
UUID `id`, sender person ID, immutable audio relative path, SHA-256 digest, detected audio format/rate/channels/bit depth, server-measured duration, byte count, creation timestamp. No mutable title, transcript, read status, or deletion flag in v1.
### `message_recipients`
`id`, message ID, recipient person ID, created timestamp. Unique `(message_id, recipient_person_id)`; reject a sender as recipient unless that behavior is deliberately added later.
### `deliveries`
`id`, message-recipient ID, recipient device ID captured at send time, state (`pending`, `delivered`), delivered timestamp nullable, idempotency key, acknowledged audio digest. A recipient assignment change never rewrites historical delivery records.
### `request_audit`
Bounded operational audit: request ID, device ID, endpoint class, result, timestamp. Apply retention separately to this operational log; it is not message content.
## API contract v1
All endpoints are versioned under `/v1`. Payloads use JSON except audio upload/download. Every non-bootstrap endpoint requires authenticated device credentials.
1. `POST /v1/device/bootstrap`
- Input: device ID + bootstrap secret.
- Output: rotated device credential and current device assignment, if any.
- Rate-limit and record failures without exposing whether a person exists.
2. `GET /v1/device/state`
- Returns the authenticated device, active person, roster of active family people, and current API capability/version values.
3. `PUT /v1/device/assignment`
- Input: selected person ID.
- Atomically validates that the person is active and unassigned, changes assignment, appends audit event, and returns the new assignment.
4. `POST /v1/messages`
- Multipart upload authenticated as the active sender.
- Metadata: idempotency key and recipient IDs; audio part only.
- Server streams upload to a temporary private file, caps size at 11 MiB, parses the WAV header, validates mono PCM16/allowed rate and a maximum measured duration of 120 seconds, hashes it, atomically commits it to archive storage, then writes the message/recipient/delivery rows in one database transaction.
- Never create a visible message record for partial/failed audio uploads.
5. `GET /v1/messages?folder=inbox|sent&sender_id=&cursor=&limit=`
- Newest-first stable cursor pagination.
- `inbox`: active person is a recipient; `sent`: active person is sender. Sender filter is only applied after this access constraint.
- Returns no audio bytes.
6. `GET /v1/messages/{message_id}/audio`
- Allowed only for the sender or an intended recipient’s active device.
- Supports resumable Range download if the device runtime can support it; otherwise v1 uses full-download-to-temp-file, hash verification, atomic SD rename, then acknowledgment.
7. `POST /v1/deliveries/{delivery_id}/ack`
- Allowed only to the exact recipient device captured in that delivery row.
- Input includes message SHA-256. It is idempotent and transitions only `pending -> delivered` after matching the server digest.
8. `GET /healthz` and `GET /readyz`
- Local/proxy monitoring only; no family data.
## Client behavior that the backend depends on
- On first launch, bootstrap using device configuration, fetch roster, and present the name selection screen.
- Persist the selected person and credential through the platform user-data path, not a compiled constant. On assignment change, erase only the Voice Messages app’s cached inbox/audio—not the separate Voice Recorder’s local memos.
- On send: record to a temporary SD WAV, show recipients, upload with a generated idempotency key, and retain the local temporary file until the server returns a committed message ID. Retry safely with the same idempotency key after connectivity loss.
- On receive: sync metadata when the app opens/refreshes; for a pending delivery, download audio to a temporary file, verify SHA-256 and WAV validity, atomically rename to the app cache, then acknowledge delivery. A failed download/SD write must not be acknowledged.
- The client is foreground-polling in v1. No background listener, wake-word behavior, tracking, transcription, or read receipts are included.
## Proposed repository and service layout
Create a new repository only after approval:
```text
/Users/adolforeyna/Projects/tactility-voice-messages/
pyproject.toml
src/tactility_voice_messages/
app.py
config.py
db.py
models.py
auth.py
wav_validation.py
archive.py
routes/
bootstrap.py
devices.py
messages.py
deliveries.py
services/
assignments.py
message_uploads.py
inbox.py
migrations/
tests/
test_bootstrap.py
test_assignment.py
test_authorization.py
test_message_upload.py
test_inbox.py
test_delivery_ack.py
test_wav_validation.py
test_archive_atomicity.py
scripts/
seed_family.py
backup_verify.py
run_dev.sh
deploy/
com.reyna.tactility-voice-messages.plist
reverse-proxy-route.json
restore-reverse-proxy-route.sh
docs/
protocol-v1.md
operations.md
```
Runtime data is deliberately outside the repository, e.g. `~/Library/Application Support/TactilityVoiceMessages/` with a `0700` parent:
```text
TactilityVoiceMessages/
data.sqlite3
media/aa/bb/<message-uuid>.wav
incoming/ # same-filesystem temporary uploads
backups/
logs/
```
## Delivery and migration design
- Launch the API as a Mac-mini user LaunchAgent, bound only to the private LAN interface required by the reverse proxy—not `0.0.0.0`.
- The reverse proxy terminates TLS and has a durable route definition plus restore script. It forwards only the exact host to the Mac mini port.
- Backups are consistent SQLite backups plus content-addressed/media manifest verification. A backup job must verify database integrity, every archive file’s hash/size, and a restoration drill before being called ready.
- The future FamReynaServer migration copies the stopped/consistent SQLite database plus media archive, updates only endpoint infrastructure, runs the same migration/version checks, and verifies authorized send/download/ack flows from a device. Do not migrate by copying an active SQLite/WAL database blindly.
## TDD implementation sequence
### Task 1: Scaffold configuration and health checks
Create the repository, 3.9-compatible dependency lock, validated configuration, `healthz`, `readyz`, and focused tests. Verify startup with an ephemeral SQLite database and a private temp directory.
### Task 2: Implement schema and migrations
Write failing tests for unique active-person assignment, immutable message rows, recipient uniqueness, and delivery state. Create migrations and prove upgrade from empty database.
### Task 3: Implement device bootstrap and request authentication
Test bootstrap rate limits, secret hashing, credential rotation, revoked-device rejection, and zero credential leakage in errors/logs. Implement the minimal device auth middleware.
### Task 4: Implement assignment and roster authorization
Test unassigned selection, rejection of second-device claim, reassignment audit events, and roster access restricted to authenticated approved devices.
### Task 5: Implement WAV validation and private atomic archive
Test malformed RIFF headers, non-PCM, wrong channel/bit-depth/rate, oversized files, over-two-minute content, hash mismatch, cleanup after DB failure, and archive path traversal rejection.
### Task 6: Implement message creation and fan-out deliveries
Test sender-only creation, recipient deduplication, atomic creation of one delivery per recipient, no visible partial upload, and idempotent retry with the same key.
### Task 7: Implement private inbox/sent listing and sender filter
Test cursor stability, newest-first ordering, no cross-person leakage, sent visibility, incoming visibility, and filter behavior.
### Task 8: Implement authorized download and delivery acknowledgment
Test that only sender/intended recipient can download, reassigned devices cannot claim historical delivery, acknowledgment digest matching, and idempotent `pending -> delivered` transition.
### Task 9: Implement seed/admin command, backup verification, and operations docs
Test seed-file validation, duplicate IDs/slugs rejection, safe credential handling, backup manifest verification, and a test restoration into a new data directory.
### Task 10: Deploy only after separate approval
Prepare but do not activate the LaunchAgent, reverse-proxy route, DNS, or public endpoint without user approval. Before go-live, test with a nonproduction device against HTTPS, prove certificate validation, send a real audio message, verify an offline recipient remains pending, then verify download-to-SD precedes one successful delivery acknowledgment.
## Acceptance tests
1. An approved blank device can select an unassigned family person; a second device cannot select that same person.
2. Reassigning a device makes its old cached inbox inaccessible and returns only the new person’s permitted messages.
3. A sender can send one validated WAV to multiple recipients; every recipient has an independent pending delivery.
4. A recipient offline for days receives the pending message when it next syncs; server retention continues afterward.
5. Sender and recipient cannot access another person’s messages through altered IDs, query filters, direct audio URLs, or replayed credentials.
6. A network interruption during upload creates no message; a retry with the same idempotency key creates exactly one message.
7. A network interruption or full SD card during download does not set delivered. A completed verified local file sets delivered exactly once.
8. The Mexico-device path uses valid TLS and reaches only the dedicated service; invalid/revoked credentials cannot enumerate people, messages, or audio.
9. A verified backup can restore the full metadata/audio archive and preserve SHA-256 validation.
## Explicit non-goals for v1
- Hermes/Pipecat integration, live conversation streaming, speech-to-text, voice recognition, push notifications, background device listener, read/listen receipts, delete/edit/forward, a browser inbox, public account creation, free device switching to already assigned people, or automatic audio transcoding.