- Python 74.6%
- JavaScript 18.7%
- CSS 4.5%
- Shell 1.8%
- HTML 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| examples/context_scripts | ||
| scripts | ||
| src/crew | ||
| systemd | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| crew.toml.example | ||
| deploy.sh | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
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/enrollwith{"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}/verifycaptures briefly and returnsbest_userandscore, 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 page lists users with clip counts, creates users, edits display names and Matrix IDs, and deletes users.
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).
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.
Settings shows the stored token, the health of the API, and the minimum accepted clip length.
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







