commit 8b4db22c93bc76164b31644cf4029d3547689e22 Author: Kyle Isom Date: Wed Mar 25 14:17:17 2026 -0700 Initial commit: project setup and db package - Project scaffolding: go.mod, Makefile, .golangci.yaml, doc.go - README, ARCHITECTURE, PROJECT_PLAN, PROGRESS documentation - db package: Open (WAL, FK, busy timeout, 0600 permissions), Migrate (sequential, transactional, idempotent), SchemaVersion, Snapshot (VACUUM INTO) - 11 tests covering open, migrate, and snapshot Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb0d7ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# runtime data +/srv + +# IDE +.idea/ +.vscode/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..e49478b --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,94 @@ +# golangci-lint v2 configuration for mcdsl. +# Principle: fail loudly. Security and correctness issues are errors, not warnings. + +version: "2" + +run: + timeout: 5m + tests: true + +linters: + default: none + enable: + # --- Correctness --- + - errcheck + - govet + - ineffassign + - unused + + # --- Error handling --- + - errorlint + + # --- Security --- + - gosec + - staticcheck + + # --- Style / conventions --- + - revive + + settings: + errcheck: + check-blank: false + check-type-assertions: true + + govet: + enable-all: true + disable: + - shadow + - fieldalignment + + gosec: + severity: medium + confidence: medium + excludes: + - G104 + + errorlint: + errorf: true + asserts: true + comparison: true + + revive: + rules: + - name: error-return + severity: error + - name: unexported-return + severity: error + - name: error-strings + severity: warning + - name: if-return + severity: warning + - name: increment-decrement + severity: warning + - name: var-naming + severity: warning + - name: range + severity: warning + - name: time-naming + severity: warning + - name: indent-error-flow + severity: warning + - name: early-return + severity: warning + # exported and package-comments enabled — this is a shared library, + # exported symbols should have documentation. + - name: exported + severity: warning + +formatters: + enable: + - gofmt + - goimports + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + + exclusions: + paths: + - vendor + rules: + - path: "_test\\.go" + linters: + - gosec + text: "G101" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a60570a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,611 @@ +# MCDSL Architecture + +Metacircular Dynamics Standard Library — Technical Design Document + +--- + +## 1. Overview + +MCDSL is a Go module providing the shared infrastructure that every +Metacircular service needs. It is not a framework — services are not structured +around it. It is a collection of well-tested packages that each solve one +problem, usable independently or together. + +### Design Principles + +- **Extract, don't invent.** Every package in MCDSL is extracted from patterns + already proven across multiple services. No speculative abstractions. +- **Optional composition.** Services import only the packages they need. No + package depends on another MCDSL package unless absolutely necessary. +- **Minimal API surface.** Each package exposes the smallest possible public + API. Configuration is via structs with sensible defaults. +- **Zero magic.** No init() functions, no global state, no reflection-based + wiring. Explicit construction, explicit errors. +- **Stdlib-compatible types.** Functions accept and return `*sql.DB`, + `http.Handler`, `*slog.Logger`, `context.Context` — not custom wrappers. + +### Module Path + +``` +git.wntrmute.dev/kyle/mcdsl +``` + +### Dependencies + +| Dependency | Purpose | +|------------|---------| +| `modernc.org/sqlite` | Pure-Go SQLite driver | +| `github.com/go-chi/chi/v5` | HTTP router | +| `github.com/pelletier/go-toml/v2` | TOML config parsing | +| `google.golang.org/grpc` | gRPC server | +| `github.com/klauspost/compress/zstd` | Zstandard compression for archives | +| `git.wntrmute.dev/kyle/mcias/clients/go` | MCIAS client library | + +All dependencies are already used by existing services. MCDSL adds no new +dependencies to the platform. + +--- + +## 2. Package: `auth` + +MCIAS token validation with caching. Extracted from the `internal/auth/` +packages in metacrypt, mcr, and mcat. + +### Types + +```go +// TokenInfo holds the validated identity of an authenticated caller. +type TokenInfo struct { + Username string + Roles []string + IsAdmin bool +} + +// Config holds MCIAS connection settings. Matches the standard [mcias] +// TOML section used by all services. +type Config struct { + ServerURL string `toml:"server_url"` + CACert string `toml:"ca_cert"` + ServiceName string `toml:"service_name"` + Tags []string `toml:"tags"` +} + +// Authenticator validates MCIAS bearer tokens with a short-lived cache. +type Authenticator struct { /* unexported fields */ } +``` + +### API + +```go +func New(cfg Config, logger *slog.Logger) (*Authenticator, error) +func (a *Authenticator) ValidateToken(token string) (*TokenInfo, error) +func (a *Authenticator) Login(username, password, totpCode string) (token string, expiresAt time.Time, err error) +func (a *Authenticator) Logout(token string) error +``` + +### Cache Behavior + +- Key: SHA-256 of the raw token, stored as `[32]byte`. +- TTL: 30 seconds (hardcoded, matches platform standard). +- Eviction: Lazy — expired entries are replaced on next lookup. No background + goroutine. +- Thread safety: `sync.RWMutex`. Reads take a read lock; cache misses + promote to write lock. + +### Admin Detection + +`IsAdmin` is set by scanning the roles list for the string `"admin"`. This +matches MCIAS's role model — admin is a role, not a flag. + +### Errors + +```go +var ( + ErrInvalidToken = errors.New("auth: invalid token") + ErrInvalidCredentials = errors.New("auth: invalid credentials") + ErrForbidden = errors.New("auth: forbidden by policy") +) +``` + +--- + +## 3. Package: `db` + +SQLite connection setup, migration runner, and snapshot utilities. Extracted +from the `internal/db/` packages across all services. + +### Opening a Database + +```go +func Open(path string) (*sql.DB, error) +``` + +Opens a SQLite database with the standard Metacircular pragmas: + +```sql +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = ON; +PRAGMA busy_timeout = 5000; +``` + +File permissions are set to `0600` (owner read/write only). The function +creates the file if it does not exist. + +Returns a standard `*sql.DB` — no wrapper type. Services use it directly +with `database/sql`. + +### Migrations + +```go +type Migration struct { + Version int + Name string + SQL string +} + +func Migrate(db *sql.DB, migrations []Migration) error +``` + +Migrations are applied sequentially in a transaction. Each migration is +recorded in a `schema_migrations` table: + +```sql +CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL +); +``` + +Already-applied migrations (by version number) are skipped. Timestamps are +stored as RFC 3339 UTC. + +Services define their migrations as a `[]Migration` slice — no embedded SQL +files, no migration DSL. The slice is the schema history. + +### Snapshots + +```go +func Snapshot(db *sql.DB, destPath string) error +``` + +Executes `VACUUM INTO` to create a consistent, standalone copy of the +database at `destPath`. This is the standard backup mechanism for all +Metacircular services. + +--- + +## 4. Package: `config` + +TOML configuration loading with environment variable overrides. Extracted from +the `internal/config/` packages across all services. + +### Standard Sections + +```go +// Base contains the configuration sections common to all services. +// Services embed this in their own config struct. +type Base struct { + Server ServerConfig `toml:"server"` + Database DatabaseConfig `toml:"database"` + MCIAS auth.Config `toml:"mcias"` + Log LogConfig `toml:"log"` +} + +type ServerConfig struct { + ListenAddr string `toml:"listen_addr"` + GRPCAddr string `toml:"grpc_addr"` + TLSCert string `toml:"tls_cert"` + TLSKey string `toml:"tls_key"` + ReadTimeout time.Duration `toml:"read_timeout"` + WriteTimeout time.Duration `toml:"write_timeout"` + IdleTimeout time.Duration `toml:"idle_timeout"` + ShutdownTimeout time.Duration `toml:"shutdown_timeout"` +} + +type DatabaseConfig struct { + Path string `toml:"path"` +} + +type LogConfig struct { + Level string `toml:"level"` +} +``` + +### Loading + +```go +func Load[T any](path string, envPrefix string) (*T, error) +``` + +1. Reads the TOML file at `path`. +2. Unmarshals into `*T`. +3. Applies environment variable overrides using `envPrefix` (e.g., prefix + `"MCR"` maps `MCR_SERVER_LISTEN_ADDR` to `Server.ListenAddr`). +4. Applies defaults for unset optional fields. +5. Validates required fields. + +Environment overrides use reflection to walk the struct, converting field +paths to `PREFIX_SECTION_FIELD` format. Only string, int, bool, duration, +and string slice fields are supported. + +### Defaults + +| Field | Default | +|-------|---------| +| `Log.Level` | `"info"` | +| `Server.ReadTimeout` | `30s` | +| `Server.WriteTimeout` | `30s` | +| `Server.IdleTimeout` | `120s` | +| `Server.ShutdownTimeout` | `60s` | + +### Validation + +`Load` returns an error if any required field is empty: +- `Server.ListenAddr` +- `Server.TLSCert` +- `Server.TLSKey` +- `Database.Path` +- `MCIAS.ServerURL` (if the MCIAS section is present) + +Services add their own validation by implementing an optional `Validate()` +method on their config type. + +--- + +## 5. Package: `httpserver` + +TLS HTTP server setup with chi, standard middleware, and graceful shutdown. +Extracted from `internal/server/` across all services. + +### Types + +```go +type Server struct { + Router *chi.Mux + Logger *slog.Logger + // unexported: httpSrv, cfg +} +``` + +### API + +```go +func New(cfg config.ServerConfig, logger *slog.Logger) *Server +func (s *Server) ListenAndServeTLS(certFile, keyFile string) error +func (s *Server) Shutdown(ctx context.Context) error +``` + +`New` creates a chi router and configures the underlying `http.Server` with: +- TLS 1.3 minimum (`tls.VersionTLS13`) +- Read/write/idle timeouts from config +- The chi router as handler + +Services access `s.Router` to register their routes. + +### Standard Middleware + +```go +func (s *Server) LoggingMiddleware(next http.Handler) http.Handler +``` + +Wraps the response writer to capture the status code, logs after the +request completes: + +``` +level=INFO msg=http method=GET path=/v1/status status=200 remote=10.0.0.1:54321 +``` + +### StatusWriter + +```go +type StatusWriter struct { + http.ResponseWriter + Status int +} +``` + +Exported for use in custom middleware that needs the response status code. + +### JSON Helpers + +```go +func WriteJSON(w http.ResponseWriter, status int, v any) +func WriteError(w http.ResponseWriter, status int, message string) +``` + +`WriteError` writes `{"error": "message"}` — the standard Metacircular +error format. + +--- + +## 6. Package: `grpcserver` + +gRPC server setup with TLS, interceptor chain, and method-map +authentication. Extracted from `internal/grpcserver/` in mcias, mcr, and +mc-proxy. + +### Types + +```go +// MethodMap classifies gRPC methods for access control. +type MethodMap struct { + Public map[string]bool // No auth required + AuthRequired map[string]bool // Valid MCIAS token required + AdminRequired map[string]bool // Admin role required +} + +type Server struct { + GRPCServer *grpc.Server + Logger *slog.Logger + // unexported: listener, auth +} +``` + +### API + +```go +func New(cfg config.ServerConfig, auth *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error) +func (s *Server) Serve() error +func (s *Server) Stop() +``` + +`New` builds a gRPC server with: +- TLS 1.3 from cert/key in config +- Unary interceptor chain: logging → auth (using MethodMap) → user handler +- Services register their implementations on `s.GRPCServer` + +### Auth Interceptor + +The auth interceptor uses the `MethodMap` to determine the required access +level for each RPC: + +1. If the method is in `Public` — pass through, no auth. +2. If the method is in `AuthRequired` — validate the bearer token from + metadata, populate `TokenInfo` in context. +3. If the method is in `AdminRequired` — validate token and require + `IsAdmin == true`. +4. If the method is not in any map — **deny by default**. This is a safety + net: forgetting to register a new RPC results in a denied request, not + an open one. + +### Context Helpers + +```go +func TokenInfoFromContext(ctx context.Context) *auth.TokenInfo +``` + +--- + +## 7. Package: `csrf` + +HMAC-SHA256 double-submit cookie CSRF protection. Extracted from the web +server packages in metacrypt, mcr, and mcat. + +### Types + +```go +type Protect struct { + // unexported: secret, cookieName, fieldName +} +``` + +### API + +```go +func New(secret []byte, cookieName, fieldName string) *Protect +func (p *Protect) Middleware(next http.Handler) http.Handler +func (p *Protect) SetToken(w http.ResponseWriter) string +func (p *Protect) TemplateFunc(w http.ResponseWriter) template.FuncMap +``` + +### Token Format + +``` +base64(nonce) "." base64(HMAC-SHA256(secret, nonce)) +``` + +Nonce is 32 bytes from `crypto/rand`. The token is set as a cookie and must +be submitted as a form field on mutating requests (POST, PUT, PATCH, DELETE). + +### Validation + +The middleware: +1. Skips safe methods (GET, HEAD, OPTIONS). +2. Reads the token from the cookie and the form field. +3. Verifies both are present and equal. +4. Verifies the HMAC signature is valid. +5. Returns 403 on any failure. + +### Template Integration + +`TemplateFunc` returns a `template.FuncMap` with a `csrfField` function that +renders the hidden input: + +```html + +``` + +--- + +## 8. Package: `web` + +Session cookie management, auth middleware, and template rendering helpers +for htmx web UIs. Extracted from `internal/webserver/` across services. + +### Session Cookies + +```go +func SetSessionCookie(w http.ResponseWriter, name, token string) +func ClearSessionCookie(w http.ResponseWriter, name string) +func GetSessionToken(r *http.Request, name string) string +``` + +All session cookies are set with: `HttpOnly`, `Secure`, +`SameSite=Strict`, `Path=/`. + +### Auth Middleware + +```go +func RequireAuth(auth *auth.Authenticator, cookieName string, loginPath string) func(http.Handler) http.Handler +``` + +Extracts the session token from the cookie, validates it via the +Authenticator, and either: +- Sets `TokenInfo` in the request context and calls the next handler. +- Redirects to `loginPath` if the token is missing or invalid. + +### Context Helpers + +```go +func TokenInfoFromContext(ctx context.Context) *auth.TokenInfo +``` + +### Template Helpers + +```go +func RenderTemplate(w http.ResponseWriter, fs embed.FS, name string, data any, funcs ...template.FuncMap) +``` + +Parses `templates/layout.html` and `templates/` from the embedded FS, +merges any provided FuncMaps, and executes the `layout` template. + +--- + +## 9. Package: `archive` + +Service directory snapshot and restore using tar.zst, with SQLite-aware +handling. This is new functionality for MCP, not extracted from existing +services. + +### Snapshot + +```go +type SnapshotOptions struct { + ServiceDir string // e.g., /srv/myservice + DBPath string // e.g., /srv/myservice/myservice.db (for VACUUM INTO) + DB *sql.DB // live database connection (for VACUUM INTO) + ExcludePatterns []string // additional glob patterns to exclude +} + +func Snapshot(opts SnapshotOptions) (io.ReadCloser, error) +``` + +1. Runs `VACUUM INTO` to create a consistent DB copy in a temp file. +2. Walks the service directory, excluding: + - `*.db`, `*.db-wal`, `*.db-shm` (live database files) + - `backups/` directory + - Any patterns in `ExcludePatterns` +3. Adds the VACUUM INTO copy as `.db` in the archive. +4. Returns a streaming tar.zst reader. + +The archive is produced as a stream — it does not need to be fully buffered +in memory. This allows piping directly over a network connection. + +### Restore + +```go +func Restore(r io.Reader, destDir string) error +``` + +Extracts a tar.zst archive into `destDir`. Creates the directory if it does +not exist. Overwrites existing files. Preserves file permissions. + +### Compression + +Zstandard compression via `github.com/klauspost/compress/zstd`. Default +compression level (3) balances speed and ratio for the typical service +directory size (SQLite DB + config + certs, usually under 100 MB). + +--- + +## 10. Package: `health` + +Standard health check implementation for both gRPC and REST. New +functionality to standardize what services already do ad-hoc. + +### gRPC + +Implements `grpc.health.v1.Health` (the standard gRPC health checking +protocol). Services register it on their gRPC server: + +```go +health.RegisterGRPC(grpcServer) +``` + +### REST + +```go +func Handler(db *sql.DB) http.HandlerFunc +``` + +Returns a handler for `GET /healthz` (or whatever path the service mounts +it on) that: +1. Pings the database. +2. Returns `200 {"status": "ok"}` or `503 {"status": "unhealthy", "error": "..."}`. + +--- + +## 11. Inter-Package Dependencies + +``` +archive ──→ db (for Snapshot) +auth ──→ (mcias client library) +config ──→ auth (for auth.Config type) +csrf ──→ (stdlib only) +db ──→ (modernc.org/sqlite) +grpcserver ──→ auth, config +health ──→ db +httpserver ──→ config +web ──→ auth, csrf +``` + +No circular dependencies. Each package can be imported independently except +where noted above. + +--- + +## 12. What MCDSL Does Not Provide + +- **Business logic.** Policy engines, engine registries, OCI handlers — these + are service-specific and stay in each service's `internal/` packages. +- **Proto definitions.** Each service owns its own proto files and generated + code. +- **CLI scaffolding.** Cobra command wiring is minimal and service-specific. +- **Database schemas.** Each service defines its own migrations. MCDSL + provides the runner, not the SQL. +- **Templates and static assets.** Each service's web UI is its own. MCDSL + provides rendering helpers, not content. + +--- + +## 13. Migration Path + +Existing services adopt MCDSL incrementally — one package at a time: + +1. Replace `internal/auth/` with `mcdsl/auth`. +2. Replace database open/pragma code with `mcdsl/db.Open`. +3. Replace migration runner with `mcdsl/db.Migrate`. +4. Replace config loading with `mcdsl/config.Load`. +5. Replace CSRF implementation with `mcdsl/csrf`. +6. Replace server setup with `mcdsl/httpserver` and `mcdsl/grpcserver`. + +Each step is independent. Services can adopt one package without adopting +all of them. The old `internal/` code can be removed after each migration. + +--- + +## 14. Security Considerations + +- **Token caching** uses SHA-256 of the token as the cache key. The raw token + is never used as a map key to prevent timing attacks on map lookup. +- **CSRF secrets** must be generated from `crypto/rand` and should be unique + per service instance. MCDSL does not generate them — the service provides + them. +- **Session cookies** are always `HttpOnly`, `Secure`, `SameSite=Strict`. + These flags are not configurable — relaxing them would be a security defect. +- **gRPC method maps** default to deny. An unregistered method is rejected, + not allowed. This is the most important safety property in the library. +- **File permissions** on databases are `0600`. This is not configurable. +- **TLS 1.3 minimum** is not configurable. Services that need TLS 1.2 (there + should be none) cannot use `httpserver` or `grpcserver`. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26705ab --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: build test vet lint clean all + +build: + go build ./... + +test: + go test ./... + +vet: + go vet ./... + +lint: + golangci-lint run ./... + +clean: + go clean ./... + +all: vet lint test build diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..bc45ec6 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,34 @@ +# MCDSL Progress + +## Current State + +Phase 1 complete. The `db` package is implemented and tested. + +## Completed + +### Phase 0: Project Setup (2026-03-25) +- Initialized Go module (`git.wntrmute.dev/kyle/mcdsl`) +- Created `.golangci.yaml` matching platform standard (with `exported` rule + enabled since this is a shared library) +- Created `Makefile` with standard targets (build, test, vet, lint, all) +- Created `.gitignore` +- Created `doc.go` package doc +- `make all` passes clean + +### Phase 1: `db` — SQLite Foundation (2026-03-25) +- `Open(path string) (*sql.DB, error)` — opens with WAL, FK, busy timeout + 5000ms, 0600 permissions, creates parent dirs +- `Migration` type with Version, Name, SQL fields +- `Migrate(database *sql.DB, migrations []Migration) error` — sequential, + transactional, idempotent, records name and timestamp in schema_migrations +- `SchemaVersion(database *sql.DB) (int, error)` — highest applied version +- `Snapshot(database *sql.DB, destPath string) error` — VACUUM INTO with + 0600 permissions, creates parent dirs +- 11 tests: open (pragmas, permissions, parent dir, existing DB), migrate + (fresh, idempotent, incremental, records name), schema version (empty), + snapshot (data integrity, permissions, parent dir) +- `make all` passes clean (vet, lint 0 issues, 11/11 tests, build) + +## Next Steps + +- Phase 2: `auth` package (MCIAS token validation with caching) diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md new file mode 100644 index 0000000..f367e72 --- /dev/null +++ b/PROJECT_PLAN.md @@ -0,0 +1,192 @@ +# MCDSL Project Plan + +Implementation phases for the Metacircular Dynamics Standard Library. + +Each phase produces a usable, tested package. Phases are ordered by +dependency (foundational packages first) and by value (most-duplicated +code first). + +--- + +## Phase 0: Project Setup + +- [ ] Initialize Go module (`git.wntrmute.dev/kyle/mcdsl`) +- [ ] Create `.golangci.yaml` (matching platform standard) +- [ ] Create `Makefile` with standard targets (build, test, vet, lint, all) +- [ ] Create `.gitignore` + +**Acceptance criteria:** `make all` passes on an empty module. + +--- + +## Phase 1: `db` — SQLite Foundation + +The most universally needed package. Every service with a database uses +identical open/pragma/migration code. + +- [ ] `Open(path string) (*sql.DB, error)` — open with WAL, FK, busy timeout, + 0600 permissions +- [ ] `Migration` type and `Migrate(db *sql.DB, migrations []Migration) error` + — sequential, transactional, idempotent, schema_migrations tracking +- [ ] `Snapshot(db *sql.DB, destPath string) error` — VACUUM INTO wrapper +- [ ] Tests: open, migrate fresh DB, migrate existing DB (idempotent), snapshot + produces valid DB, file permissions + +**Acceptance criteria:** A service can replace its `internal/db/` open and +migrate code with `mcdsl/db` and pass its existing tests. + +--- + +## Phase 2: `auth` — MCIAS Token Validation + +The second most duplicated package. Every authenticated service has its own +copy of the cache-and-validate logic. + +- [ ] `Config` type matching `[mcias]` TOML section +- [ ] `TokenInfo` type (Username, Roles, IsAdmin) +- [ ] `New(cfg Config, logger *slog.Logger) (*Authenticator, error)` +- [ ] `ValidateToken(token string) (*TokenInfo, error)` with 30s SHA-256 cache +- [ ] `Login(username, password, totpCode string) (token string, expiresAt time.Time, err error)` +- [ ] `Logout(token string) error` +- [ ] Error types: `ErrInvalidToken`, `ErrInvalidCredentials`, `ErrForbidden` +- [ ] Tests: cache hit, cache miss, cache expiry, admin detection, concurrent + access, error propagation + +**Acceptance criteria:** A service can replace its `internal/auth/` with +`mcdsl/auth` and pass its existing tests. + +--- + +## Phase 3: `config` — TOML Configuration + +- [ ] `Base` type with standard sections (Server, Database, MCIAS, Log) +- [ ] `ServerConfig`, `DatabaseConfig`, `LogConfig` types +- [ ] `Load[T any](path string, envPrefix string) (*T, error)` — generic + loader with TOML parse, env overrides, defaults, validation +- [ ] Environment override via reflection (`PREFIX_SECTION_FIELD`) +- [ ] Required field validation (listen addr, TLS paths, DB path) +- [ ] Default application (timeouts, log level) +- [ ] Optional `Validate()` interface for service-specific validation +- [ ] Tests: load valid config, missing required fields, env overrides, defaults, + custom validation + +**Acceptance criteria:** A service can replace its `internal/config/` with +`mcdsl/config` embedding `config.Base`, and pass its existing tests. + +--- + +## Phase 4: `httpserver` — HTTP Server Setup + +- [ ] `Server` type wrapping chi + `http.Server` +- [ ] `New(cfg config.ServerConfig, logger *slog.Logger) *Server` +- [ ] `ListenAndServeTLS(certFile, keyFile string) error` +- [ ] `Shutdown(ctx context.Context) error` +- [ ] `LoggingMiddleware` — captures status code, logs request metadata +- [ ] `StatusWriter` — exported response writer wrapper +- [ ] `WriteJSON` and `WriteError` helpers +- [ ] Tests: server starts and shuts down cleanly, logging middleware captures + status, JSON helpers produce correct output + +**Acceptance criteria:** A service can replace its server setup and middleware +boilerplate with `mcdsl/httpserver`. + +--- + +## Phase 5: `csrf` — CSRF Protection + +- [ ] `New(secret []byte, cookieName, fieldName string) *Protect` +- [ ] `Middleware(next http.Handler) http.Handler` +- [ ] `SetToken(w http.ResponseWriter) string` +- [ ] `TemplateFunc(w http.ResponseWriter) template.FuncMap` +- [ ] Token format: `base64(nonce).base64(HMAC-SHA256(secret, nonce))` +- [ ] Tests: token generation, validation, middleware rejects missing/invalid + tokens, safe methods pass through + +**Acceptance criteria:** A service can replace its CSRF implementation with +`mcdsl/csrf` and its web UI continues to work. + +--- + +## Phase 6: `web` — Session and Template Helpers + +- [ ] `SetSessionCookie`, `ClearSessionCookie`, `GetSessionToken` +- [ ] `RequireAuth` middleware (validates token, redirects to login) +- [ ] `TokenInfoFromContext` context helper +- [ ] `RenderTemplate` helper for layout + page template pattern +- [ ] Tests: cookie setting/clearing, auth middleware redirect, template + rendering + +**Acceptance criteria:** A service can replace its web session/auth +boilerplate with `mcdsl/web`. + +--- + +## Phase 7: `grpcserver` — gRPC Server Setup + +- [ ] `MethodMap` type (Public, AuthRequired, AdminRequired) +- [ ] `New(cfg config.ServerConfig, auth *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error)` +- [ ] `Serve() error` and `Stop()` +- [ ] Auth interceptor using MethodMap (default deny for unmapped methods) +- [ ] Logging interceptor +- [ ] `TokenInfoFromContext` context helper +- [ ] Tests: public method allowed, auth method requires token, admin method + requires admin, unmapped method denied, logging + +**Acceptance criteria:** A service can replace its gRPC server setup and +interceptor logic with `mcdsl/grpcserver`. + +--- + +## Phase 8: `health` — Health Checks + +- [ ] `RegisterGRPC(srv *grpc.Server)` — register `grpc.health.v1.Health` +- [ ] `Handler(db *sql.DB) http.HandlerFunc` — REST health endpoint +- [ ] Tests: healthy response, unhealthy response (closed DB) + +**Acceptance criteria:** Services have a standard health check that MCP can +query. + +--- + +## Phase 9: `archive` — Service Directory Snapshots + +- [ ] `SnapshotOptions` type +- [ ] `Snapshot(opts SnapshotOptions) (io.ReadCloser, error)` — streaming + tar.zst with DB exclusion/injection +- [ ] `Restore(r io.Reader, destDir string) error` +- [ ] Exclude patterns: `*.db`, `*.db-wal`, `*.db-shm`, `backups/` +- [ ] Tests: snapshot roundtrip (snapshot then restore produces identical + files), DB consistency (VACUUM INTO copy matches), excludes work, + streaming (no full buffer) + +**Acceptance criteria:** MCP agent can snapshot and restore a service +directory using this package. + +--- + +## Phase 10: Service Migration (First Adopter) + +Pick one service (mcat is the simplest) and migrate it to use MCDSL: + +- [ ] Replace `internal/auth/` with `mcdsl/auth` +- [ ] Replace `internal/config/` with `mcdsl/config` +- [ ] Replace web session/CSRF code with `mcdsl/csrf` and `mcdsl/web` +- [ ] Verify `make all` passes +- [ ] Document the migration process for other services + +**Acceptance criteria:** mcat works identically using MCDSL, with its +`internal/` packages reduced to service-specific logic only. + +--- + +## Phase 11: Broader Adoption + +Migrate remaining services one at a time: + +- [ ] metacrypt +- [ ] mcr +- [ ] mc-proxy (subset: db, config — no web/csrf) +- [ ] mcias (subset: db, config, httpserver — owns the auth client, not a consumer) + +Each migration follows the same pattern as Phase 10. Services are migrated +independently — there is no big-bang cutover. diff --git a/README.md b/README.md new file mode 100644 index 0000000..204ef22 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# MCDSL — Metacircular Dynamics Standard Library + +MCDSL is a shared Go library for Metacircular Dynamics services. It extracts +the common patterns that every service implements independently — MCIAS +authentication, SQLite database setup, TLS server bootstrapping, CSRF +protection, configuration loading, and service data snapshots — into a single, +tested, reusable module. + +## Why + +Every Metacircular service follows the same patterns (see +`engineering-standards.md`). Today, each service copy-pastes these patterns +into its own `internal/` packages. This means: + +- Bug fixes must be applied N times (once per service). +- Subtle divergences accumulate (e.g., CSRF tokens use base64 in one service, + hex in another; auth cache keys are `[32]byte` in some, hex strings in + others). +- New services require copying and adapting boilerplate from an existing + service. + +MCDSL extracts the 95%+ identical code into a shared library. Services import +it and provide only their service-specific logic. + +## Module Path + +``` +git.wntrmute.dev/kyle/mcdsl +``` + +## Packages + +| Package | Purpose | +|---------|---------| +| `auth` | MCIAS token validation with 30-second SHA-256 cache | +| `db` | SQLite connection setup (WAL, FK, busy timeout), migration runner, VACUUM INTO snapshots | +| `config` | TOML config loading with environment variable overrides, standard section types | +| `httpserver` | TLS 1.3 HTTP server setup with chi, graceful shutdown, logging middleware | +| `grpcserver` | gRPC server setup with TLS, interceptor chain helpers, method map auth | +| `csrf` | HMAC-SHA256 double-submit cookie CSRF protection | +| `web` | Session cookie management, template rendering helpers, auth middleware | +| `archive` | tar.zst service directory snapshot and restore with SQLite-aware handling | +| `health` | Standard health check implementation (gRPC Health/v1 + REST) | + +## Quick Start + +```go +import ( + "git.wntrmute.dev/kyle/mcdsl/auth" + "git.wntrmute.dev/kyle/mcdsl/db" + "git.wntrmute.dev/kyle/mcdsl/config" + "git.wntrmute.dev/kyle/mcdsl/httpserver" +) + +// Load config with standard sections + service-specific fields. +type MyConfig struct { + config.Base + MyService MyServiceConfig `toml:"my_service"` +} + +cfg, err := config.Load[MyConfig]("my-service.toml", "MYSERVICE") + +// Open database with standard pragmas and run migrations. +database, err := db.Open(cfg.Database.Path) +migrations := []db.Migration{ + {Version: 1, Name: "initial schema", SQL: `CREATE TABLE ...`}, +} +db.Migrate(database, migrations) + +// Set up MCIAS authentication with token caching. +mcauth, err := auth.New(cfg.MCIAS) + +// Start TLS server with standard middleware. +srv := httpserver.New(cfg.Server, logger) +srv.Route(func(r chi.Router) { + r.Use(srv.LoggingMiddleware) + // register routes... +}) +srv.ListenAndServeTLS() +``` + +## Build and Test + +```bash +go build ./... +go test ./... +go vet ./... +golangci-lint run ./... +``` + +## Documentation + +- [ARCHITECTURE.md](ARCHITECTURE.md) — full library specification +- [PROJECT_PLAN.md](PROJECT_PLAN.md) — implementation phases +- [PROGRESS.md](PROGRESS.md) — development status +- [../engineering-standards.md](../engineering-standards.md) — platform-wide standards diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..e18457f --- /dev/null +++ b/db/db.go @@ -0,0 +1,181 @@ +// Package db provides SQLite database setup, migrations, and snapshots +// for Metacircular services. +// +// All databases are opened with the standard Metacircular pragmas (WAL mode, +// foreign keys, busy timeout) and restrictive file permissions (0600). +package db + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "time" + + _ "modernc.org/sqlite" // SQLite driver (pure Go, no CGo). +) + +// Open opens or creates a SQLite database at path with the standard +// Metacircular pragmas: +// +// PRAGMA journal_mode = WAL; +// PRAGMA foreign_keys = ON; +// PRAGMA busy_timeout = 5000; +// +// The file is created with 0600 permissions (owner read/write only). +// The parent directory is created if it does not exist. +// +// Open returns a standard [*sql.DB] — no wrapper types. Services use it +// directly with database/sql. +func Open(path string) (*sql.DB, error) { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0700); err != nil { + return nil, fmt.Errorf("db: create directory %s: %w", dir, err) + } + + // Pre-create the file with restrictive permissions if it does not exist. + if _, err := os.Stat(path); os.IsNotExist(err) { + f, createErr := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) //nolint:gosec // path is caller-provided config, not user input + if createErr != nil { + return nil, fmt.Errorf("db: create file %s: %w", path, createErr) + } + _ = f.Close() + } + + database, err := sql.Open("sqlite", path) + if err != nil { + return nil, fmt.Errorf("db: open %s: %w", path, err) + } + + pragmas := []string{ + "PRAGMA journal_mode = WAL", + "PRAGMA foreign_keys = ON", + "PRAGMA busy_timeout = 5000", + } + for _, p := range pragmas { + if _, execErr := database.Exec(p); execErr != nil { + _ = database.Close() + return nil, fmt.Errorf("db: %s: %w", p, execErr) + } + } + + // Ensure permissions are correct even if the file already existed. + if err := os.Chmod(path, 0600); err != nil { + _ = database.Close() + return nil, fmt.Errorf("db: chmod %s: %w", path, err) + } + + return database, nil +} + +// Migration is a numbered, named schema change. Services define their +// migrations as a []Migration slice — the slice is the schema history. +type Migration struct { + // Version is the migration number. Must be unique and should be + // sequential starting from 1. + Version int + + // Name is a short human-readable description (e.g., "initial schema"). + Name string + + // SQL is the DDL/DML to execute. Multiple statements are allowed + // (separated by semicolons). Each migration runs in a transaction. + SQL string +} + +// Migrate applies all pending migrations from the given slice. It creates +// the schema_migrations tracking table if it does not exist. +// +// Each migration runs in its own transaction. Already-applied migrations +// (identified by version number) are skipped. Timestamps are stored as +// RFC 3339 UTC. +func Migrate(database *sql.DB, migrations []Migration) error { + _, err := database.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + applied_at TEXT NOT NULL DEFAULT '' + )`) + if err != nil { + return fmt.Errorf("db: create schema_migrations: %w", err) + } + + for _, m := range migrations { + applied, checkErr := migrationApplied(database, m.Version) + if checkErr != nil { + return checkErr + } + if applied { + continue + } + + tx, txErr := database.Begin() + if txErr != nil { + return fmt.Errorf("db: begin migration %d (%s): %w", m.Version, m.Name, txErr) + } + + if _, execErr := tx.Exec(m.SQL); execErr != nil { + _ = tx.Rollback() + return fmt.Errorf("db: migration %d (%s): %w", m.Version, m.Name, execErr) + } + + now := time.Now().UTC().Format(time.RFC3339) + if _, execErr := tx.Exec( + `INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)`, + m.Version, m.Name, now, + ); execErr != nil { + _ = tx.Rollback() + return fmt.Errorf("db: record migration %d: %w", m.Version, execErr) + } + + if commitErr := tx.Commit(); commitErr != nil { + return fmt.Errorf("db: commit migration %d: %w", m.Version, commitErr) + } + } + + return nil +} + +// SchemaVersion returns the highest applied migration version, or 0 if +// no migrations have been applied. +func SchemaVersion(database *sql.DB) (int, error) { + var version sql.NullInt64 + err := database.QueryRow(`SELECT MAX(version) FROM schema_migrations`).Scan(&version) + if err != nil { + return 0, fmt.Errorf("db: schema version: %w", err) + } + if !version.Valid { + return 0, nil + } + return int(version.Int64), nil +} + +// Snapshot creates a consistent backup of the database at destPath using +// SQLite's VACUUM INTO. The destination file is created with 0600 +// permissions. +func Snapshot(database *sql.DB, destPath string) error { + dir := filepath.Dir(destPath) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("db: create snapshot directory %s: %w", dir, err) + } + + if _, err := database.Exec("VACUUM INTO ?", destPath); err != nil { + return fmt.Errorf("db: snapshot: %w", err) + } + + if err := os.Chmod(destPath, 0600); err != nil { + return fmt.Errorf("db: chmod snapshot %s: %w", destPath, err) + } + + return nil +} + +func migrationApplied(database *sql.DB, version int) (bool, error) { + var count int + err := database.QueryRow( + `SELECT COUNT(*) FROM schema_migrations WHERE version = ?`, version, + ).Scan(&count) + if err != nil { + return false, fmt.Errorf("db: check migration %d: %w", version, err) + } + return count > 0, nil +} diff --git a/db/db_test.go b/db/db_test.go new file mode 100644 index 0000000..93b04bc --- /dev/null +++ b/db/db_test.go @@ -0,0 +1,304 @@ +package db + +import ( + "database/sql" + "os" + "path/filepath" + "testing" +) + +func TestOpen(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + + database, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + defer func() { _ = database.Close() }() + + // Verify WAL mode is enabled. + var journalMode string + if err := database.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil { + t.Fatalf("query journal_mode: %v", err) + } + if journalMode != "wal" { + t.Fatalf("journal_mode = %q, want %q", journalMode, "wal") + } + + // Verify foreign keys are enabled. + var fk int + if err := database.QueryRow("PRAGMA foreign_keys").Scan(&fk); err != nil { + t.Fatalf("query foreign_keys: %v", err) + } + if fk != 1 { + t.Fatalf("foreign_keys = %d, want 1", fk) + } + + // Verify busy timeout. + var timeout int + if err := database.QueryRow("PRAGMA busy_timeout").Scan(&timeout); err != nil { + t.Fatalf("query busy_timeout: %v", err) + } + if timeout != 5000 { + t.Fatalf("busy_timeout = %d, want 5000", timeout) + } +} + +func TestOpenFilePermissions(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + + database, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + _ = database.Close() + + info, err := os.Stat(path) + if err != nil { + t.Fatalf("Stat: %v", err) + } + perm := info.Mode().Perm() + if perm != 0600 { + t.Fatalf("permissions = %o, want 0600", perm) + } +} + +func TestOpenCreatesParentDir(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "sub", "dir", "test.db") + + database, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + _ = database.Close() + + if _, err := os.Stat(path); err != nil { + t.Fatalf("database file does not exist: %v", err) + } +} + +func TestOpenExistingDB(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + + // Create and populate. + db1, err := Open(path) + if err != nil { + t.Fatalf("Open (first): %v", err) + } + if _, err := db1.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY)"); err != nil { + t.Fatalf("create table: %v", err) + } + if _, err := db1.Exec("INSERT INTO t (id) VALUES (42)"); err != nil { + t.Fatalf("insert: %v", err) + } + _ = db1.Close() + + // Reopen and verify data persists. + db2, err := Open(path) + if err != nil { + t.Fatalf("Open (second): %v", err) + } + defer func() { _ = db2.Close() }() + + var id int + if err := db2.QueryRow("SELECT id FROM t").Scan(&id); err != nil { + t.Fatalf("select: %v", err) + } + if id != 42 { + t.Fatalf("id = %d, want 42", id) + } +} + +var testMigrations = []Migration{ + { + Version: 1, + Name: "create users", + SQL: `CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`, + }, + { + Version: 2, + Name: "add email", + SQL: `ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''`, + }, +} + +func TestMigrate(t *testing.T) { + database := openTestDB(t) + + if err := Migrate(database, testMigrations); err != nil { + t.Fatalf("Migrate: %v", err) + } + + // Verify both migrations applied. + version, err := SchemaVersion(database) + if err != nil { + t.Fatalf("SchemaVersion: %v", err) + } + if version != 2 { + t.Fatalf("schema version = %d, want 2", version) + } + + // Verify schema is correct. + if _, err := database.Exec("INSERT INTO users (name, email) VALUES ('a', 'a@b.c')"); err != nil { + t.Fatalf("insert into migrated schema: %v", err) + } +} + +func TestMigrateIdempotent(t *testing.T) { + database := openTestDB(t) + + // Run twice. + if err := Migrate(database, testMigrations); err != nil { + t.Fatalf("Migrate (first): %v", err) + } + if err := Migrate(database, testMigrations); err != nil { + t.Fatalf("Migrate (second): %v", err) + } + + version, err := SchemaVersion(database) + if err != nil { + t.Fatalf("SchemaVersion: %v", err) + } + if version != 2 { + t.Fatalf("schema version = %d, want 2", version) + } +} + +func TestMigrateIncremental(t *testing.T) { + database := openTestDB(t) + + // Apply only the first migration. + if err := Migrate(database, testMigrations[:1]); err != nil { + t.Fatalf("Migrate (first only): %v", err) + } + + version, err := SchemaVersion(database) + if err != nil { + t.Fatalf("SchemaVersion: %v", err) + } + if version != 1 { + t.Fatalf("schema version = %d, want 1", version) + } + + // Now apply all — should pick up only migration 2. + if err := Migrate(database, testMigrations); err != nil { + t.Fatalf("Migrate (all): %v", err) + } + + version, err = SchemaVersion(database) + if err != nil { + t.Fatalf("SchemaVersion: %v", err) + } + if version != 2 { + t.Fatalf("schema version = %d, want 2", version) + } +} + +func TestMigrateRecordsName(t *testing.T) { + database := openTestDB(t) + + if err := Migrate(database, testMigrations); err != nil { + t.Fatalf("Migrate: %v", err) + } + + var name string + err := database.QueryRow( + `SELECT name FROM schema_migrations WHERE version = 1`, + ).Scan(&name) + if err != nil { + t.Fatalf("query migration name: %v", err) + } + if name != "create users" { + t.Fatalf("migration name = %q, want %q", name, "create users") + } +} + +func TestSchemaVersionEmpty(t *testing.T) { + database := openTestDB(t) + + // Create the table but apply no migrations. + if err := Migrate(database, nil); err != nil { + t.Fatalf("Migrate(nil): %v", err) + } + + version, err := SchemaVersion(database) + if err != nil { + t.Fatalf("SchemaVersion: %v", err) + } + if version != 0 { + t.Fatalf("schema version = %d, want 0", version) + } +} + +func TestSnapshot(t *testing.T) { + database := openTestDB(t) + + // Create some data. + if _, err := database.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)"); err != nil { + t.Fatalf("create table: %v", err) + } + if _, err := database.Exec("INSERT INTO t (val) VALUES ('hello')"); err != nil { + t.Fatalf("insert: %v", err) + } + + // Snapshot. + dir := t.TempDir() + snapPath := filepath.Join(dir, "snap.db") + if err := Snapshot(database, snapPath); err != nil { + t.Fatalf("Snapshot: %v", err) + } + + // Verify snapshot file permissions. + info, err := os.Stat(snapPath) + if err != nil { + t.Fatalf("Stat snapshot: %v", err) + } + if perm := info.Mode().Perm(); perm != 0600 { + t.Fatalf("snapshot permissions = %o, want 0600", perm) + } + + // Open snapshot and verify data. + snapDB, err := sql.Open("sqlite", snapPath) + if err != nil { + t.Fatalf("open snapshot: %v", err) + } + defer func() { _ = snapDB.Close() }() + + var val string + if err := snapDB.QueryRow("SELECT val FROM t").Scan(&val); err != nil { + t.Fatalf("select from snapshot: %v", err) + } + if val != "hello" { + t.Fatalf("val = %q, want %q", val, "hello") + } +} + +func TestSnapshotCreatesParentDir(t *testing.T) { + database := openTestDB(t) + + dir := t.TempDir() + snapPath := filepath.Join(dir, "sub", "snap.db") + if err := Snapshot(database, snapPath); err != nil { + t.Fatalf("Snapshot: %v", err) + } + + if _, err := os.Stat(snapPath); err != nil { + t.Fatalf("snapshot file does not exist: %v", err) + } +} + +func openTestDB(t *testing.T) *sql.DB { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "test.db") + database, err := Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + t.Cleanup(func() { _ = database.Close() }) + return database +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..f2e2a47 --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +// Package mcdsl is the Metacircular Dynamics Standard Library. +// +// It provides shared infrastructure packages for Metacircular services: +// authentication, database setup, configuration loading, HTTP/gRPC server +// bootstrapping, CSRF protection, session management, health checks, and +// service directory snapshots. +package mcdsl diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c5dbd6e --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.wntrmute.dev/kyle/mcdsl + +go 1.25.7 + +require modernc.org/sqlite v1.47.0 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.42.0 // indirect + modernc.org/libc v1.70.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 0000000..aadd7dd --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +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/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/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +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.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +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=