Compare commits
4
Commits
3f8e4cf593
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc7f0cc1fd | ||
|
|
f36f0fe431 | ||
|
|
8bdf950fcf | ||
|
|
78d9b276ee |
@@ -58,9 +58,9 @@
|
||||
"Bash(git reset --hard*)",
|
||||
"Bash(git restore .*)",
|
||||
"Bash(mkfs*)",
|
||||
"Bash(rm -rf $HOME*)",
|
||||
"Bash(rm -rf /*)",
|
||||
"Bash(rm -rf ~*)",
|
||||
"Bash(rm -rf $HOME)",
|
||||
"Bash(rm -rf /)",
|
||||
"Bash(rm -rf ~)",
|
||||
"Bash(su *)",
|
||||
"Bash(sudo *)",
|
||||
"Bash(toj)",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCW5B4X073XJCWS4QTHRAVSG', 'description', NULL, 'The gateway accepts any client. `/ws/voice` and `/devices` check no credential, so anything on the LAN (or the tailnet) can open `ws://192.168.86.149:8600/ws/voice` and talk to Tatlock through the gateway — including Tatlock''s Home Assistant controls — or list the devices. Found in the workspace security sweep on 2026-09-23 by reading the gateway source: no token, auth or secret handling anywhere in `gateway/src/desklock_gateway/`.
|
||||
|
||||
Port 8600 has to stay LAN-open: it is the one address the firmware connects to (`GATEWAY_WS_URI` in `firmware/main/secrets.h`). So the fix is a credential, not a lockdown.
|
||||
|
||||
## Shape
|
||||
|
||||
- A per-device token, provisioned into firmware through `secrets.h` (already the secrets path, and already gitignored — check), sent on the WebSocket handshake (an `Authorization: Bearer` header, or a first frame if the ESP-IDF client makes headers awkward).
|
||||
- The gateway compares it in constant time against its configured set (one token per device, so one can be revoked alone) and closes the socket on a mismatch before any audio or text is processed.
|
||||
- `/devices` requires the same credential, or an admin one.
|
||||
- Tokens reach the gateway container as a Portainer stack variable, never in the tracked stack file.
|
||||
- The protocol change is recorded in `docs/architecture.md` (standing rule: device–gateway protocol changes update it).
|
||||
|
||||
## Rollout without a dark period
|
||||
|
||||
Gateway first in a mode that accepts both token and no-token and logs which each device used; then flash the devices; then make the token required. Verify: a connection without a token is refused, each flashed device still converses, and `/devices` without a credential is refused.
|
||||
|
||||
## Tests
|
||||
|
||||
A socket without a token is closed before the first frame is read; a wrong token likewise; a correct one converses; comparison is constant-time. Each mutation-checked.', NULL, '2026-09-23 11:45:03', '2026-09-23 11:45:03.865', '2026-09-23 11:45:03.865', NULL, '93027bc356d0bbc60ae0e38b9afdeb88', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
@@ -0,0 +1 @@
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCW5B4X073XJCWS4QTHRAVSG', 'T-1', '2026-09-23 11:45:03.721', '2026-09-23 11:45:03.721', NULL, '4549dc91ac440f4b47a95e9072256904', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||
@@ -0,0 +1,20 @@
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCW5B4X073XJCWS4QTHRAVSG', 'story', NULL, 'Gateway: per-device token on /ws/voice and /devices', NULL, 'backlog', 'high', NULL, NULL, NULL, '2026-09-23 11:45:03.720', '2026-09-23 11:45:03.720', NULL, '7ef01f11d6709f6e094634f5aacc6789', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06GCW5B4X073XJCWS4QTHRAVSG', 'story', NULL, 'Gateway: per-device token on /ws/voice and /devices', 'The gateway accepts any client. `/ws/voice` and `/devices` check no credential, so anything on the LAN (or the tailnet) can open `ws://192.168.86.149:8600/ws/voice` and talk to Tatlock through the gateway — including Tatlock''s Home Assistant controls — or list the devices. Found in the workspace security sweep on 2026-09-23 by reading the gateway source: no token, auth or secret handling anywhere in `gateway/src/desklock_gateway/`.
|
||||
|
||||
Port 8600 has to stay LAN-open: it is the one address the firmware connects to (`GATEWAY_WS_URI` in `firmware/main/secrets.h`). So the fix is a credential, not a lockdown.
|
||||
|
||||
## Shape
|
||||
|
||||
- A per-device token, provisioned into firmware through `secrets.h` (already the secrets path, and already gitignored — check), sent on the WebSocket handshake (an `Authorization: Bearer` header, or a first frame if the ESP-IDF client makes headers awkward).
|
||||
- The gateway compares it in constant time against its configured set (one token per device, so one can be revoked alone) and closes the socket on a mismatch before any audio or text is processed.
|
||||
- `/devices` requires the same credential, or an admin one.
|
||||
- Tokens reach the gateway container as a Portainer stack variable, never in the tracked stack file.
|
||||
- The protocol change is recorded in `docs/architecture.md` (standing rule: device–gateway protocol changes update it).
|
||||
|
||||
## Rollout without a dark period
|
||||
|
||||
Gateway first in a mode that accepts both token and no-token and logs which each device used; then flash the devices; then make the token required. Verify: a connection without a token is refused, each flashed device still converses, and `/devices` without a credential is refused.
|
||||
|
||||
## Tests
|
||||
|
||||
A socket without a token is closed before the first frame is read; a wrong token likewise; a correct one converses; comparison is constant-time. Each mutation-checked.', 'backlog', 'high', NULL, NULL, NULL, '2026-09-23 11:45:03.720', '2026-09-23 11:45:03.865', NULL, 'de24fb675828ba55b4fdd3defd406cdb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at;
|
||||
@@ -14,6 +14,9 @@ until the first tagged release.
|
||||
removed. `make test` means the same thing from any directory.
|
||||
- Firmware targets source `~/esp-idf/export.sh` themselves, so `idf.py` resolves without
|
||||
having to remember. Override the location with `IDF_EXPORT=`.
|
||||
- `make setup` now proves the gateway environment actually works instead of trusting a
|
||||
clean `pip install` exit code: it collects the test suite and checks `ruff`/`mypy`
|
||||
resolve in the venv, and fails the target if any of that is broken (T-47).
|
||||
|
||||
## [0.2.2] — 2026-07-19
|
||||
|
||||
|
||||
@@ -44,8 +44,28 @@ lint: lint-gateway ## Lint every component that has a linter
|
||||
# --- gateway ------------------------------------------------------------------
|
||||
|
||||
.PHONY: setup
|
||||
setup: ## Create the gateway venv and install dev deps (no ML models)
|
||||
setup: ## Gateway venv + dev deps + prove it works (firmware needs export.sh — see below)
|
||||
cd $(GATEWAY) && $(PYTHON) -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
||||
@# This covers the gateway half only, deliberately. The firmware half needs
|
||||
@# `source ~/esp-idf/export.sh` in every shell (see the firmware gotchas
|
||||
@# above); a Makefile recipe runs in its own subshell, so it cannot leave
|
||||
@# that sourced in the caller's shell. A `setup` that appeared to prepare
|
||||
@# firmware and silently left `idf.py` unresolved would be worse than one
|
||||
@# that says plainly it does not touch that half — hence `build-firmware`
|
||||
@# sources export.sh itself, per target, instead.
|
||||
@#
|
||||
@# Exit 0 from `pip install` is not evidence (D-24) — pip reports success
|
||||
@# even when the result is unusable (e.g. a dependency that resolved but
|
||||
@# doesn't actually import, or a stale .venv left over from a different
|
||||
@# Python). Prove the environment works instead of trusting the install
|
||||
@# step: `--collect-only` imports every test module and therefore every
|
||||
@# src module each one pulls in (T-47). It runs zero tests, so it stays
|
||||
@# cheap, and it also confirms ruff/mypy landed in .venv/bin — the venv
|
||||
@# is the only place either binary exists (see gateway gotchas above);
|
||||
@# `--version` is enough to prove each resolves and runs.
|
||||
cd $(GATEWAY) && .venv/bin/python -m pytest tests/ --collect-only -q
|
||||
cd $(GATEWAY) && .venv/bin/ruff --version >/dev/null
|
||||
cd $(GATEWAY) && .venv/bin/mypy --version >/dev/null
|
||||
|
||||
.PHONY: setup-speech
|
||||
setup-speech: ## Additionally install faster-whisper and piper
|
||||
|
||||
@@ -33,6 +33,28 @@ where = ["src"]
|
||||
line-length = 100
|
||||
src = ["src"]
|
||||
|
||||
# Selected explicitly, because the default set is not a constant.
|
||||
#
|
||||
# With no `select` here, ruff lints with whatever its installed version
|
||||
# defaults to — 413 rules under 0.16.3. `dev` pins only `ruff>=0.6`, and CI
|
||||
# installs that extra fresh on every run, so the gate's scope was a function of
|
||||
# when pip last resolved rather than of this code. Two findings appeared here
|
||||
# the first time a converged environment ran the gate, in a file nobody had
|
||||
# touched (T-47).
|
||||
#
|
||||
# That is the failure this repo keeps meeting from the other side: a check
|
||||
# whose result depends on something other than the thing it checks. Pinning the
|
||||
# ruff version would freeze the symptom; naming the rules fixes it, and makes
|
||||
# a future ruff release a decision rather than a surprise.
|
||||
#
|
||||
# ASYNC is here on purpose — this is a websocket gateway, and it is the one
|
||||
# family whose findings would be genuine bugs rather than style.
|
||||
# BLE (blind except) is deliberately absent: main.py catches bare Exception
|
||||
# when a device disappears mid-send, which is correct there and would need a
|
||||
# noqa on every occurrence to say so.
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "W", "F", "I", "UP", "B", "ASYNC", "SIM", "C4"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
|
||||
@@ -7,7 +7,7 @@ Protocol (see docs/architecture.md — keep in sync):
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
|
||||
@@ -41,7 +41,7 @@ async def _ensure_filler() -> bytes | None:
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
||||
return datetime.now(UTC).astimezone().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
@app.get("/healthz")
|
||||
|
||||
Reference in New Issue
Block a user