~bigbes/lethe

85a2dd338d467361311faae8cb10e77841418abb — Eugene Blikh a month ago 0ff155a
docs(lethe-server): record verify section, mark Verified
1 files changed, 61 insertions(+), 1 deletions(-)

M docs/tasks/lethe-server.md
M docs/tasks/lethe-server.md => docs/tasks/lethe-server.md +61 -1
@@ 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