Crew user system: users, Matrix identities, and enrolled voiceprints for starfleet speaker ID.
  • Python 74.6%
  • JavaScript 18.7%
  • CSS 4.5%
  • Shell 1.8%
  • HTML 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
troed 29e4fc124a
All checks were successful
CI / Sanity check (ubuntu-latest) (push) Successful in 30s
CI / Sanity check (ubuntu-latest) (pull_request) Successful in 30s
Merge branch 'main' into devel
2026-10-17 19:44:16 +02:00
.forgejo/workflows chore: scaffold crew repository and package 2026-09-03 10:30:52 +02:00
docs docs: regenerate README screenshots with firmware UI 2026-09-11 13:53:25 +02:00
examples/context_scripts docs: per-user prompts and context setup 2026-09-07 16:25:08 +02:00
scripts feat: crew requires TLS material and verifies the fleet CA on outbound calls (#9) 2026-10-17 18:16:28 +02:00
src/crew feat: crew requires TLS material and verifies the fleet CA on outbound calls (#9) 2026-10-17 18:16:28 +02:00
systemd feat: add crew deployment, example config, and operations docs 2026-09-03 10:49:08 +02:00
tests feat: crew requires TLS material and verifies the fleet CA on outbound calls (#9) 2026-10-17 18:16:28 +02:00
.gitignore chore: scaffold crew repository and package 2026-09-03 10:30:52 +02:00
AGENTS.md docs: firmware endpoints, config, and OTA control-plane overview 2026-09-11 12:32:20 +02:00
crew.toml.example feat: crew requires TLS material and verifies the fleet CA on outbound calls (#9) 2026-10-17 18:16:28 +02:00
deploy.sh feat: add crew deployment, example config, and operations docs 2026-09-03 10:49:08 +02:00
LICENSE chore: scaffold crew repository and package 2026-09-03 10:30:52 +02:00
pyproject.toml feat(firmware): version parsing, manifest validation, and Ed25519 signature check 2026-09-11 07:40:06 +02:00
README.md feat: crew requires TLS material and verifies the fleet CA on outbound calls (#9) 2026-10-17 18:16:28 +02:00
uv.lock feat(firmware): version parsing, manifest validation, and Ed25519 signature check 2026-09-11 07:40:06 +02:00

crew

User system for starfleet speaker ID: users, Matrix identities, enrolled voiceprint embeddings, and the connected-device registry (guided enrollment and verification through computer's capture endpoint). Sister service to computer (voice pipeline).

Design: docs/specs/2026-09-02-crew-user-system-design.md.

computer owns recognition. crew owns records. computer polls crew's GET /registry; enrollment clips are proxied to computer's POST /internal/users/enroll, and only the returned embedding is stored.

Setup

uv sync --group dev

Configure

Copy the example and edit the live file. Never commit the live file:

mkdir -p ~/.config/crew
cp crew.toml.example ~/.config/crew/crew.toml

Important settings:

Setting Meaning
api.bind, api.port, api.token crew HTTPS listener and bearer token
api.tls_cert, api.tls_key leaf certificate + key for HTTPS serving; both required (crew refuses to start without them)
store.db_path SQLite database
computer.enroll_url, computer.token existing computer enroll endpoint and token
computer.devices_url computer's connected-device list (admin UI device view)
computer.capture_url computer's guided-capture endpoint ({device_id} placeholder required)
clips.min_clip_s minimum accepted enrollment-clip duration
ingest.token token roster must send when POSTing context to /internal/context (empty trusts the LAN)
roster.url, roster.token roster API crew provisions per-user context jobs against
roster.scripts_dir allowlisted scripts dir on the roster host; job script_path values are built from it
firmware.dir directory for published releases (<version>/{image.bin,manifest.json,manifest.sig} + latest.json)
firmware.public_base_url device-reachable base URL for firmware downloads (default: request's own base URL)
firmware.signing_public_key base64 Ed25519 public key releases must be signed with (required for uploads)
trust.ca_bundle fleet CA bundle for outbound TLS (see TLS); overridden by --ca-bundle, then $STARFLEET_CA_BUNDLE

Run

uv run crew serve --config ~/.config/crew/crew.toml

TLS

Both surfaces -- the JSON API and the admin UI it serves -- are HTTPS only, and TLS material is required: [api] tls_cert and [api] tls_key must both be set in crew.toml, or the process refuses to start naming the missing key. Point them at PEM files readable by the service on the server host; the certificate must cover the host name browsers and clients use.

Outbound calls -- the CLI, and crew's own calls to computer and roster -- verify the fleet's private CA. The bundle is resolved as an explicit --ca-bundle flag, then $STARFLEET_CA_BUNDLE, then [trust] ca_bundle in crew.toml; when none is set crew fails loudly rather than falling back to the system trust store. The CLI's default URL is https://127.0.0.1:4030, so even a bare crew list needs a CA bundle, and a remote service needs its own host name:

crew list --url https://<crew-host>:4030 --ca-bundle <ca.pem>

API

Endpoint Purpose
GET /health status, version, user count, clip count
GET /users user metadata and clip counts
POST /users create {id?, display_name, matrix_user_id?}
GET /users/{id} user detail and non-vector clip metadata
PATCH /users/{id} update display name or Matrix ID
DELETE /users/{id} delete a user and cascade its clips
POST /users/{id}/clips validate multipart WAV and store its embedding
DELETE /users/{id}/clips/{clip_id} delete one clip
POST /users/{id}/clips/enroll guided capture on a connected device; store the returned vector
GET /devices stored devices merged with computer's live connection list; rows carry firmware_version, latest_firmware, and the derived update state
PUT /devices/{id} patch name / device_type / owner_user_id (upserts)
PUT /firmware/{version} publish a release: multipart image + manifest + signature; validates sha256 and the Ed25519 signature; releases are immutable (409 on re-upload)
GET /firmware published releases, newest first, with a latest flag
GET /firmware/latest.json {"version": "<newest>"} (open on-LAN; devices act on schedule events, not on this)
GET /firmware/{version}/manifest.json / manifest.sig / image.bin device download surface (open on-LAN; TLS + signature are the authenticity story)
PUT /devices/{id}/firmware-update schedule an update {"version": "<v>|"latest"} (replaces any pending one)
DELETE /devices/{id}/firmware-update cancel the pending update
GET /devices/{id}/firmware-pending {"version", "url"} for the pending update, consumed by computer's OTA relay (404 when none)
POST /devices/{id}/verify guided capture; identify who is speaking right now
GET /registry computer-compatible user/vector export
GET /public-config public min_clip_s for the admin UI (no token)
GET /device-owners device_id → owner_user_id map for computer prompt resolution
GET /users/{id}/session-prompt composed session prompt plus the context blocks behind it
GET/PUT/DELETE /users/{id}/prompt-sections/{key} per-user prompt sections (about, instructions)
GET /context-options catalog of available context options
GET/PUT/DELETE /users/{id}/context-options/{option_id} per-user context options; PUT provisions the roster job
GET /users/{id}/context-options stored context options for a user
POST /internal/context roster context deliveries (ingest token, not the API token)

Enrollment uploads must be 16 kHz mono 16-bit WAV files at least min_clip_s seconds long. Recordings are never committed to this repository.

Devices

computer reports the clients currently connected to its realtime endpoint; crew merges that live list with its own stored rows (device_type, name, owner_user_id) at GET /devices. device_type is one of esp32, pc, phone; PUT /devices/{id} upserts a row, so a connected-but-unstored device can be labeled on first sight.

Guided enrollment and verification run through computer's capture endpoint (capture_url, {device_id} placeholder):

  • POST /users/{id}/clips/enroll with {"device_id": "..."} makes the connected device record, computer extracts the vector, and crew stores it as that user's clip — no file upload needed.
  • POST /devices/{id}/verify captures briefly and returns best_user and score, i.e. who is speaking right now.

Both pass through computer's 404 (no such user/device), 408 (no speech), 409 (already capturing), and 503 (speaker model down) verbatim.

Firmware (OTA control plane)

Crew stores immutable, Ed25519-signed firmware releases and per-device update schedules. The upload endpoint (PUT /firmware/{version}) validates the image's sha256 against the manifest and the manifest's signature against firmware.signing_public_key before accepting; releases are immutable. Devices are told about a scheduled update by computer (which relays GET /devices/{id}/firmware-pending as a server.ota.schedule event) and download the release themselves from the open-on-LAN file endpoints. Update status is derived: a pending schedule flips to applied once the device reports the scheduled version. Design: docs/specs/2026-09-10-ota-design.md.

Admin UI

Open https://<crew-host>:4030/admin after crew serve starts. The admin UI is served over the same TLS-only listener (see TLS), so the browser must trust the fleet CA first. Enter the crew API token once; the browser keeps it in localStorage and sends it as the bearer token with X-Client-Id: admin-ui.

The token gate

The page lists users with clip counts, creates users, edits display names and Matrix IDs, and deletes users.

The user list

Each user detail view has four tabs: Identity edits the display name and Matrix ID, Prompts edits the prompt sections (About, Instructions), Context toggles the per-user context options (see below), and Voice manages the enrollment clips — uploading audio files (normalized to 16 kHz mono WAV in the browser before upload) and recording from the microphone (encoded to 16 kHz mono WAV in the browser).

User identity tab Prompt sections tab Context options tab Voice clips tab

A devices section shows every stored or connected device with a live connection badge, lets you set type, name, and owner, and offers per-device enroll into user and verify speaker actions driving the guided-capture flow above. Enrollment clips store embeddings only and cannot be played back. Enrollment requires computer running; upload errors report the enroll failure verbatim.

The devices section

Settings shows the stored token, the health of the API, and the minimum accepted clip length.

The settings section

These screenshots use fictitious data (see scripts/makedoc_seed.py) and are generated with a headless Chromium; run bash scripts/makedoc.sh to regenerate them.

Per-user prompts and context

GET /users/{id}/session-prompt returns the session prompt computer injects for a device owner: an opening line, the user's non-blank prompt sections in about, instructions order (the About the user: header appears only when the about section has content), and one block per enabled context option with its latest fetched content and time. It is empty until the user has sections or enabled options with delivered context.

Two context options ship in v1 (see GET /context-options):

Option Param Schedule Context
weather location daily 06:05 weather report text
briefing calendar_entity daily 06:15 today's calendar summaries

How an option flows: enabling it in the admin UI has crew provision a scheduled job on the roster host (crew-{user_id}-{option_id}) that runs the option's script with the user's params as environment variables → the job runs on schedule and its stdout is delivered back to crew's POST /internal/context (ingest token from [ingest]) → crew stores the latest content per user and key → computer injects it via session-prompt. Disabling an option pauses its roster job so it stops firing and delivering, and enabling it resumes the job; removing the option deletes the job. Stale stored content is still filtered out at render until fresh data arrives.

Config: [ingest] token protects the delivery endpoint; [roster] url, [roster] token, and [roster] scripts_dir point at the roster API and its allowlisted scripts directory (see the Configure table above).

Example scripts live in examples/context_scripts/: crew_weather.sh (fetches wttr.in) and crew_briefing.sh (Home Assistant calendar REST via a sourced env file). Copy them to the roster host's scripts dir, make them executable, and adjust the data sources to your own.

CLI

crew list --url https://<crew-host>:4030 --ca-bundle <ca.pem> --token "$CREW_TOKEN"
crew create-user --name "Ada Lovelace" --matrix-user-id "@ada:example.org"
crew delete-user ada-lovelace
crew enroll ./ada-1.wav --user ada-lovelace
crew doctor

The client commands (list, create-user, delete-user, enroll) use the default https://127.0.0.1:4030 and resolve their CA bundle the same way (see TLS); crew never falls back to the system trust store. (crew doctor has no --url/--ca-bundle and never touches the network.)

Deploy

The deploy environment lives in the local-only ~/.config/crew/deploy.env (never commit): it exports SERVER_HOST, SERVER_USER, SERVER_REPO_DIR:

source ~/.config/crew/deploy.env
./deploy.sh

Development

uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
uv run mypy src/
uv run pytest tests/ -x -q