8.5 KiB
AGENTS.md
The engineering contract for this repo. Read it before any change.
The bar: minimal, DRY, no bloat — non-negotiable
Match the implementation to the real complexity of the problem, and stop there. The shortest correct solution wins. If 200 lines do a 20-line job, it's wrong — rewrite it. A senior calling your code "overcomplicated" is a failed review.
- DRY in the purest sense. Every piece of knowledge has exactly one authoritative representation. The same condition written twice — even in unrelated parts of the system — is a defect. The discriminator: the same check at two trust boundaries (untrusted frontend + trusted backend) is not duplication — those are distinct axes of the problem. The same check inside one trust domain (app layer + DB layer, both trusted) is. Always ask: does this axis exist in the problem, or only in my solution?
- No speculative abstraction. Solve the case in front of you, not a hypothetical future. Introduce an abstraction at the second real instance, not the first (Rule of Three). A struct/trait/enum/
AppSpecat n=1 that is longer than what it replaces is a defect. YAGNI. - No ceremony. No struct where a field works; no trait where a function works; no error enum where
anyhowat the binary edge works; no test that exists only to exercise a helper you added to enable that test; no config knob nothing sets; no job/feature wired for an axis that doesn't exist yet. - Core logic stays in one cohesive home. Never leak or scatter a concern across helpers, wrappers, and
utilmodules. Don't reimplement in a test harness, CLI, or example what a Score (or any owning module) already does — compose it. - Comments: WHY only. A non-obvious constraint, an invariant, a workaround. Never narrate WHAT the code does. If deleting a comment loses nothing, it was bloat. No multi-paragraph docstrings.
- This governs everything an agent emits — code, tests, docs, commit messages, and chat replies alike. Concise, information-dense, unbloated. Here brevity is correctness, not style.
Formal frame: minimize accidental complexity (Brooks); the floor is the problem's intrinsic (Kolmogorov) complexity. Over-duplication and over-abstraction are the same error — a description longer than the problem warrants.
For non-trivial implementation or code review, load the essential-code-review skill and run its simplification pass before considering the work complete.
For substantial prose or documentation changes, load the humanizer skill and audit the final text before completion.
What Harmony is
Orchestration for decentralized micro datacenters — small clusters in homes, offices, and community spaces instead of hyperscalers. It replaces the Terraform/Ansible/Helm "YAML mud pit" (config validated at runtime, fragmented across tools, failing at 3 AM) with one Rust codebase where the compiler catches infrastructure misconfigurations before anything deploys. Infrastructure-as-real-code, not a wrapper around existing tools.
Score-Topology-Interpret — the core pattern
- Score — declarative desired state; a struct generic over
T: Topology. Serializable, cloneable, idempotent. - Topology — what an environment can do; exposes capabilities as traits (
DnsServer,K8sclient,HelmCommand,LoadBalancer, …). E.g.K8sAnywhereTopology(local k3d or any cluster),HAClusterTopology(bare-metal HA). - Interpret — translates a Score into operations against a Topology's capabilities; returns an
Outcome(SUCCESS, NOOP, FAILURE, RUNNING, QUEUED, BLOCKED).
Compile-time safety through trait bounds:
impl<T: Topology + DnsServer + DhcpServer> Score<T> for DnsScore { ... }
A Topology missing a required capability is a compile error, not a runtime surprise. Higher-order topologies compose via blanket impls: if T: PostgreSQL then FailoverTopology<T>: PostgreSQL automatically — zero boilerplate.
Capability & Score rules (non-negotiable)
- Capabilities are industry concepts, not tools.
DnsServer, notOpnsenseDns;SecretVault, notOpenbaoStore. Test: if you can swap the backend without rewriting any Score, the boundary is right. Exception: when the developer codes to the implementation —PostgreSQL, notDatabase, because you write PG-specific SQL. - Scores encapsulate operational complexity. Init sequences, retries, distro quirks live inside the Score. A high-level example is ~15 lines, not ~400 of imperative orchestration.
- Idempotent and order-independent. Twice = once. Declare needs via trait bounds; never assume another Score ran first.
See docs/guides/writing-a-score.md.
Deploy architecture (ADR-023, non-negotiable)
- Deploy with Scores, not handrolled manifests. No
k8s_openapi::api::*structs outsideScore::interpret. CLIs, examples, and test harnesses compose*Scoretypes. Building aDeployment/Service/ConfigMapin a harness is the mud pit in Rust clothing — reach for the Score, or write the missing one. - E2E uses the same Scores as prod. Only the
Topologychanges. If e2e needs something prod doesn't, add a knob to the Score — don't fork the manifest. - One Score per deployable component; compose upward (
MyAppScorepulls inPostgresScore, …). No monolithic "deploy everything" Scores. - Deploy returns only after smoke-test success.
helm install && hopeis the anti-pattern Harmony exists to kill. Convergence errors read likerustc, not "exit 1 from helm". (Contract shape open; principle locked.) - Deploy logic lives in a
*-deploycrate depending on bothharmonyand the runtime crate. Runtime binaries stay free of theharmonydep. One deploy crate per app area. - Topologies are compile-time, selected at runtime. A deploy binary lists its topologies; a new backend is a rebuild. No
Box<dyn Topology>loaders. - Extend Scores with companions, not API changes. New cross-cutting capabilities (dry-run, observability, smoke-test) wrap a Score; the base
Score/InterpretAPI stays small. thiserrorin libraries;anyhowonly at binary glue (main.rs-level, where the error is just printed).
See docs/adr/023-deploy-architecture.md.
Architecture (hexagonal, ADR-002)
harmony/src/ — domain/ (score.rs, topology/, interpret/, inventory/, maestro/) is the framework core; infra/ (opnsense, brocade, kube, sqlx) holds driven adapters; modules/ (k8s, postgresql, okd, helm, monitoring, kvm, network, …) holds concrete deployment modules. The domain stays isolated from adapters.
Key crates
| Crate | Purpose |
|---|---|
harmony |
Core framework: domain, infra adapters, deployment modules |
harmony_cli |
CLI + optional TUI (--features tui) |
harmony_config |
Unified config+secret management (env → SQLite → OpenBao → prompt) |
harmony_secret / _derive |
Secret backends (LocalFile, OpenBao, Infisical) |
harmony_execution |
Execution engine |
harmony_agent / harmony_inventory_agent |
Persistent agent framework (NATS JetStream mesh), hardware discovery |
harmony_assets |
Asset management (URLs, local cache, S3) |
harmony_composer |
Infrastructure composition tool |
harmony-k8s |
Kubernetes utilities |
k3d |
Local K3D cluster management |
brocade |
Brocade switch integration |
opnsense-codegen / opnsense-api |
Typed OPNsense client (XML models → Rust); exist because OPNsense ships no typed API. Support crates, not core. |
Key ADRs (docs/adr/)
001 Rust · 002 hexagonal · 003 capabilities at domain level (no vendor lock-in) · 005 Rust DSL over YAML · 007 k3d default runtime · 009 Helm inflated to vanilla k8s YAML · 015 higher-order topologies via blanket impls · 016 agent mesh on NATS JetStream · 020 unified config+secret · 023 deploy architecture.
Build & test
./build/check.sh # full CI: check + fmt + clippy + test
cargo check --all-targets --all-features --keep-going
cargo fmt --check
cargo clippy
cargo test # or: cargo test -p <crate> <test>
cargo run -p <example_crate>
mdbook build
Conventions
Rust edition 2024, resolver v2. Conventional commits (feat:/fix:/chore:/docs:/refactor:). Small, single-purpose PRs (~200 lines, excluding generated code). License AGPL v3. Lean on the type system — compile-time guarantees over runtime checks; abstractions domain-level, not provider-specific.
Dos and Don't
- Never tag "Co-Authored-By" with an agent name in commit messages.