# 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`.