~bigbes/lethe

4ca03bead630f9c1027dfbee5b6b8c75314f9498 — Eugene Blikh a month ago 8a74479
feat: bootstrap lethe server skeleton + wire contract
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"`
}