From 85a2dd338d467361311faae8cb10e77841418abb Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 06:13:19 +0300 Subject: [PATCH] docs(lethe-server): record verify section, mark Verified --- docs/tasks/lethe-server.md | 62 +++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/docs/tasks/lethe-server.md b/docs/tasks/lethe-server.md index a132e82428935d9f4e193056a234b6b511695649..3a1954954f8e15fa9ad113dd06efd17ec1bc7f89 100644 --- a/docs/tasks/lethe-server.md +++ b/docs/tasks/lethe-server.md @@ -1,6 +1,6 @@ # lethe-server -**Status:** Execute (verify pending) +**Status:** Verified **Module:** `sourcecraft.dev/bigbes/lethe` **Branch:** master **Worktree:** none @@ -396,6 +396,66 @@ Greenfield — no compat surface. Wire format is the only forward-compat concern - Per-route per-user rate limits (would need `Authenticator` to expose a per-identity bucket). - Pluggable auth backends beyond Authelia (the OIDC verifier is generic enough that pointing at a different IdP works, but only Authelia is documented). +## Verify + +Date: 2026-04-26. Run against `master` HEAD; binary built fresh from `cmd/lethe`; e2e smoke driven through a real listener on `127.0.0.1:18888` with `tmp/lethe.yaml` (sqlite at `tmp/lethe.db`, forward-auth header trust, `alice/bob/admin` in allowlist, `admin` is admin). + +### Positive + +- `go test ./... -race -count=1` — green across all packages. +- `go build ./cmd/lethe` — binary at `tmp/lethe`. +- Server listens on `127.0.0.1:18888`; PID file written. +- `GET /healthz` → `200 ok`. +- `GET /readyz` → `200` with `{"checks":{"database":"ok"}}`. +- `GET /metrics` → `200` Prometheus exposition. +- `POST /api/v1/ingest` (alice, NDJSON, `application/x-ndjson`) → `200 {"accepted":2}`; second identical post → `200 {"accepted":2}` (idempotent upsert). +- `GET /api/v1/sessions` (alice) → `200` with one session, `ended_at` extended by `MAX(turn.timestamp)`. +- `GET /api/v1/sessions/{tool}/{host}/{sid}` (alice) → `200` with turns inline in `seq` order. +- Admin override: `GET /api/v1/sessions?owner=alice` (admin) → `200` with alice's session. + +### Negative + +All return `application/problem+json`: +- `POST /api/v1/ingest` no auth → `401`. +- `POST /api/v1/ingest` non-allowlisted user → `403`. +- `POST /api/v1/ingest` wrong `Content-Type` → `415`. +- `GET /api/v1/sessions/claude-code/phoebe/nope-id` → `404`. +- `GET /no-such-route` → `404` (chi `NotFound` handler routed through `apierror.Render`). +- `GET /api/v1/sessions?owner=alice` as non-admin → `403`. +- Per-line malformed JSON in NDJSON → `200` with `errors[]`; `accepted` reflects committed lines only (chunk aborts before commit on parse error, per spec). +- Bind validation: `bind: 0.0.0.0:18889` and `bind: 127.0.0.1:999999` both rejected at config load with `err.code=CONFIG_VALIDATE` on `loopback_bind` tag; process exits `1` before any listener opens. + +### Per-user isolation (security boundary) + +- Same `(tool, host, session_id)` ingested by alice and bob coexist as distinct rows (`owner` first in PK). +- `GET /api/v1/sessions` returns only the caller's sessions. +- `GET /api/v1/sessions/{tool}/{host}/{sid}` for another owner's session → `404` (does not leak existence). +- Wire-payload `owner` injection ignored: ingesting NDJSON whose Session/Turn JSON includes a stray `"owner"` key still attributes the row to the authenticated identity (confirmed via `internal/shared/wire/` having no `owner` field — the wire types literally cannot deserialize it). + +### Graceful shutdown + +`SIGTERM` to the running server → process exits within 2s. Final log lines: + +``` +"signal received; shutting down" +"stopping component (CallStop)" component=server.Server +"stopped component (CallStop)" component=server.Server +"destroying component" component=database.Database +"lethe stopped" +``` + +No in-flight request errors, no panic, exit code 0. + +### Invariants (re-confirmed) + +- `internal/shared/wire/` has no `owner` field anywhere (`grep -rin "owner" internal/shared/wire/` empty). +- All HTTP error paths render through `internal/pkg/apierror/apierror.go:88`, which sets `Content-Type: application/problem+json`. The chi `NotFound`/`MethodNotAllowed` handlers route through the same renderer (Phase 5 fix). +- Composite primary keys lead with `owner` (`internal/platform/database/migrations/0001_init.up.sql:39,60` — sessions: `(owner, tool, host, session_id)`; turns: `(owner, tool, host, session_id, turn_id)`). + +### Result + +All positive, negative, isolation, shutdown, and invariant checks pass. No regressions. Task is verified. + ## Conclusion ### Deviations from plan