A .air.toml => .air.toml +22 -0
@@ 0,0 1,22 @@
+root = "."
+tmp_dir = "tmp"
+
+[build]
+ cmd = "go build -o ./tmp/lethe ./cmd/lethe"
+ bin = "./tmp/lethe"
+ args_bin = ["-config", "config.yaml"]
+ include_ext = ["go", "yaml", "sql"]
+ exclude_dir = ["tmp", "vendor", "data", "docs"]
+ delay = 1000
+
+[log]
+ time = false
+
+[color]
+ main = "yellow"
+ watcher = "cyan"
+ build = "green"
+ runner = "magenta"
+
+[misc]
+ clean_on_exit = true
A .gitignore => .gitignore +16 -0
@@ 0,0 1,16 @@
+# Built binaries
+/lethe
+/tmp/
+
+# SQLite database files (real, not example)
+*.db
+*.db-shm
+*.db-wal
+data/
+
+# Local config (never commit secrets)
+config.yaml
+
+# IDE state
+.idea/
+.vscode/
A .golangci.yml => .golangci.yml +28 -0
@@ 0,0 1,28 @@
+version: "2"
+
+run:
+ timeout: 5m
+
+linters:
+ default: none
+ enable:
+ - errcheck
+ - govet
+ - staticcheck
+ - revive
+ - gosec
+ - unused
+ settings:
+ revive:
+ rules:
+ - name: exported
+ disabled: true
+
+formatters:
+ enable:
+ - gofmt
+ - goimports
+
+issues:
+ max-issues-per-linter: 0
+ max-same-issues: 0
A Dockerfile => Dockerfile +22 -0
@@ 0,0 1,22 @@
+# syntax=docker/dockerfile:1.7
+
+FROM golang:1.25-alpine AS builder
+
+WORKDIR /src
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/lethe ./cmd/lethe
+
+FROM gcr.io/distroless/static-debian12:nonroot
+
+WORKDIR /app
+COPY --from=builder /out/lethe /app/lethe
+
+# Server binds 127.0.0.1 inside the container; expose only on the compose
+# network. The reverse proxy on the host is the public surface.
+EXPOSE 8080
+
+ENTRYPOINT ["/app/lethe"]
+CMD ["-config", "/config.yaml"]
A Justfile => Justfile +36 -0
@@ 0,0 1,36 @@
+binary := "lethe"
+version := `git describe --tags 2>/dev/null || echo dev`
+
+default:
+ @just --list
+
+build:
+ CGO_ENABLED=0 go build -ldflags "-X main.version={{version}}" -o {{binary}} ./cmd/lethe
+
+run:
+ go run ./cmd/lethe -config config.yaml
+
+air:
+ air
+
+test:
+ go test -race ./...
+
+lint:
+ golangci-lint run --fix ./...
+
+fmt:
+ gofmt -w .
+ goimports -w .
+
+tidy:
+ go mod tidy
+
+docker-build:
+ docker build -t {{binary}}:{{version}} .
+
+docker-up:
+ docker compose up -d
+
+docker-down:
+ docker compose down
A README.md => README.md +204 -0
@@ 0,0 1,204 @@
+# lethe
+
+Personal AI assistant log aggregator. `lethe` is a small, single-binary Go
+service that ingests turn-level NDJSON from AI assistant collectors (Claude
+Code, opencode, etc.), stores it in SQLite, and exposes a JSON API for
+listing and reading sessions.
+
+Search and the collector binary live in sibling repos / tasks
+(`lethe-collector-claude-code`, `lethe-search-and-opencode`); this repo
+is just the server.
+
+## Purpose
+
+- A single private store for all my AI assistant transcripts across hosts
+ and tools.
+- Per-user data isolation so each authenticated user only sees their own
+ sessions; admins can override with `?owner=`.
+- Simple deploy: one Go binary, one SQLite file, all schema changes via
+ embedded migrations, `127.0.0.1` bind behind a reverse proxy.
+
+## Quickstart
+
+```bash
+cp config.example.yaml config.yaml
+just run
+# or, for hot reload during development:
+just air
+```
+
+The server reads `config.yaml` by default. Pass `-config <path>` to override.
+
+## Trust model
+
+`lethe` always binds `127.0.0.1`. It is not safe to expose it to the
+network directly. All authentication assumes a trusted reverse proxy on
+the same host (e.g. Caddy or Traefik) that terminates TLS and either
+injects auth headers or relays an OIDC bearer.
+
+The implicit assumption is that nothing else on the host binds
+`127.0.0.1` and forges auth headers. That is the whole trust boundary
+for path (a) below.
+
+There are two independent auth paths, each gated by the
+`auth.allowed_users` allowlist as a defense-in-depth check. If both are
+enabled and a request carries both an `Authorization` header and the
+configured forward-auth header, the bearer is validated first; the
+header is only consulted if bearer validation fails.
+
+### (a) Forward-auth: reverse proxy + Authelia + `Remote-User`
+
+The reverse proxy runs Authelia forward-auth on the lethe vhost. On
+success Authelia signs the user identity into headers (`Remote-User`,
+`Remote-Email`, `Remote-Groups`) which the proxy forwards to lethe.
+`lethe` reads the configured `auth.forward_auth.user_header` (default
+`Remote-User`), checks the allowlist, and 403s on miss.
+
+Sample Caddy snippet (`/etc/caddy/Caddyfile`):
+
+```caddy
+lethe.example.com {
+ forward_auth http://127.0.0.1:9091 {
+ uri /api/verify?rd=https://auth.example.com
+ copy_headers Remote-User Remote-Email Remote-Groups Remote-Name
+ }
+
+ reverse_proxy 127.0.0.1:8080
+}
+```
+
+`lethe` config:
+
+```yaml
+auth:
+ allowed_users: ["bigbes"]
+ forward_auth:
+ enabled: true
+ user_header: "Remote-User"
+```
+
+Sample request (the proxy injects the headers on your behalf — this curl
+shows what reaches lethe, not what you'd send from a browser):
+
+```bash
+curl -H 'Remote-User: bigbes' http://127.0.0.1:8080/api/v1/sessions
+```
+
+### (b) OIDC bearer: Authelia issues, lethe validates
+
+`lethe` is registered as an OIDC client of Authelia. `lethe` only
+validates tokens; it never issues them. The bearer must already be
+obtained out-of-band by whatever client wants to call the API (the
+collector, scripts, etc.).
+
+Sample Authelia client entry
+(`identity_providers.oidc.clients` in `configuration.yml`):
+
+```yaml
+identity_providers:
+ oidc:
+ clients:
+ - client_id: lethe
+ client_name: Lethe
+ client_secret: '<argon2 hash>'
+ public: false
+ authorization_policy: two_factor
+ redirect_uris:
+ - https://lethe.example.com/oauth2/callback
+ scopes:
+ - openid
+ - profile
+ - email
+ userinfo_signing_algorithm: none
+ token_endpoint_auth_method: client_secret_basic
+```
+
+`lethe` config (note: only the issuer URL, audience, and the username
+claim — no client secret on the lethe side, since lethe never starts a
+flow):
+
+```yaml
+auth:
+ allowed_users: ["bigbes"]
+ oidc:
+ enabled: true
+ issuer: "https://auth.example.com"
+ audience: "lethe"
+ username_claim: "preferred_username"
+```
+
+Sample request:
+
+```bash
+curl -H "Authorization: Bearer $TOKEN" \
+ https://lethe.example.com/api/v1/sessions
+```
+
+### Public endpoints
+
+`/healthz`, `/readyz`, and `/metrics` are mounted outside the auth
+middleware so the host can scrape them locally without going through
+the proxy. Everything under `/api/v1/*` is authed.
+
+## API surface
+
+| Method | Path | Auth | Notes |
+|--------|-----------------------------------------------------|------|-------|
+| POST | `/api/v1/ingest` | yes | NDJSON body of `TurnEvent` records. Idempotent at the turn level per owner. |
+| GET | `/api/v1/sessions` | yes | Paginated. Filters: `tool`, `host`, `since`, `until`. Admins may pass `?owner=<user>` or `?owner=*`. |
+| GET | `/api/v1/sessions/{tool}/{host}/{session_id}` | yes | Full session with turns inline. Admins may pass `?owner=<user>`. |
+| GET | `/healthz` | no | Liveness. |
+| GET | `/readyz` | no | Readiness — DB ping etc. |
+| GET | `/metrics` | no | Prometheus. |
+
+The `owner` field is server-derived; the wire format has no `owner`. A
+non-admin caller passing `?owner=` gets 403.
+
+Errors are RFC 7807 `application/problem+json`.
+
+## Production deployment
+
+`lethe` binds `127.0.0.1` only. Run it on the same host as a reverse
+proxy that terminates TLS and injects auth headers (or relays an OIDC
+bearer). Do not expose the listener to the network directly.
+
+When run via the bundled `docker-compose.yml`, the service does not
+publish a port to the host — it only `expose`s `8080` on the compose
+network. The reverse proxy reaches lethe through that network.
+
+## Backup
+
+The SQLite DB is the only state. Take consistent online backups with
+`sqlite3 .backup`. Sample cron with 14-day retention:
+
+```cron
+# /etc/cron.d/lethe-backup
+# Daily online backup of the lethe SQLite database, 14-day retention.
+PATH=/usr/bin:/bin
+
+15 03 * * * lethe \
+ ts=$(date +\%Y\%m\%d-\%H\%M\%S); \
+ sqlite3 /var/lib/lethe/lethe.db ".backup '/var/backups/lethe/lethe-$ts.db'" && \
+ find /var/backups/lethe -name 'lethe-*.db' -mtime +14 -delete
+```
+
+`/var/backups/lethe` should live on a different volume from the live DB.
+
+## Layout
+
+```
+.
+├── cmd/lethe/ # main, thin shell
+├── internal/
+│ └── shared/wire/ # locked NDJSON contract shared with the collector
+├── config.example.yaml
+├── Justfile
+├── .air.toml
+├── Dockerfile
+├── docker-compose.yml
+└── .golangci.yml
+```
+
+The rest of the layout (`internal/config`, `internal/server`,
+`internal/domain/...`, `internal/platform/...`, `migrations/`) lands in
+later phases.
A cmd/lethe/main.go => cmd/lethe/main.go +18 -0
@@ 0,0 1,18 @@
+// Command lethe is the lethe server binary. Phase 1 ships a stub that prints
+// the version and selected config path; real wiring lands in Phase 9.
+package main
+
+import (
+ "flag"
+ "fmt"
+)
+
+const version = "0.1.0-dev"
+
+func main() {
+ configPath := flag.String("config", "config.yaml", "path to YAML config file")
+ flag.Parse()
+
+ fmt.Printf("lethe %s\n", version)
+ fmt.Printf("config: %s\n", *configPath)
+}
A config.example.yaml => config.example.yaml +28 -0
@@ 0,0 1,28 @@
+server:
+ bind: "127.0.0.1:8080"
+ shutdown_grace: 10s
+
+database:
+ path: "./lethe.db"
+ busy_timeout: 5s
+
+auth:
+ allowed_users: ["bigbes"]
+ admins: []
+ forward_auth:
+ enabled: true
+ user_header: "Remote-User"
+ oidc:
+ enabled: false
+ issuer: "https://auth.example.com"
+ audience: "lethe"
+ username_claim: "preferred_username"
+
+logging:
+ level: "info"
+ format: "tint" # tint | json
+
+ingest:
+ max_body_bytes: 16777216 # 16 MiB
+ max_turn_content_bytes: 4194304 # 4 MiB
+ chunk_size: 500
A docker-compose.yml => docker-compose.yml +12 -0
@@ 0,0 1,12 @@
+services:
+ lethe:
+ build: .
+ # Do NOT publish to the host: the trust model assumes a reverse proxy on
+ # the host (Caddy/Traefik) that handles TLS + Authelia forward-auth and
+ # forwards to lethe via this compose network.
+ expose:
+ - "8080"
+ volumes:
+ - ./data:/data
+ - ./config.yaml:/config.yaml:ro
+ restart: unless-stopped
M docs/tasks/lethe-server.md => docs/tasks/lethe-server.md +14 -0
@@ 395,3 395,17 @@ Greenfield — no compat surface. Wire format is the only forward-compat concern
- Cursor-based pagination / total-count on `/api/v1/sessions` — offset+limit is enough for the expected volume.
- 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).
+
+## Conclusion
+
+### Deviations from plan
+
+- **Phase 1**: `go.mod` directive is `go 1.25.0`, not `go 1.22+`. `go get` of golang-migrate/v4.19, viper 1.21, and prometheus 1.23 forced the bump (each requires ≥1.24). Plan said "Go 1.22+" so this satisfies the floor; flagging because the explicit number changed and the Dockerfile builder image was bumped to `golang:1.25-alpine` to match (consistency fix folded into the same Phase 1 commit).
+- **Phase 1**: added `internal/deps/deps.go` with blank imports of every direct dep so `go mod tidy` keeps them in `go.mod` until real packages start importing them. Transitional file; expected to shrink each phase and disappear by end of Phase 9. Without it, `go mod tidy` strips the dep stub the plan called for.
+- **Phase 1**: `.golangci.yml` uses the v2 schema (golangci-lint 2.11.4 rejects v1). Same lint set as the plan listed (errcheck, govet, staticcheck, revive, gosec, unused, gofmt, goimports).
+
+### Notes carried forward
+
+- Phase 3 should add `migrate-up`, `migrate-down`, `migrate-create` to the Justfile alongside the migration runner so the targets aren't dead.
+- Each phase from 2 onward must remove the dep it adopts from `internal/deps/deps.go`; Phase 9 deletes the file.
+- README's Caddy/Authelia snippets use `auth.example.com` placeholders; replace with phoebe-specific values when the production deploy lands (out of scope for this task).
A go.mod => go.mod +55 -0
@@ 0,0 1,55 @@
+module sourcecraft.dev/bigbes/lethe
+
+go 1.25.0
+
+require (
+ github.com/coreos/go-oidc/v3 v3.18.0
+ github.com/go-chi/chi/v5 v5.2.5
+ github.com/go-playground/validator/v10 v10.30.2
+ github.com/golang-migrate/migrate/v4 v4.19.1
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/prometheus/client_golang v1.23.2
+ github.com/spf13/viper v1.21.0
+ go.bigb.es/auxilia v0.2.0
+ modernc.org/sqlite v1.49.1
+)
+
+require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dominikbraun/graph v0.23.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.13 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.4 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/ncruces/go-strftime v1.0.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/prometheus/client_model v0.6.2 // indirect
+ github.com/prometheus/common v0.66.1 // indirect
+ github.com/prometheus/procfs v0.16.1 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/sagikazarmark/locafero v0.11.0 // indirect
+ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
+ github.com/soverenio/vanilla v0.0.0-20240904095435-e5a9e99f83ca // indirect
+ github.com/spf13/afero v1.15.0 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.yaml.in/yaml/v2 v2.4.2 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/crypto v0.49.0 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
+ golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/text v0.35.0 // indirect
+ google.golang.org/protobuf v1.36.8 // indirect
+ modernc.org/libc v1.72.0 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+)
A go.sum => go.sum +156 -0
@@ 0,0 1,156 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
+github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo=
+github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
+github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
+github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
+github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
+github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
+github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
+github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
+github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
+github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
+github.com/soverenio/vanilla v0.0.0-20240904095435-e5a9e99f83ca h1:rB4eZDa14pktnbTiEn9GpTt3b78e1l+kK35/qhoc/PI=
+github.com/soverenio/vanilla v0.0.0-20240904095435-e5a9e99f83ca/go.mod h1:EiaNftWfwv+c5RT1C2PXI9ZJjBJwtOymKQl56sC+ORE=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.bigb.es/auxilia v0.2.0 h1:qAkyDiNCihdUMqxiHOavAuqgtGNZ8ZkIwCFV6UcaM80=
+go.bigb.es/auxilia v0.2.0/go.mod h1:7TFf9RZZo0YbXQ3mY9GnirwxgVTa9HXaJBHr+1rU0LY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
+go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
+golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
+golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
+google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
+modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
+modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
+modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
+modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
+modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
+modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
+modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
+modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
A internal/deps/deps.go => internal/deps/deps.go +23 -0
@@ 0,0 1,23 @@
+// Package deps records the locked set of direct dependencies for the lethe
+// server during early scaffolding (Phase 1). Real packages adopt these as
+// they come online (config — viper/validator; server — chi/prometheus;
+// database — sqlx/modernc.org/sqlite/golang-migrate; auth — go-oidc;
+// platform — auxilia steward/culpa/scribe).
+//
+// Once every dep below has at least one real importer, this file is
+// expected to disappear in the same commit that completes the migration.
+package deps
+
+import (
+ _ "github.com/coreos/go-oidc/v3/oidc"
+ _ "github.com/go-chi/chi/v5"
+ _ "github.com/go-playground/validator/v10"
+ _ "github.com/golang-migrate/migrate/v4"
+ _ "github.com/jmoiron/sqlx"
+ _ "github.com/prometheus/client_golang/prometheus"
+ _ "github.com/spf13/viper"
+ _ "go.bigb.es/auxilia/culpa"
+ _ "go.bigb.es/auxilia/scribe"
+ _ "go.bigb.es/auxilia/steward"
+ _ "modernc.org/sqlite"
+)
A internal/shared/wire/wire.go => internal/shared/wire/wire.go +36 -0
@@ 0,0 1,36 @@
+// Package wire defines the locked NDJSON ingest contract shared with the
+// lethe-collector. Changes here ripple into the collector — be deliberate.
+package wire
+
+import "encoding/json"
+
+// TurnEvent is the on-the-wire representation of a single turn in a session.
+// It is the only message type accepted by POST /api/v1/ingest. Sessions are
+// upserted from SessionMeta carried on each turn; there is no separate
+// session event on the wire.
+type TurnEvent struct {
+ Tool string `json:"tool"`
+ Host string `json:"host"`
+ SessionID string `json:"session_id"`
+ TurnID string `json:"turn_id"`
+ Seq int64 `json:"seq"`
+ Role string `json:"role"` // user | assistant | tool | system
+ Timestamp int64 `json:"timestamp"` // unix epoch seconds
+ Content string `json:"content"`
+ Model *string `json:"model,omitempty"`
+ TokensIn *int64 `json:"tokens_in,omitempty"`
+ TokensOut *int64 `json:"tokens_out,omitempty"`
+ CostUSD *float64 `json:"cost_usd,omitempty"`
+ ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
+ SessionMeta SessionMeta `json:"session_meta"`
+ Metadata json.RawMessage `json:"metadata,omitempty"`
+}
+
+// SessionMeta carries the per-session attributes that the server uses to
+// upsert the parent session row on first-seen turn for that session.
+type SessionMeta struct {
+ WorkingDir *string `json:"working_dir,omitempty"`
+ SourceFile string `json:"source_file"`
+ StartedAt *int64 `json:"started_at,omitempty"` // optional; server falls back to MIN(turn.timestamp)
+ Metadata json.RawMessage `json:"metadata,omitempty"`
+}