Files
mcias/PROJECT_PLAN.md
Kyle Isom 8f706f10ec Phase 8 plan: add Dockerfile step (Step 8.6)
- PROJECT_PLAN.md: insert Step 8.6 (Dockerfile) before the
  documentation step (renumbered to 8.7); acceptance criteria cover
  multi-stage build, non-root runtime user, EXPOSE ports, VOLUME /data,
  dist/mcias.conf.docker.example, Makefile docker target, and image
  size target (<50 MB)
- ARCHITECTURE.md §18: add Dockerfile to artifact inventory table;
  add Dockerfile Design section covering build stages, security
  properties (no shell, non-root uid 10001, TLS inside container),
  operator workflow, and the new Makefile docker target
2026-03-11 14:47:07 -07:00

29 KiB

MCIAS Project Plan

Discrete implementation steps with acceptance criteria. See ARCHITECTURE.md for design rationale.


Phase 0 — Repository Bootstrap

Step 0.1: Go module and dependency setup

Acceptance criteria:

  • go.mod exists with module path git.wntrmute.dev/kyle/mcias
  • Required dependencies declared: modernc.org/sqlite (CGo-free SQLite), golang.org/x/crypto (Argon2, Ed25519 helpers), github.com/golang-jwt/jwt/v5, github.com/pelletier/go-toml/v2, github.com/google/uuid, git.wntrmute.dev/kyle/goutils
  • go mod tidy succeeds; go build ./... succeeds on empty stubs

Step 0.2: .gitignore

Acceptance criteria:

  • .gitignore excludes: build output (mciassrv, mciasctl), *.db, *.db-wal, *.db-shm, test coverage files, editor artifacts

Phase 1 — Foundational Packages

Step 1.1: internal/model — shared data types

Acceptance criteria:

  • Account, Role, Token, AuditEvent, PGCredential structs defined
  • Account types and statuses as typed constants
  • No external dependencies (pure data definitions)
  • Tests: struct instantiation and constant validation

Step 1.2: internal/config — configuration loading

Acceptance criteria:

  • TOML config parsed into a Config struct matching ARCHITECTURE.md §11
  • Validation: required fields present, listen_addr parseable, TLS paths non-empty, Argon2 params within safe bounds (memory >= 64MB, time >= 2)
  • Master key config: exactly one of passphrase_env or keyfile set
  • Tests: valid config round-trips; missing required field → error; unsafe Argon2 params rejected

Step 1.3: internal/crypto — key management and encryption helpers

Acceptance criteria:

  • GenerateEd25519KeyPair() (crypto/ed25519.PublicKey, crypto/ed25519.PrivateKey, error)
  • MarshalPrivateKeyPEM(key crypto/ed25519.PrivateKey) ([]byte, error) (PKCS#8)
  • ParsePrivateKeyPEM(pemData []byte) (crypto/ed25519.PrivateKey, error)
  • SealAESGCM(key, plaintext []byte) (ciphertext, nonce []byte, err error)
  • OpenAESGCM(key, nonce, ciphertext []byte) ([]byte, error)
  • DeriveKey(passphrase string, salt []byte) ([]byte, error) — Argon2id KDF for master key derivation (separate from password hashing)
  • Key is always 32 bytes (AES-256)
  • crypto/rand used for all nonce/salt generation
  • Tests: seal/open round-trip; open with wrong key → error; PEM round-trip; nonces are unique across calls

Step 1.4: internal/db — database layer

Acceptance criteria:

  • Open(path string) (*DB, error) opens/creates SQLite DB with WAL mode and foreign keys enabled
  • Migrate(db *DB) error applies schema from ARCHITECTURE.md §9 idempotently (version table tracks applied migrations)
  • CRUD for accounts, account_roles, token_revocation, system_tokens, pg_credentials, audit_log, server_config
  • All queries use prepared statements (no string concatenation)
  • Tests: in-memory SQLite (:memory:); each CRUD operation; FK constraint enforcement; migration idempotency

Phase 2 — Authentication Core

Step 2.1: internal/token — JWT issuance and validation

Acceptance criteria:

  • IssueToken(key ed25519.PrivateKey, claims Claims) (string, error)
    • Header: {"alg":"EdDSA","typ":"JWT"}
    • Claims: iss, sub, iat, exp, jti (UUID), roles
  • ValidateToken(key ed25519.PublicKey, tokenString string) (Claims, error)
    • Must check alg header before any other processing; reject non-EdDSA
    • Validates exp, iat, iss, jti presence
    • Returns structured error types for: expired, invalid signature, wrong alg, missing claim
  • RevokeToken(db *DB, jti string, reason string) error
  • PruneExpiredTokens(db *DB) (int64, error) — removes rows past expiry
  • Tests: happy path; expired token rejected; wrong alg (none/HS256/RS256) rejected; tampered signature rejected; missing jti rejected; revoked token checked by caller; pruning removes only expired rows

Step 2.2: internal/auth — login and credential verification

Acceptance criteria:

  • HashPassword(password string, params ArgonParams) (string, error) — returns PHC-format string
  • VerifyPassword(password, hash string) (bool, error) — constant-time; re-hashes if params differ (upgrade path)
  • ValidateTOTP(secret []byte, code string) (bool, error) — RFC 6238, 1-window tolerance, constant-time
  • Login(ctx, db, key, cfg, req LoginRequest) (LoginResponse, error)
    • Orchestrates: load account → verify status → verify password → verify TOTP (if required) → issue JWT → write token_revocation row → write audit_log
    • On any failure: write audit_log (login_fail or login_totp_fail); return generic error to caller (no information about which step failed)
  • Tests: successful login; wrong password (timing: compare duration against a threshold — at minimum assert no short-circuit); wrong TOTP; suspended account; deleted account; TOTP enrolled but not provided; constant-time comparison verified (both branches take comparable time)

Phase 3 — HTTP Server

Step 3.1: internal/middleware — HTTP middleware

Acceptance criteria:

  • RequestLogger — logs method, path, status, duration, IP
  • RequireAuth(key ed25519.PublicKey, db *DB) — extracts Bearer token, validates, checks revocation, injects claims into context; returns 401 on failure
  • RequireRole(role string) — checks claims from context for role; returns 403 on failure
  • RateLimit(rps float64, burst int) — per-IP token bucket; returns 429 on limit exceeded
  • Tests: logger captures fields; RequireAuth rejects missing/invalid/revoked tokens; RequireRole rejects insufficient roles; RateLimit triggers after burst exceeded

Step 3.2: internal/server — HTTP handlers and router

Acceptance criteria:

  • Router wired per ARCHITECTURE.md §8 (all endpoints listed)
  • POST /v1/auth/login — delegates to auth.Login; returns {token, expires_at}
  • POST /v1/auth/logout — revokes current token from context; 204
  • POST /v1/auth/renew — validates current token, issues new one, revokes old
  • POST /v1/token/validate — validates submitted token; returns claims
  • DELETE /v1/token/{jti} — admin or role-scoped; revokes by JTI
  • GET /v1/health — 200 {"status":"ok"}
  • GET /v1/keys/public — returns Ed25519 public key as JWK
  • POST /v1/accounts — admin; creates account
  • GET /v1/accounts — admin; lists accounts (no password hashes in response)
  • GET /v1/accounts/{id} — admin; single account
  • PATCH /v1/accounts/{id} — admin; update status/fields
  • DELETE /v1/accounts/{id} — admin; soft-delete + revoke all tokens
  • GET|PUT /v1/accounts/{id}/roles — admin; get/replace role set
  • POST /v1/auth/totp/enroll — returns TOTP secret + otpauth URI
  • POST /v1/auth/totp/confirm — confirms TOTP enrollment
  • DELETE /v1/auth/totp — admin; removes TOTP from account
  • GET|PUT /v1/accounts/{id}/pgcreds — get/set Postgres credentials
  • Credential fields (password hash, TOTP secret, Postgres password) are never included in any API response
  • Tests: each endpoint happy path; auth middleware applied correctly; invalid JSON body → 400; credential fields absent from all responses

Step 3.3: cmd/mciassrv — server binary

Acceptance criteria:

  • Reads config file path from --config flag
  • Loads config, derives master key, opens DB, runs migrations, loads/generates signing key
  • Starts HTTPS listener on configured address
  • Graceful shutdown on SIGINT/SIGTERM (30s drain)
  • If no signing key exists in DB, generates one and stores it encrypted
  • Integration test: start server on random port, hit /v1/health, assert 200

Phase 4 — Admin CLI

Step 4.1: cmd/mciasctl — admin CLI

Acceptance criteria:

  • Subcommands:
    • mciasctl account create --username NAME --type human|system
    • mciasctl account list
    • mciasctl account suspend --id UUID
    • mciasctl account delete --id UUID
    • mciasctl role grant --account UUID --role ROLE
    • mciasctl role revoke --account UUID --role ROLE
    • mciasctl token issue --account UUID (system accounts)
    • mciasctl token revoke --jti JTI
    • mciasctl pgcreds set --account UUID --host H --port P --db D --user U --password P
    • mciasctl pgcreds get --account UUID
  • CLI reads admin JWT from MCIAS_ADMIN_TOKEN env var or --token flag
  • All commands make HTTPS requests to mciassrv (base URL from --server flag or MCIAS_SERVER env var)
  • Tests: flag parsing; missing required flags → error; help text complete

Phase 5 — End-to-End Tests and Hardening

Step 5.1: End-to-end test suite

Acceptance criteria:

  • TestE2ELoginLogout — create account, login, validate token, logout, validate token again (must fail)
  • TestE2ETokenRenewal — login, renew, old token rejected, new token valid
  • TestE2EAdminFlow — create account via CLI, assign role, login as user
  • TestE2ETOTPFlow — enroll TOTP, login without code (fail), login with code (succeed)
  • TestE2ESystemAccount — create system account, issue token, validate token, rotate token (old token rejected)
  • TestE2EAlgConfusion — attempt login, forge token with alg=HS256/none, submit to validate endpoint, assert 401

Step 5.2: Security hardening review

Acceptance criteria:

  • golangci-lint run ./... passes with zero warnings
  • go test -race ./... passes with zero race conditions
  • Manual review checklist:
    • No password/token/secret in any log line (grep audit)
    • All crypto/rand — no math/rand usage
    • All token comparisons use crypto/subtle
    • Argon2id params meet OWASP minimums
    • JWT alg validated before signature check in all code paths
    • All DB queries use parameterized statements
    • TLS min version enforced in server config

Step 5.3: Documentation and commit

Acceptance criteria:

  • README.md updated with: build instructions, config file example, first-run steps
  • Each phase committed separately with appropriate commit messages
  • PROGRESS.md reflects completed state

Phase 6 — mciasdb: Database Maintenance Tool

See ARCHITECTURE.md §16 for full design rationale, trust model, and command surface.

Step 6.1: cmd/mciasdb — binary skeleton and config loading

Acceptance criteria:

  • Binary at cmd/mciasdb/main.go parses --config flag
  • Loads [database] and [master_key] sections from mciassrv config format
  • Derives master key (passphrase or keyfile, identical logic to mciassrv)
  • Opens SQLite DB with WAL mode and FK enforcement (reuses internal/db)
  • Exits with error if DB cannot be opened (e.g., busy-timeout exceeded)
  • Help text lists all subcommands

Step 6.2: Schema subcommands

Acceptance criteria:

  • mciasdb schema verify — opens DB, reports current schema version, exits 0 if up-to-date, exits 1 if migrations pending
  • mciasdb schema migrate — applies any pending migrations, reports each one applied, exits 0
  • Tests: verify on fresh DB reports version 0; migrate advances to current version; verify after migrate exits 0

Step 6.3: Account and role subcommands

Acceptance criteria:

  • mciasdb account list — prints uuid, username, type, status for all accounts
  • mciasdb account get --id UUID — prints single account record
  • mciasdb account create --username NAME --type human|system — inserts row, prints new UUID
  • mciasdb account set-password --id UUID — prompts twice (confirm), re-hashes with Argon2id, updates row; no --password flag permitted
  • mciasdb account set-status --id UUID --status STATUS — updates status
  • mciasdb account reset-totp --id UUID — clears totp_required and totp_secret_enc
  • mciasdb role list --id UUID — prints roles
  • mciasdb role grant --id UUID --role ROLE — inserts role row
  • mciasdb role revoke --id UUID --role ROLE — deletes role row
  • All write operations append an audit log row with actor tagged mciasdb
  • Tests: each subcommand happy path against in-memory SQLite; unknown UUID returns error; set-password with mismatched confirmation returns error

Step 6.4: Token subcommands

Acceptance criteria:

  • mciasdb token list --id UUID — prints jti, issued_at, expires_at, revoked_at for account
  • mciasdb token revoke --jti JTI — sets revoked_at = now on the row
  • mciasdb token revoke-all --id UUID — revokes all non-revoked tokens for account
  • mciasdb prune tokens — deletes rows from token_revocation where expires_at < now; prints count removed
  • All write operations append an audit log row
  • Tests: revoke on unknown JTI returns error; revoke-all on account with no active tokens is a no-op (exits 0); prune removes only expired rows

Step 6.5: Audit log subcommands

Acceptance criteria:

  • mciasdb audit tail [--n N] — prints last N events (default 50), newest last
  • mciasdb audit query --account UUID — filters by actor_id or target_id
  • mciasdb audit query --type EVENT_TYPE — filters by event_type
  • mciasdb audit query --since TIMESTAMP — filters by event_time >= RFC-3339 timestamp
  • Flags are combinable (AND semantics)
  • --json flag on any audit subcommand emits newline-delimited JSON
  • Tests: tail returns correct count; query filters correctly; --json output is valid JSON

Step 6.6: Postgres credentials subcommands

Acceptance criteria:

  • mciasdb pgcreds get --id UUID — decrypts and prints host, port, db, username, password with a warning header that output is sensitive
  • mciasdb pgcreds set --id UUID --host H --port P --db D --user U — prompts for password interactively (no --password flag), encrypts with AES-256-GCM, stores row
  • All write operations append an audit log row
  • Tests: get on account with no pgcreds returns error; set then get round-trips correctly (decrypted value matches original)

Step 6.7: .gitignore and documentation

Acceptance criteria:

  • .gitignore updated to exclude mciasdb binary
  • README.md updated with mciasdb usage section (when to use vs mciasctl, config requirements, example commands)
  • PROGRESS.md updated to reflect Phase 6 complete


Phase 7 — gRPC Interface

See ARCHITECTURE.md §17 for full design rationale, proto definitions, and transport security requirements.

Step 7.1: Protobuf definitions and generated code

Acceptance criteria:

  • proto/mcias/v1/ directory contains .proto files for all service groups: auth.proto, token.proto, account.proto, admin.proto
  • All RPC methods mirror the REST API surface (see ARCHITECTURE.md §8 and §17)
  • proto/generate.go contains a //go:generate protoc ... directive that produces Go stubs under gen/mcias/v1/ using protoc-gen-go and protoc-gen-go-grpc
  • Protobuf field conventions: snake_case field names, google.protobuf.Timestamp for all time fields, no credential fields in response messages (same exclusion rules as JSON API)
  • go generate ./... re-runs protoc idempotently
  • Tests: generated code compiles cleanly (go build ./... succeeds)

Step 7.2: internal/grpcserver — gRPC handler implementations

Acceptance criteria:

  • Package internal/grpcserver implements all generated gRPC service interfaces
  • Handlers delegate to the same internal/auth, internal/token, and internal/db packages used by the REST server — no duplicated logic
  • Authentication: Bearer token extracted from gRPC metadata key authorization, validated via internal/middleware logic (same JWT validation path)
  • Admin RPCs require the admin role (checked identically to REST middleware)
  • Errors mapped to canonical gRPC status codes:
    • Unauthenticated → codes.Unauthenticated
    • Forbidden → codes.PermissionDenied
    • Not found → codes.NotFound
    • Invalid input → codes.InvalidArgument
    • Internal error → codes.Internal (no internal detail leaked to caller)
  • Tests: unit tests for each RPC handler; unauthenticated calls rejected; credential fields absent from all response messages

Step 7.3: TLS and interceptors

Acceptance criteria:

  • gRPC server uses the same TLS certificate and key as the REST server (loaded from config); minimum TLS 1.2 enforced via tls.Config
  • Unary server interceptor chain:
    1. Request logger (method name, peer IP, status, duration)
    2. Auth interceptor (extracts Bearer token, validates, injects claims into context.Context)
    3. Rate-limit interceptor (per-IP token bucket, same parameters as REST)
  • No credential material logged by any interceptor
  • Tests: interceptor chain applied correctly; rate-limit triggers after burst

Step 7.4: cmd/mciassrv integration — dual-stack server

Acceptance criteria:

  • mciassrv starts both the REST listener and the gRPC listener on separate ports (gRPC port configurable: [server] grpc_addr; defaults to same host, port+1)
  • Both listeners share the same signing key, DB connection, and config
  • Graceful shutdown drains both servers within the configured drain window
  • Config: grpc_addr added to [server] section (optional; if absent, gRPC listener is disabled)
  • Integration test: start server, connect gRPC client, call Health RPC, assert OK response

Step 7.5: cmd/mciasgrpcctl — gRPC admin CLI (optional companion)

Acceptance criteria:

  • Binary at cmd/mciasgrpcctl/main.go with subcommands mirroring mciasctl
  • Uses generated gRPC client stubs; connects to --server address with TLS
  • Auth via --token flag or MCIAS_TOKEN env var (sent as gRPC metadata)
  • Custom CA cert support identical to mciasctl
  • Tests: flag parsing; missing required flags → error; help text complete

Step 7.6: Documentation and commit

Acceptance criteria:

  • ARCHITECTURE.md §17 written (proto service layout, transport security, interceptor chain, dual-stack operation)
  • README.md updated with gRPC section: enabling gRPC, connecting clients, example grpcurl invocations
  • .gitignore updated to exclude the mciasgrpcctl binary (using a root-anchored path /mciasgrpcctl); generated code in gen/ is committed to the repository so that consumers do not need the protoc toolchain
  • PROGRESS.md updated to reflect Phase 7 complete

Phase 8 — Operational Artifacts

See ARCHITECTURE.md §18 for full design rationale and artifact inventory.

Step 8.1: systemd service unit

Acceptance criteria:

  • dist/mcias.service — a hardened systemd unit for mciassrv:
    • Type=notify with sd_notify support (or Type=simple with explicit ExecStart if notify is not implemented)
    • User=mcias / Group=mcias — dedicated service account
    • CapabilityBoundingSet= — no capabilities required (port > 1024 assumed; document this assumption)
    • ProtectSystem=strict, ProtectHome=true, PrivateTmp=true
    • ReadWritePaths=/var/lib/mcias for the SQLite file
    • EnvironmentFile=/etc/mcias/env for MCIAS_MASTER_PASSPHRASE
    • Restart=on-failure, RestartSec=5
    • LimitNOFILE=65536
  • dist/mcias.env.example — template environment file with comments explaining each variable
  • Tests: systemd-analyze verify dist/mcias.service exits 0 (run in CI if systemd available; skip gracefully if not)

Step 8.2: Default configuration file

Acceptance criteria:

  • dist/mcias.conf.example — fully-commented TOML config covering all sections from ARCHITECTURE.md §11, including the new grpc_addr field from Phase 7
  • Comments explain every field, acceptable values, and security implications
  • Defaults match the ARCHITECTURE.md §11 reference config
  • A separate dist/mcias-dev.conf.example suitable for local development (localhost address, relaxed timeouts, short token expiry for testing)

Step 8.3: Installation script

Acceptance criteria:

  • dist/install.sh — POSIX shell script (no bash-isms) that:
    • Creates mcias system user and group (idempotent)
    • Copies binaries to /usr/local/bin/
    • Creates /etc/mcias/ and /var/lib/mcias/ with correct ownership/perms
    • Installs dist/mcias.service to /etc/systemd/system/
    • Prints post-install instructions (key generation, first-run steps)
    • Does not start the service automatically
  • Script is idempotent (safe to re-run after upgrades)
  • Tests: shellcheck passes with zero warnings

Step 8.4: Man pages

Acceptance criteria:

  • man/man1/mciassrv.1 — man page for the server binary; covers synopsis, description, options, config file format, signals, exit codes, and files
  • man/man1/mciasctl.1 — man page for the admin CLI; covers all subcommands with examples
  • man/man1/mciasdb.1 — man page for the DB maintenance tool; includes trust model and safety warnings
  • man/man1/mciasgrpcctl.1 — man page for the gRPC CLI (Phase 7)
  • Man pages written in mdoc format (BSD/macOS compatible) or troff (Linux)
  • make man target in Makefile generates compressed .gz versions
  • Tests: man --warnings -l man/man1/*.1 exits 0 (or equivalent lint)

Step 8.5: Makefile

Acceptance criteria:

  • Makefile at repository root with targets:
    • build — compile all binaries to bin/
    • testgo test -race ./...
    • lintgolangci-lint run ./...
    • generatego generate ./... (proto stubs from Phase 7)
    • man — build compressed man pages
    • install — run dist/install.sh
    • clean — remove bin/ and generated artifacts
    • dist — build release tarballs for linux/amd64 and linux/arm64 (using GOOS/GOARCH cross-compilation)
  • make build works from a clean checkout after go mod download
  • Tests: make build produces binaries; make test passes; make lint passes

Step 8.6: Dockerfile

Acceptance criteria:

  • Dockerfile at repository root using a multi-stage build:
    • Build stage: golang:1.26-bookworm — compiles all four binaries with CGO_ENABLED=1 (required for SQLite via modernc.org/sqlite) and -trimpath -ldflags="-s -w" to strip debug info
    • Runtime stage: debian:bookworm-slim — installs only ca-certificates and libc6; copies binaries from the build stage
    • Final image runs as a non-root user (uid=10001, gid=10001; named mcias)
    • EXPOSE 8443 (REST) and EXPOSE 9443 (gRPC); both are overridable via env
    • VOLUME /data — operator mounts the SQLite database here
    • ENTRYPOINT ["mciassrv"] with CMD ["-config", "/etc/mcias/mcias.conf"]
    • Image must not contain the Go toolchain, source code, or build cache
  • dist/mcias.conf.docker.example — config template suitable for container deployment: listen_addr = "0.0.0.0:8443", grpc_addr = "0.0.0.0:9443", db_path = "/data/mcias.db", TLS cert/key paths under /etc/mcias/
  • Makefile gains a docker target: docker build -t mcias:$(VERSION) . where VERSION defaults to the output of git describe --tags --always
  • Tests:
    • docker build . completes without error (run in CI if Docker available; skip gracefully if not)
    • docker run --rm mcias:latest mciassrv --help exits 0
    • Image size documented in PROGRESS.md (target: under 50 MB)

Step 8.7: Documentation

Acceptance criteria:

  • README.md updated with: quick-start section referencing both the install script and the Docker image, links to man pages, configuration walkthrough
  • ARCHITECTURE.md §18 updated to include the Dockerfile in the artifact inventory and document the container deployment model
  • PROGRESS.md updated to reflect Phase 8 complete

Phase 9 — Client Libraries

See ARCHITECTURE.md §19 for full design rationale, API surface, and per-language implementation notes.

Step 9.1: API surface definition and shared test fixtures

Acceptance criteria:

  • clients/README.md — defines the canonical client API surface that all language implementations must expose:
    • Client type: configured with server URL, optional CA cert, optional token
    • Client.Login(username, password, totp_code) → (token, expires_at, error)
    • Client.Logout() → error
    • Client.RenewToken() → (token, expires_at, error)
    • Client.ValidateToken(token) → (claims, error)
    • Client.GetPublicKey() → (public_key_jwk, error)
    • Client.Health() → error
    • Account management methods (admin only): CreateAccount, ListAccounts, GetAccount, UpdateAccount, DeleteAccount
    • Role management: GetRoles, SetRoles
    • Token management: IssueServiceToken, RevokeToken
    • PG credentials: GetPGCreds, SetPGCreds
  • clients/testdata/ — shared JSON fixtures for mock server responses (used by all language test suites)
  • A mock MCIAS server (test/mock/) in Go that can be started by integration tests in any language via subprocess or a pre-built binary

Step 9.2: Go client library

Acceptance criteria:

  • clients/go/ — Go module git.wntrmute.dev/kyle/mcias/clients/go
  • Package mciasgoclient exposes the canonical API surface from Step 9.1
  • Uses net/http with crypto/tls; custom CA cert supported via x509.CertPool
  • Token stored in-memory; Client.Token() accessor returns current token
  • Thread-safe: concurrent calls from multiple goroutines are safe
  • All JSON decoding uses DisallowUnknownFields
  • Tests:
    • Unit tests with httptest.Server for all methods
    • Integration test against mock server (Step 9.1) covering full login → validate → logout flow
    • go test -race ./... passes with zero race conditions
  • go doc comments on all exported types and methods

Step 9.3: Rust client library

Acceptance criteria:

  • clients/rust/ — Rust crate mcias-client
  • Uses reqwest (async, TLS-enabled) and serde / serde_json
  • Exposes the canonical API surface from Step 9.1 as an async Rust API (tokio-compatible)
  • Custom CA cert supported via reqwest::Certificate
  • Token stored in Arc<Mutex<Option<String>>> for async-safe sharing
  • Errors: typed MciasError enum covering Unauthenticated, Forbidden, NotFound, InvalidInput, Transport, Server
  • Tests:
    • Unit tests with wiremock or mockito for all methods
    • Integration test against mock server covering full login → validate → logout
    • cargo test passes; cargo clippy -- -D warnings passes
  • cargo doc with #[doc] comments on all public items

Step 9.4: Common Lisp client library

Acceptance criteria:

  • clients/lisp/ — ASDF system mcias-client
  • Uses dexador for HTTP and yason (or cl-json) for JSON
  • TLS handled by the underlying Dexador/Usocket stack; custom CA cert documented (platform-specific)
  • Exposes the canonical API surface from Step 9.1 as synchronous functions with keyword arguments
  • Errors signalled as conditions: mcias-error, mcias-unauthenticated, mcias-forbidden, mcias-not-found
  • Token stored as a slot on the mcias-client CLOS object
  • Tests:
    • Unit tests using fiveam with mock HTTP responses (via dexador mocking or a local test server)
    • Integration test against mock server covering full login → validate → logout
    • Tests run with SBCL; (asdf:test-system :mcias-client) passes
  • Docstrings on all exported symbols; (describe 'mcias-client) is informative

Step 9.5: Python client library

Acceptance criteria:

  • clients/python/ — Python package mcias_client; supports Python 3.11+
  • Uses httpx (sync and async variants) or requests (sync-only acceptable for v1)
  • Exposes the canonical API surface from Step 9.1 as a MciasClient class
  • Custom CA cert supported via ssl.create_default_context() with cafile
  • Token stored as an instance attribute; client.token property
  • Errors: MciasError base class with subclasses MciasAuthError, MciasForbiddenError, MciasNotFoundError
  • Typed: full py.typed marker; all public symbols annotated with PEP 526 type annotations; mypy --strict passes
  • Tests:
    • Unit tests with pytest and respx (httpx mock) or responses (requests) for all methods
    • Integration test against mock server covering full login → validate → logout
    • pytest passes; ruff check and mypy --strict pass
  • Docstrings on all public classes and methods; help(MciasClient) is informative

Step 9.6: Documentation and commit

Acceptance criteria:

  • Each client library has its own README.md with: installation instructions, quickstart example, API reference summary, error handling guide
  • ARCHITECTURE.md §19 written (client library design, per-language notes, versioning strategy)
  • PROGRESS.md updated to reflect Phase 9 complete

Implementation Order

Phase 0 → Phase 1 (1.1, 1.2, 1.3, 1.4 in parallel or sequence)
        → Phase 2 (2.1 then 2.2)
        → Phase 3 (3.1, 3.2, 3.3 in sequence)
        → Phase 4
        → Phase 5
        → Phase 6 (6.1 → 6.2 → 6.3 → 6.4 → 6.5 → 6.6 → 6.7)
        → Phase 7 (7.1 → 7.2 → 7.3 → 7.4 → 7.5 → 7.6)
        → Phase 8 (8.1 → 8.2 → 8.3 → 8.4 → 8.5 → 8.6)
        → Phase 9 (9.1 → 9.2 → 9.3 → 9.4 → 9.5 → 9.6)

Each step must have passing tests before the next step begins.