From 4ca03bead630f9c1027dfbee5b6b8c75314f9498 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 25 Apr 2026 22:17:09 +0300 Subject: [PATCH] feat: bootstrap lethe server skeleton + wire contract --- .air.toml | 22 ++++ .gitignore | 16 +++ .golangci.yml | 28 +++++ Dockerfile | 22 ++++ Justfile | 36 +++++++ README.md | 204 +++++++++++++++++++++++++++++++++++ cmd/lethe/main.go | 18 ++++ config.example.yaml | 28 +++++ docker-compose.yml | 12 +++ docs/tasks/lethe-server.md | 14 +++ go.mod | 55 ++++++++++ go.sum | 156 +++++++++++++++++++++++++++ internal/deps/deps.go | 23 ++++ internal/shared/wire/wire.go | 36 +++++++ 14 files changed, 670 insertions(+) create mode 100644 .air.toml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Dockerfile create mode 100644 Justfile create mode 100644 README.md create mode 100644 cmd/lethe/main.go create mode 100644 config.example.yaml create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/deps/deps.go create mode 100644 internal/shared/wire/wire.go diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000000000000000000000000000000000000..a57beb7314ce900599db7ea5e7336332272ffb80 --- /dev/null +++ b/.air.toml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d7accd9636cf2630bea665ccbd49f2a86b89223d --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe905a5f1165318ad033d763f4a2529f184ab15f --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f97d8bbce3892932c69003e82fee86fb3b3c9bef --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000000000000000000000000000000000000..bef5b126c0c18350f3a70b6efcde833fd5f6d9d2 --- /dev/null +++ b/Justfile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f2247c324099d8640bdd72c9fb29d1e90926464d --- /dev/null +++ b/README.md @@ -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 ` 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: '' + 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=` or `?owner=*`. | +| GET | `/api/v1/sessions/{tool}/{host}/{session_id}` | yes | Full session with turns inline. Admins may pass `?owner=`. | +| 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. diff --git a/cmd/lethe/main.go b/cmd/lethe/main.go new file mode 100644 index 0000000000000000000000000000000000000000..025d4523e6cd4d0352b361acaf8a20a18edac6fe --- /dev/null +++ b/cmd/lethe/main.go @@ -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) +} diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d1a71f0312d00735c7b3ea3d5bdfcd9fc0adf023 --- /dev/null +++ b/config.example.yaml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..99e660d1d02eb5f24224d8bc3c04d5da4b64653e --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docs/tasks/lethe-server.md b/docs/tasks/lethe-server.md index c9180ca1deb1c5951d608200937b278a91671ca5..fef72e88ad9c13f0bfea72603c084f7e2260cb5f 100644 --- a/docs/tasks/lethe-server.md +++ b/docs/tasks/lethe-server.md @@ -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). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..b5e5a9b272a76a482404880fd366a3b87fd34794 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..f75a7ddad86a2ccaab7e6700ce80601dd6bd496d --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/deps/deps.go b/internal/deps/deps.go new file mode 100644 index 0000000000000000000000000000000000000000..c8356c927c83e7bd22c884287484a27e3718df30 --- /dev/null +++ b/internal/deps/deps.go @@ -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" +) diff --git a/internal/shared/wire/wire.go b/internal/shared/wire/wire.go new file mode 100644 index 0000000000000000000000000000000000000000..6aad70f64715fc5353e28836419c9ce0660e9572 --- /dev/null +++ b/internal/shared/wire/wire.go @@ -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"` +}