# 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/` - `test` — `go test -race ./...` - `lint` — `golangci-lint run ./...` - `generate` — `go 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: Documentation **Acceptance criteria:** - `README.md` updated with: quick-start section referencing the install script, links to man pages, configuration walkthrough - ARCHITECTURE.md §18 written (operational artifact inventory, file locations, systemd integration notes) - `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>>` 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.