feat: ADR — unified configuration backend across canopy domains

Description

Canopy has at least 7 configuration domains, each handled differently today. Each shares substantial plumbing concerns (where the file lives, how it's loaded, how it's reloaded, how it's versioned) but each invented its own answer. This issue tracks the cross-canopy ADR that unifies the backend (where config comes from) while leaving the per-domain overlays (precedence, audit, mutation surface) where they are.

Surfaced during ADR-023 ratification for epic &51 (#460) — the worker portal redesign's composition baseline source raised the broader question: is composition special, or is it just the 7th instance of an under-generalized pattern?

Current state — 7 domains, 7 mechanisms

Domain Storage Format Override Mutation Audit Origin ADR
Service config config/{svc}/default.yaml + env yaml + env vars YAML env > env-yaml > default (3-tier) filesystem + env none ADR-012
Secrets secrets/dev.yaml (SOPS) YAML encrypted env > yaml filesystem encrypted none ADR-017
Jurisdiction policy jurisdiction.toml per-jurisdiction TOML single-tier, PAMMS-cited filesystem (xtask) citation audit ADR-006, ADR-011
Rulesets rulesets/federal/ + rulesets/{juris}/ JDM federal + per-jurisdiction overlay filesystem (xtask) rules engine ADR-003, ADR-006
Theme rulesets/{juris}/theme.toml TOML per-jurisdiction only filesystem none (in CLAUDE.md)
IDP rulesets/{juris}/idp.toml (planned #493 (closed)) TOML per-jurisdiction only filesystem none (planned)
Composition (ratified 2026-05-20) rulesets/{juris}/composition/*.toml + DB JSON Patch TOML + JSON Patch 5-layer top-wins filesystem + DB write API + Studio JWS AuditEvent ADR-021/022

What's shared vs domain-specific

Shared plumbing — one canopy-wide mechanism could handle all of these:

  • Where does the file live? Filesystem, git clone, HTTP URL, S3 bucket
  • How do I get the latest version? Boot-time, scheduled refresh, signal-driven, filesystem-watch
  • How do I detect schema drift? deny_unknown_fields, JSON schema validation, etc.
  • How do I version it? Git SHA, content hash, ETag, mtime

Domain-specific overlays — genuinely different per domain, do not generalize:

  • Precedence rules (3-tier env-wins vs 5-tier user-wins vs single-tier vs federal+overlay)
  • Mutation surfaces (xtask only vs runtime write API vs encrypted-at-rest)
  • Audit requirements (none vs PAMMS citation vs JWS-signed)
  • Validation (per-domain schema + semantic checks)

The ADR's question

Define the ConfigBackend (or ConfigSource) trait that every config domain in canopy plugs into. Per-domain overlays (precedence, audit, mutation, validation) stay domain-specific and unchanged.

Concretely, the ADR ratifies:

. The trait shape (read-only contract: load(path) -> Result<Bytes, Error>, version(path) -> Result<Version, Error>, subscribe(path) -> Stream<ChangeEvent>) . v1 backend impls (likely filesystem only — matches current state) . v2+ backend impls (git clone with refresh, HTTP, S3) . Per-domain migration path: how each existing domain (ADR-006/011/012/017) plugs into the new backend without breaking . Whether write capability is part of the trait (likely separate WritableBackend sub-trait; only some backends are writable)

Acceptance Criteria

  • docs/modules/ROOT/pages/adrs/adr-NNN-unified-config-backend.adoc ratifying the trait shape + v1 backend(s) + migration path
  • Cross-references from ADR-006, ADR-011, ADR-012, ADR-017, ADR-021, ADR-022 indicating which config domain plugs in
  • No runtime code in the ADR-only MR; implementation MRs follow
  • Antora nav updated

Why this came up now

Filed during epic &51 (#460) Stage-2 ADR ratification (composability runtime + storage layering + promote-PR). When drafting ADR-023 (promote-PR mechanism for composition baselines), the question "what is the contract between canopy and the source of composition baselines" became "what is the contract between canopy and the source of any config baseline". Composition isn't special — it's the 7th instance.

ADR-023 was deferred (#488 (closed) closed-deferred); #492 (closed) (Stage-3 promote-live-to-baseline implementation) deferred too. Composition's v1 ships using the existing filesystem rulesets/{juris}/ pattern (Studio writes JSON Patch overrides to DB; "promote" affordance descoped from v1 Studio per #500). This issue defines the broader pattern that future ADRs (revised promote, unified config) compose against.

Context & References

  • ADR-006 (jurisdiction-agnostic rulesets) — filesystem only today
  • ADR-011 (policy-to-rules pipeline) — PAMMS citation overlay
  • ADR-012 (layered YAML configuration) — service-config 3-tier
  • ADR-017 (encrypted secrets at rest) — SOPS overlay
  • ADR-021 (composability runtime + plugin model) — composition runtime
  • ADR-022 (composition override storage layering) — composition overlay
  • Epic &51 (#460) — surfaced this question
  • Deferred: #488 (closed) (ADR-023 promote-PR), #492 (closed) (Stage-3 promote-live-to-baseline)

Labels

type::feature, priority::medium, program::infrastructure, service::shared-crates, workflow::ready