feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy

- T01: Go module (keycape), full directory skeleton, Makefile, CI workflow
- T02: spec/canonical-model.yaml with 6 entities + Go domain types
- T03: spec/ldap-schema.yaml + validator binary with structural/semantic rules
- T04: Error taxonomy — 4 stable error types, JSON format, HTTP helpers

28 tests pass, go vet clean, go build clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 01:27:54 +01:00
parent f3b1cdcba4
commit 329e996619
21 changed files with 1992 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
---
id: ADR-0001
title: "Implementation language for KeyCape: Go"
status: accepted
date: 2026-03-13
decided_by: Bernd
hub_decision_id: 620beb04-fa3f-4a9d-9806-02890a7a2b0d
workstream: KEY-WP-0001 (keycape-implementation)
alternatives_considered: [Rust]
---
# ADR-0001 — Implementation Language: Go
## Status
Accepted
## Context
KeyCape must be implemented in a language that satisfies the requirements of spec §11:
> Keycape SHOULD be implemented in Go or Rust.
> Key requirements: stateless, small memory footprint, simple deployment, clear logging,
> structured telemetry.
Both Go and Rust are valid per spec. A decision was needed before T01 (project setup).
### What KeyCape actually does
KeyCape is an **orchestrating boundary service**, not a protocol or security engine:
- delegates authentication UI and session to **Authelia**
- delegates identity storage to **LLDAP**
- delegates MFA enforcement to **privacyIDEA**
- its own code is: HTTP endpoints, config loading, JWT signing (via library), JSON/JWKS handling,
structured telemetry, adapter glue, migration CLI tooling, LDAP schema validator
The main risks are **understandability and clean integration boundaries**, not memory safety in
hot loops or complex parser internals.
## Decision
**Go.**
## Rationale
### Why Go fits KeyCape
Go is especially strong for the actual implementation surface of KeyCape:
| What KeyCape does | Go fit |
|---|---|
| HTTP API server | excellent |
| Config loading and static validation | excellent |
| Adapter code to Authelia, LLDAP, privacyIDEA | excellent |
| JWT/JWKS/JSON handling | excellent |
| Structured logging and Prometheus metrics | excellent |
| CLI tooling (migration, validator, export) | excellent |
| Integration tests in containers | excellent |
Go also provides:
- faster iteration to a working, testable v1
- simpler dependency and build model
- easy static binaries and minimal container images
- low enough runtime overhead for the stated lightweight target
- straightforward output for coding agents across a growing infra codebase
### Why not Rust for this scope
Rust's advantages are real — stronger compile-time safety, better memory control, excellent for
security-critical infrastructure — but they pay back most clearly when:
- substantial protocol machinery is implemented internally
- complex async concurrency or parser-heavy code is required
- the service is intended as a long-lived, standalone security product for others
KeyCape's design **intentionally avoids** all of that. It stays narrow and delegates to existing
components. For a façade/orchestrator, developer friction matters more than theoretical maximal
correctness.
### Decision rule
> **Pick Go if KeyCape is primarily an orchestrating boundary service.**
> **Pick Rust if KeyCape starts becoming a real protocol/security engine.**
Based on the v0.1 spec, KeyCape is clearly the first.
## Consequences
### Positive
- fastest path to a working, testable, operationally simple implementation
- simple build, deploy, and CI story
- lower friction for coding agents producing coherent infra code
### Negative / risks
- weaker compiler-enforced invariants than Rust
- easier to write sloppy code if discipline lapses
- error handling and domain modeling can drift if not designed carefully
### Compensating guardrails (mandatory)
To recover the rigor that Rust would provide via the type system:
1. **Typed domain models** for the profile contract — no raw maps or untyped JSON in business logic
2. **Narrow adapter interfaces**`server/` layer never sees LDAP, Authelia, or privacyIDEA types directly
3. **Layered architecture** — protocol layer | domain layer | adapter layer | migration layer (hard boundaries)
4. **Strict schema/config validation** at startup and in CI
5. **Fuzz and property tests** around the LDAP schema validator, redirect URI checker, and claim mapping
6. **No cleverness** — small, deterministic functions
## Revisit trigger
Reconsider this decision if a subcomponent (e.g. LDAP schema validator, token normalization
engine, or a future high-throughput policy evaluator) demonstrably needs stronger guarantees.
That subcomponent could be redesigned in Rust without regretting the overall Go choice — Go and
Rust interop via CGo or separate binaries is feasible.