From 5983a007056b0c61b8820f96e47c1be263061322 Mon Sep 17 00:00:00 2001 From: Chris Roadfeldt Date: Tue, 28 Jul 2026 00:26:13 -0500 Subject: [PATCH] =?UTF-8?q?[DCM-02]=20Architecture=20=E2=80=94=20control-p?= =?UTF-8?q?lane,=20convergence,=20runtime,=20persistence,=20topology,=20in?= =?UTF-8?q?tegrations,=20governance,=20credentials/auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Republished from croadfeldt/dcm; see dcm-project/dcm#108. Co-Authored-By: Claude Opus 4.8 --- architecture/00-layering-data-model-vs-dcm.md | 6 + architecture/00-split-manifest.md | 867 +++++++++++ architecture/DCM-Capabilities-Matrix.md | 730 +++++++++ architecture/DISCUSSION-TOPICS.md | 646 ++++++++ architecture/WALKTHROUGH.md | 581 +++++++ architecture/adopted-standards-dcm.md | 94 ++ architecture/control-plane/api-versioning.md | 402 +++++ architecture/control-plane/components.md | 852 +++++++++++ .../control-plane/internal-component-auth.md | 367 +++++ architecture/control-plane/self-health.md | 463 ++++++ .../control-plane/session-revocation.md | 361 +++++ .../dependency-orchestration.md | 348 +++++ architecture/convergence-engine/overview.md | 214 +++ .../convergence-engine/policy-evaluation.md | 278 ++++ .../convergence-engine/recovery-and-retry.md | 453 ++++++ architecture/convergence-engine/scoring.md | 535 +++++++ architecture/core-petal-integration.md | 82 + .../auth-implementation.md | 333 ++++ .../authority-enforcement.md | 376 +++++ .../credentials-and-auth/credentials.md | 464 ++++++ .../credentials-and-auth/provider-callback.md | 420 ++++++ architecture/data-policy-boundary.md | 58 + architecture/dcm-pattern-catalog-overlay.md | 408 +++++ architecture/dcm-platform-requirements.md | 485 ++++++ architecture/dependency-resolution.md | 117 ++ architecture/design-principles.md | 210 +++ .../accreditation-monitor.md | 607 ++++++++ .../contribution-pipeline.md | 454 ++++++ .../governance-enforcement/policy-profiles.md | 1333 +++++++++++++++++ .../registry-enforcement.md | 390 +++++ .../images/core-petal-integration.png | Bin 0 -> 415680 bytes architecture/ingestion/engine.md | 376 +++++ architecture/ingestion/workload-analysis.md | 267 ++++ .../automation-outcome-providers.md | 126 ++ architecture/integrations/itsm.md | 666 ++++++++ .../integrations/kessel-evaluation.md | 472 ++++++ .../integrations/koku-focus-adoption.md | 70 + architecture/layering.md | 166 ++ architecture/operator-perspective.md | 249 +++ architecture/overview.md | 75 + .../persistence/postgres-implementation.md | 542 +++++++ architecture/persistence/postgres-mandate.md | 186 +++ .../runtime-features/deployment-redundancy.md | 797 ++++++++++ .../runtime-features/federation-runtime.md | 761 ++++++++++ .../runtime-features/notifications.md | 601 ++++++++ architecture/runtime-features/scheduling.md | 337 +++++ .../runtime-features/webhooks-messaging.md | 825 ++++++++++ .../topology/canonical-9-layer-hierarchy.md | 346 +++++ .../topology/placement-and-priority-bands.md | 396 +++++ architecture/trust-attestation.md | 50 + architecture/trust-flows.md | 90 ++ architecture/trust-profiles.md | 101 ++ 52 files changed, 20433 insertions(+) create mode 100644 architecture/00-layering-data-model-vs-dcm.md create mode 100644 architecture/00-split-manifest.md create mode 100644 architecture/DCM-Capabilities-Matrix.md create mode 100644 architecture/DISCUSSION-TOPICS.md create mode 100644 architecture/WALKTHROUGH.md create mode 100644 architecture/adopted-standards-dcm.md create mode 100644 architecture/control-plane/api-versioning.md create mode 100644 architecture/control-plane/components.md create mode 100644 architecture/control-plane/internal-component-auth.md create mode 100644 architecture/control-plane/self-health.md create mode 100644 architecture/control-plane/session-revocation.md create mode 100644 architecture/convergence-engine/dependency-orchestration.md create mode 100644 architecture/convergence-engine/overview.md create mode 100644 architecture/convergence-engine/policy-evaluation.md create mode 100644 architecture/convergence-engine/recovery-and-retry.md create mode 100644 architecture/convergence-engine/scoring.md create mode 100644 architecture/core-petal-integration.md create mode 100644 architecture/credentials-and-auth/auth-implementation.md create mode 100644 architecture/credentials-and-auth/authority-enforcement.md create mode 100644 architecture/credentials-and-auth/credentials.md create mode 100644 architecture/credentials-and-auth/provider-callback.md create mode 100644 architecture/data-policy-boundary.md create mode 100644 architecture/dcm-pattern-catalog-overlay.md create mode 100644 architecture/dcm-platform-requirements.md create mode 100644 architecture/dependency-resolution.md create mode 100644 architecture/design-principles.md create mode 100644 architecture/governance-enforcement/accreditation-monitor.md create mode 100644 architecture/governance-enforcement/contribution-pipeline.md create mode 100644 architecture/governance-enforcement/policy-profiles.md create mode 100644 architecture/governance-enforcement/registry-enforcement.md create mode 100644 architecture/images/core-petal-integration.png create mode 100644 architecture/ingestion/engine.md create mode 100644 architecture/ingestion/workload-analysis.md create mode 100644 architecture/integrations/automation-outcome-providers.md create mode 100644 architecture/integrations/itsm.md create mode 100644 architecture/integrations/kessel-evaluation.md create mode 100644 architecture/integrations/koku-focus-adoption.md create mode 100644 architecture/layering.md create mode 100644 architecture/operator-perspective.md create mode 100644 architecture/overview.md create mode 100644 architecture/persistence/postgres-implementation.md create mode 100644 architecture/persistence/postgres-mandate.md create mode 100644 architecture/runtime-features/deployment-redundancy.md create mode 100644 architecture/runtime-features/federation-runtime.md create mode 100644 architecture/runtime-features/notifications.md create mode 100644 architecture/runtime-features/scheduling.md create mode 100644 architecture/runtime-features/webhooks-messaging.md create mode 100644 architecture/topology/canonical-9-layer-hierarchy.md create mode 100644 architecture/topology/placement-and-priority-bands.md create mode 100644 architecture/trust-attestation.md create mode 100644 architecture/trust-flows.md create mode 100644 architecture/trust-profiles.md diff --git a/architecture/00-layering-data-model-vs-dcm.md b/architecture/00-layering-data-model-vs-dcm.md new file mode 100644 index 0000000..c5e27b3 --- /dev/null +++ b/architecture/00-layering-data-model-vs-dcm.md @@ -0,0 +1,6 @@ +# Layering — Data Model vs DCM (superseded) + +**Superseded 2026-07-23 by [`layering.md`](layering.md)**, which restates this boundary in +current vocabulary (UDLM substrate vs DCM realization) and carries the deferred Higher-Order +Model rationale. This file remains as a filename target for the historical +[`00-split-manifest.md`](00-split-manifest.md); do not extend it. diff --git a/architecture/00-split-manifest.md b/architecture/00-split-manifest.md new file mode 100644 index 0000000..2322993 --- /dev/null +++ b/architecture/00-split-manifest.md @@ -0,0 +1,867 @@ +--- +status: ✅ Decisions locked — ready for execution +created: 2026-05-26 +purpose: Plan and record the split of architecture/data-model/ into two repos (udlm + dcm) +permanence: Kept in dcm as a permanent contextual artifact (helps future contributors understand the boundary) +--- + +# UDLM / DCM Split Manifest + +This document is the **plan of record** for splitting the existing +`architecture/data-model/` directory into two independent repos: + +- **`udlm`** — Universal Data Lifecycle Model (substrate) — `github.com/croadfeldt/udlm` +- **`dcm`** — Data Center Management (operational platform built on udlm) — this repo + +This document is kept in dcm as a **permanent contextual artifact**. It captures +the rationale and boundary work that drove the split; future contributors can +read it to understand why files live where they do and which decisions are +load-bearing. + +Companion doc: [`00-layering-data-model-vs-dcm.md`](00-layering-data-model-vs-dcm.md) +— captures the conceptual layering that justifies this split. (Held uncommitted +until the split lands so its repo references can be made concrete.) + +--- + +## The boundary rule (recap) + +For each file or section, the test is: + +> *"Could a peer of DCM, built independently, choose to do this differently and +> still be a valid realization of the same data?"* + +- **Yes →** belongs in **dcm** (it's an implementation choice) +- **No, it would break interop or invalidate the data →** belongs in **udlm** + (it's a substrate invariant) + +**udlm owns** entity types, four states + transitions + invariants, contracts +(provider, policy, event payloads, data store), provenance, identity, reference +taxonomies. The state vocabulary lives here because peers must share it to +interoperate. + +**dcm owns** the convergence engine, runtime/orchestration, deployment topology, +monitors, integrations with specific external systems, ease-of-use packaging, +implementation specifics. + +### Compatibility model (LOCKED) + +**udlm enforces wire-level compatibility at the data/event/contract boundary; +it does not enforce implementation portability.** + +Concretely: +- Any system conformant to udlm version X produces data that any other system + conformant to the same major version of udlm can read, interpret, and + exchange — **versioning applicability rules withstanding**. +- Federation between peers is **literal interop**, not "architecturally similar + systems requiring adapters." +- A peer realization's storage, internal APIs, control-plane components, and + runtime mechanics are NOT constrained by udlm — those are dcm-layer choices. + +**Implications for udlm spec authoring:** +1. Wire formats are **normative** (identifier strings, timestamps, event payloads, error envelopes). +2. Error/code/state vocabularies that cross interop boundaries are **closed**. +3. udlm **must** define a schema-sharing mechanism so peers can exchange schemas + for their custom types and resolve each other's data with context. +4. Versioning is a first-class concern — every wire contract carries a version + and a compatibility window. + +This position is the K8s precedent: K8s API + CRDs are wire-compatible across +distributions; controllers are not portable. We are in the same shape. + +### Sidebar — the validating analogy + +A useful test (user-provided, captured here for future contributors): + +| Analogy | Layer | +|---|---| +| **Directions** — where you can go, what destinations exist | udlm | +| **Goals** for the rules of the road (safety, predictability, interop) | udlm | +| **Rules-of-the-road requirements** (what cars + drivers must satisfy) | udlm | +| **Driver requirements** (license classes, competencies) | udlm | +| **Published rules-of-the-road manual** (RFCs, NIST cited as substrate) | udlm | +| **The road itself** (control-plane components, persistence) | dcm | +| **Turn signals** (the physical lights and signaling infrastructure) | dcm | +| **Cars actually driving** (convergence engine, the intent→realized loop) | dcm | +| **Actual rules of the road** (specific enforcement, matrix evaluator) | dcm | +| **DMV licensing process** (profile thresholds, approval enforcement, GitOps PR) | dcm | + +The analogy stress-tests classifications. When unsure: "is this a *direction +or requirement* (udlm) or *infrastructure or enforcement* (dcm)?" + +--- + +## File-level classification (61 files total) + +Counts: **udlm 22 / dcm 18 / both 21** = 61. (Plus 7 net-new udlm contract docs +authored during the split — see "Newly identified udlm contracts" below.) + +### Pure udlm (22 files) — move as-is + +``` +00-context-and-purpose.md 12-audit-provenance-observability.md +00-foundations.md 15-universal-groups.md +01-entity-types.md 16-universal-audit.md +02-four-states.md 30-composite-service-model.md +03-layering-and-versioning.md 33-event-catalog.md +04b-ownership-sharing-allocation.md 50-subscription-lifecycle.md +05-resource-type-hierarchy.md 52-test-framework-specification.md +07-service-dependencies.md A-provider-contract.md +08-resource-grouping.md B-policy-contract.md +09-entity-relationships.md +10-information-providers.md +11-data-store-contracts.md +11-storage-providers.md +``` + +### Pure dcm (18 files) — move as-is + +``` +14-policy-profiles.md 39-dcm-self-health.md +17-deployment-redundancy.md 41-operational-reference.md +18-webhooks-messaging.md 42-itsm-integration.md +22-dcm-federation.md 44-kessel-integration-evaluation.md +23-notification-model.md 45-consistency-review.md +25-control-plane-components.md 46-workload-analysis.md +29-scoring-model.md 47-accreditation-monitor.md +34-api-versioning-strategy.md 49-implementation-specifications.md +35-session-revocation.md +36-internal-component-auth.md +``` + +### Needs per-section split (21 files) — see detail below + +``` +00-design-priorities.md 31-credential-management.md +04-examples.md 31-credential-provider-model.md +06-resource-service-entities.md 32-authority-tier-model.md +13-ingestion-model.md 37-scheduled-requests.md +19-auth-providers.md 38-request-dependency-graph.md +20-registry-governance.md 40-standards-catalog.md +21-information-providers-advanced.md 43-provider-callback-auth.md +24-operational-models.md 48-location-topology-layers.md +26-accreditation-and-authorization-matrix.md 51-infrastructure-optimization.md +27-governance-matrix.md 53-capability-discovery.md +28-federated-contribution-model.md +``` + +--- + +## Proposed `udlm` repo layout (LOCKED — numeric prefixes dropped) + +``` +udlm/ +├── README.md # what udlm is, who consumes it, how to extend +├── CONFORMANCE.md # NEW — what a conformant realization must provide (wire contract surface) +├── foundations/ +│ ├── context-and-purpose.md +│ ├── foundations.md +│ ├── entity-types.md +│ ├── four-states.md +│ ├── layering-and-versioning.md +│ ├── examples.md # ALL examples (kept clean — see resolved #1) +│ └── ownership-sharing-allocation.md +├── entities/ +│ ├── resource-type-hierarchy.md +│ ├── resource-service-entities.md # udlm portion +│ ├── service-dependencies.md +│ ├── resource-grouping.md +│ ├── entity-relationships.md +│ └── composite-service-model.md +├── contracts/ +│ ├── provider-contract.md # was A- +│ ├── policy-contract.md # was B- +│ ├── information-providers.md +│ ├── data-store-contracts.md +│ ├── storage-providers.md +│ ├── information-providers-advanced.md # udlm portion +│ ├── event-catalog.md +│ ├── provider-callback-auth.md # udlm portion (mechanism-neutral two-layer abstract — see resolved #3) +│ ├── capability-discovery.md # udlm portion +│ ├── identifier-scheme.md # NEW +│ ├── time-and-clock.md # NEW +│ ├── error-model.md # NEW +│ ├── retry-semantics.md # NEW (extract from 24/25/recovery profiles) +│ ├── rate-limit-and-backpressure.md # NEW (extract from 49) +│ └── schema-sharing.md # NEW (peer schema exchange — required by wire-compat) +├── lifecycle/ +│ ├── ingestion-model.md # udlm portion +│ ├── operational-models.md # udlm portion (timeouts, cancellation, orphan contracts) +│ ├── scheduled-requests.md # udlm portion +│ ├── request-dependency-graph.md # udlm portion +│ └── subscription-lifecycle.md +├── governance/ +│ ├── auth-providers.md # udlm portion (auth mode taxonomy) +│ ├── registry-governance.md # udlm portion +│ ├── accreditation-and-authorization-matrix.md # udlm portion +│ ├── governance-matrix.md # udlm portion +│ ├── federated-contribution-model.md # udlm portion +│ ├── credentials.md # NEW — merged 31a + 31b (see resolved #6) +│ └── authority-tier-model.md # udlm portion +├── observability/ +│ ├── audit-provenance-observability.md +│ ├── universal-groups.md +│ └── universal-audit.md +├── topology/ +│ └── location-topology-layers.md # udlm portion: layered-topology contract + assembly rules + lifecycle ONLY (specific 9-layer hierarchy moved to dcm — see resolved #4) +├── design-principles/ +│ ├── design-priorities.md # udlm portion (the four principles as contracts) +│ └── infrastructure-optimization.md # udlm portion (data-contract principle + four domains; PostgreSQL mandate moved to dcm — see resolved #5) +├── reference/ +│ └── standards-catalog.md # udlm portion (the normative external standards list) +├── docs/ +│ └── consumer-perspective.md # NEW — the "driver's handbook" (narrative perspective for consumers) +└── tests/ + └── test-framework-specification.md +``` + +**Notes on the layout:** +- Numeric prefixes dropped. Reading order is conveyed by README + section names. +- `CONFORMANCE.md` at top-level: defines what any peer realization must provide + to be wire-compatible. This is the conformance surface DAV will validate + against. +- `contracts/` is the wire-compatibility surface — every doc here is normative. +- `docs/` is the single narrative directory (no separate `guides/`). +- udlm has no `deployment/`, no `mcp-servers/`, no API surface — it's pure + specification. + +--- + +## Proposed `dcm` repo layout + +dcm gets a layout shaped around its concerns, not mirroring udlm: + +``` +dcm/ +├── README.md # what dcm is, links to udlm +├── architecture/ +│ ├── overview.md # links to udlm as substrate +│ ├── layering.md # adapted from 00-layering-data-model-vs-dcm.md +│ ├── control-plane/ +│ │ ├── components.md # ← 25-control-plane-components +│ │ ├── self-health.md # ← 39-dcm-self-health +│ │ ├── internal-component-auth.md # ← 36 +│ │ ├── session-revocation.md # ← 35 +│ │ └── api-versioning.md # ← 34 +│ ├── convergence-engine/ +│ │ ├── overview.md # new — the intent → realized loop +│ │ ├── policy-evaluation.md # ← 14, dcm parts of 27 +│ │ ├── scoring.md # ← 29 +│ │ ├── recovery-and-retry.md # ← dcm parts of 24 +│ │ └── dependency-orchestration.md # ← dcm parts of 38 +│ ├── ingestion/ +│ │ ├── engine.md # ← dcm parts of 13 +│ │ └── workload-analysis.md # ← 46 +│ ├── credentials-and-auth/ +│ │ ├── auth-implementation.md # ← dcm parts of 19 +│ │ ├── credentials.md # ← dcm parts of 31a + 31b (CONSOLIDATED) +│ │ ├── provider-callback.md # ← dcm parts of 43 (mTLS + interaction credential mechanism) +│ │ └── authority-enforcement.md # ← dcm parts of 32 +│ ├── governance-enforcement/ +│ │ │ # NOTE: matrix-evaluator content merged into convergence-engine/policy-evaluation.md (← dcm parts of 27) +│ │ ├── accreditation-monitor.md # ← 47, dcm parts of 26 +│ │ ├── registry-enforcement.md # ← dcm parts of 20 +│ │ ├── contribution-pipeline.md # ← dcm parts of 28 +│ │ └── policy-profiles.md # ← 14 +│ ├── runtime-features/ +│ │ ├── scheduling.md # ← dcm parts of 37 +│ │ ├── notifications.md # ← 23 +│ │ ├── webhooks-messaging.md # ← 18 +│ │ └── federation-runtime.md # ← 22 +│ ├── topology/ +│ │ ├── canonical-9-layer-hierarchy.md # ← MOVED from udlm 48 (Country → ... → Unit as DCM's canonical default) +│ │ └── placement-and-priority-bands.md # ← dcm parts of 48 +│ ├── persistence/ +│ │ ├── postgres-mandate.md # ← MOVED from udlm 51 (single-required-infrastructure decision) +│ │ └── postgres-implementation.md # ← dcm parts of 51 +│ ├── integrations/ +│ │ ├── itsm.md # ← 42 +│ │ └── kessel-evaluation.md # ← 44 +│ ├── design-principles.md # ← dcm parts of 00-design-priorities +│ ├── operator-perspective.md # NEW — how to operationalize udlm (companion to udlm's consumer-perspective) +│ └── consistency-review.md # ← 45 (meta doc; archived 2026-07-23 → docs/archive/) +├── deployment/ # (existing) +├── requirements/ +│ └── dcm-platform-requirements.md +├── examples/ +│ └── three-tier-application.md # ← dcm parts of 04-examples (orchestration scenarios) +└── reference/ + └── implementation-standards.md # ← dcm parts of 40-standards-catalog +``` + +**Notes on the dcm layout:** +- Organized by **architectural concern**, not by file number. Numbers were + scaffolding; the split is the moment to drop them. +- Every dcm spec opens with: `> Implements contracts defined in udlm: [link]` +- The `convergence-engine/` group is new — it's the heart of dcm and currently + scattered across 24, 27, 38, 14. Worth consolidating. +- `runtime-features/` is the catch-all for "things dcm does that aren't core + convergence": scheduling, notifications, federation, webhooks. + +--- + +## Per-section split for the 21 "both" files + +For each file: which sections go to udlm, which to dcm, and how the dcm doc +references the udlm doc. + +> **Note on paths**: the path references in the per-section blocks below use +> the **legacy numeric layout** (e.g., `udlm/40-governance/27-...md`). The final +> layout drops numeric prefixes per the LOCKED udlm layout above. Mapping is +> mechanical: drop the numeric directory prefix, drop the numeric file prefix. +> Example: `udlm/40-governance/27-governance-matrix.md` → +> `udlm/governance/governance-matrix.md`. The execution phase will apply this +> rename uniformly. + +### 1. `00-design-priorities.md` + +**udlm sections** → `udlm/70-design-principles/00-design-priorities.md` +- *Design Principles as Interoperability Substrate* — Four invariant principles (consumer sovereignty, zero trust, federation, policy as code) form the contract foundation any realization must honor. +- *Authority Tiers (model definition)* — Ordered decision authority vocabulary (auto, reviewed, verified, authorized) plus custom extensions. +- *Profile Scaling Model (definition)* — Named profiles (homelab, dev, standard, prod, fsi, sovereign) with constraint matrices. + +**dcm sections** → `dcm/architecture/design-principles.md` +- *Design Priorities: Implementation Choices* — Specific trade-offs (latency vs governance rigor, velocity vs stability). +- *Approval Tier Model (runtime enforcement)* — Tier-to-capability mappings and enforcement gates. +- *Profile-Governed System Constraints* — Per-profile enforcement of limits and validation modes. +- *Policy as Code Requirement* — Integration with external policy engines (OPA, etc.) and audit logging. +- *Documentation Discipline Requirements* — Internal governance for document lifecycle (status badges, related-doc links). + +**Cross-ref:** dcm doc opens with: *"Implements the design principles defined in [udlm/70-design-principles/00-design-priorities.md]."* + +--- + +### 2. `04-examples.md` — **resolved: all examples → udlm, separate dcm doc** + +**udlm sections** → `udlm/00-foundations/04-examples.md` (kept clean — all examples) +- *VM Provisioning Example (basic intent-to-realized lifecycle)* +- *IP Allocation Example (allocation ownership)* +- *VLAN Attachment Example (cross-entity coordination)* +- *Brownfield Ingestion Example* +- *Drift Detection Example* + + Each example is rewritten to stay contract-level — describe what happens at + each state without orchestration mechanics. Any paragraphs that wandered into + "DCM does X" get dropped (those scenarios re-surface in the dcm examples doc). + +**dcm sections** → `dcm/examples/orchestration-scenarios.md` (new — composed scenarios) +- *Three-Tier Application Example (full dependency group orchestration)* +- *VM Provisioning with timeout/cancellation propagation* +- *IP Allocation with provider's internal lifecycle reconciliation* +- New scenarios specific to dcm features (retry, scoring-driven placement, etc.) + +**Cross-ref:** dcm doc opens with: *"Builds on the canonical examples in [udlm/00-foundations/04-examples.md] to illustrate dcm-specific orchestration features."* + +**Resolution:** Originally proposed per-paragraph splitting; analogy made the +cleaner answer obvious — keep all udlm examples as clean contract illustrations, +let dcm write its own orchestration-scenarios doc fresh. + +--- + +### 3. `06-resource-service-entities.md` + +**udlm sections** → `udlm/10-entities/06-resource-service-entities.md` +- *Resource/Service Request vs Entity (fundamental distinction)* +- *Ownership Models (allocation, whole_allocation, full_transfer, hybrid_transfer)* +- *Entity Lifecycle (provider-side: requested, creating, created, deleting, deleted)* +- *Provider Internal Lifecycle and Notification Model* — what events providers emit and what fields they may update. + +**dcm sections** → distributed across `dcm/architecture/convergence-engine/` and `dcm/architecture/control-plane/` +- *Request/Entity Relationship Management* — operational tracking → `convergence-engine/` +- *Ownership Model Enforcement at Dispatch* — dispatcher logic → `convergence-engine/` +- *Provider Notification Consumption* — receipt, validation, reconciliation → `runtime-features/` or `convergence-engine/` +- *Entity Lifecycle Monitoring* — polling/webhook detection → `runtime-features/` + +**Cross-ref:** dcm convergence engine doc cites this udlm doc as the authoritative entity-lifecycle contract. + +--- + +### 4. `13-ingestion-model.md` + +**udlm sections** → `udlm/30-lifecycle/13-ingestion-model.md` +- *Brownfield Ingestion Problem Statement and Flow* +- *Enrichment Stages (discovery, enrichment, readiness)* +- *Transitional Tenant Mechanism* +- *Auto-Assignment Signals* — the contract; rules of which signals exist. +- *Ingestion Lifecycle (states: discovered, enriching, ready, ingested)* + +**dcm sections** → `dcm/architecture/ingestion/engine.md` +- *Ingestion Engine Implementation* +- *Information Provider Integration (polling/webhook orchestration)* +- *Enrichment Policy Enforcement (specific rules, profile-driven)* +- *Transitional Tenant and Auto-Assignment Execution* +- *Ingestion Scheduling* + +**Cross-ref:** dcm engine doc opens: *"Realizes the ingestion contract defined in [udlm/30-lifecycle/13-ingestion-model.md]."* + +--- + +### 5. `19-auth-providers.md` + +**udlm sections** → `udlm/40-governance/19-auth-providers.md` +- *Authentication Modes (built-in, GitHub/GitLab OAuth, LDAP, AD, OIDC, mTLS)* — taxonomy of supported mechanisms. +- *Multiple Provider Authentication* — multi-provider routing as a contract. +- *Credential Types and Issuance* — data model only. + +**dcm sections** → `dcm/architecture/credentials-and-auth/auth-implementation.md` +- *Authentication Implementation within DCM* — library choices, integration mechanics. +- *Credential Management Service Integration* +- *Provider Authentication Routing Logic* +- *Session Management and Token Lifecycle* + +**Cross-ref:** dcm doc cites udlm/40-governance/19-auth-providers.md as the mode-taxonomy contract. + +--- + +### 6. `20-registry-governance.md` + +**udlm sections** → `udlm/40-governance/20-registry-governance.md` +- *Three-Tier Registry Model (submission, review, publication)* +- *Proposal/Review/Publication Workflow* — the artifact lifecycle contract. +- *Versioning and Deprecation Lifecycle* +- *Resource Type Registry (standard type definitions + extension contract)* + +**dcm sections** → `dcm/architecture/governance-enforcement/registry-enforcement.md` +- *Registry Governance Enforcement* — operational enforcement of the three-tier workflow. +- *Provider Selection Tie-Breaking* — selection algorithm when multiple providers match. +- *Artifact Lifecycle Management* — storage, versioning, deprecation warnings. +- *Review Queue and Approval Workflow* — review-tier mechanics. + +**Cross-ref:** dcm doc: *"Enforces the registry contract in [udlm/40-governance/20-registry-governance.md]."* + +--- + +### 7. `21-information-providers-advanced.md` + +**udlm sections** → `udlm/20-contracts/21-information-providers-advanced.md` +- *Confidence Scoring and Hybrid Descriptor Model* +- *Authority and Priority Declarations* +- *Schema Versioning for Providers* +- *Well-Known Provider Registry (the contract for canonical providers)* + +**dcm sections** → fold into `dcm/architecture/ingestion/engine.md` +- *Ingestion-Time Conflict Detection and Resolution* +- *Write-Back Capability Implementation* +- *Air-Gapped Verification Model* +- *Provider Priority and Fallback Logic* + +**Cross-ref:** dcm ingestion engine doc cites this udlm doc as the trust/authority contract. + +--- + +### 8. `24-operational-models.md` + +**udlm sections** → `udlm/30-lifecycle/24-operational-models.md` +- *Timeout Model and State Machine* — deadline contract. +- *Cancellation Request and Propagation Model* — cancel state contract. +- *Orphan Detection and Prevention* — accountability contract. +- *Discovery Scheduling and Continuous Reconciliation* — reality-vs-intent contract. +- *Recovery Policy Model* — failure semantics contracts. +- *Compensation* — rollback contract. + +**dcm sections** → `dcm/architecture/convergence-engine/recovery-and-retry.md` +- *Timeout Enforcement Mechanisms* +- *Cancellation Execution and Cleanup* +- *Orphan Detection Implementation* +- *Discovery Job Scheduling and Execution* +- *Recovery Policy Evaluation* +- *Compensation Execution* + +**Cross-ref:** dcm doc: *"Implements the operational contracts in [udlm/30-lifecycle/24-operational-models.md]."* + +--- + +### 9. `26-accreditation-and-authorization-matrix.md` + +**udlm sections** → `udlm/40-governance/26-accreditation-and-authorization-matrix.md` +- *Data Classification Levels (restricted, sensitive, internal, public)* +- *Accreditation Model and Lifecycle* +- *Accreditation Gap Handling (policy options)* +- *Authorization Matrix (data/capability × subject/context)* +- *Zero Trust Interaction Model (five-check boundary)* +- *Federation Tunnel Model (the contract for secure inter-DCM channels)* + +**dcm sections** → split between `dcm/architecture/governance-enforcement/accreditation-monitor.md` and `dcm/architecture/runtime-features/federation-runtime.md` +- *Accreditation Governance Enforcement* → accreditation-monitor +- *Authorization Evaluation at Runtime* → governance-enforcement +- *Zero Trust Boundary Implementation* → spans multiple dcm areas +- *Federation Tunnel Establishment and Maintenance* → federation-runtime +- *Profile-Governed Accreditation Constraints* → governance-enforcement + +**Cross-ref:** all dcm fragments cite this udlm doc as the security-substrate contract. + +--- + +### 10. `27-governance-matrix.md` + +**udlm sections** → `udlm/40-governance/27-governance-matrix.md` +- *Unified Governance Matrix as Single Enforcement Point* (the architectural invariant) +- *Matrix Four Axes (Subject/Data/Target/Context)* +- *Rule Structure and Decision Vocabulary (ALLOW, DENY, STRIP_FIELD, REDACT, AUDIT_ONLY)* +- *Soft vs Hard Enforcement* (the distinction as a contract) +- *Field-Level Controls* (the granular policy contract) + +**dcm sections** → `dcm/architecture/convergence-engine/policy-evaluation.md` (matrix-evaluator content merged here; no separate matrix-evaluator.md) +- *Evaluation Algorithm* +- *Hard Enforcement Mechanics* +- *Soft Enforcement Execution* +- *Sovereignty Zone Management* +- *Profile-Governed Policy Configurations* +- *Policy Caching and Invalidation* + +**Cross-ref:** dcm doc: *"Implements the governance matrix contract in [udlm/40-governance/27-governance-matrix.md]."* + +--- + +### 11. `28-federated-contribution-model.md` — **resolved by wire-compat decision** + +**udlm sections** → `udlm/40-governance/28-federated-contribution-model.md` +- *Four Contributor Types (Platform Admin, Consumer/Tenant, Service Provider, Peer DCM)* — federated actor taxonomy. +- *Contribution Artifact Types (resource types, policies, profiles, accreditations, locations, credentials, provider definitions)* +- *Universal Contribution Pipeline (submission → review → publication)* — governance invariant. +- *Consumer/Provider/Federation Contribution Models* — what each contributor type may contribute (this is contract: who may write what). +- *Artifact Lifecycle and Versioning* + +**dcm sections** → `dcm/architecture/governance-enforcement/contribution-pipeline.md` +- *Contribution Store Structure* +- *Review Queue and Approval Workflow* (GitOps PR mechanics live here) +- *Contribution Pipeline Orchestration* +- *Consumer Contribution Enforcement* +- *Provider Contribution Integration* +- *Federation Contribution Synchronization* + +**Cross-ref:** dcm doc: *"Operationalizes the contribution contract in [udlm/40-governance/28-federated-contribution-model.md] via a GitOps-style PR workflow."* + +**Note:** The GitOps-specific bits (PR review, branch protection rules) are a dcm choice. A peer of dcm could use a different review channel. + +--- + +### 12 + 13. `31-credential-management.md` + `31-credential-provider-model.md` — **resolved: merged on both sides** + +**udlm sections** → `udlm/governance/credentials.md` (one consolidated doc) +- *Credential Scope (DCM-internal vs consumer-facing)* +- *Credential Types (api_key, JWT, mTLS cert, SSH key, secret, signing key, HSM-backed, dcm_interaction)* — full taxonomy +- *Credential Lifecycle (issuance, active, rotation, revocation, expired)* +- *Rotation Protocol (parallel validity windows)* — contract +- *Revocation Model and Propagation* — contract +- *Consumer Credential Delivery* — the contract, not the mechanism +- *Provider API Contract for Credentials* — how providers accept credentials +- *Cryptographic Requirements* (defers to standards catalog) +- *Registration and Profile-Governed Configuration* — the constraint vocabulary + +**dcm sections** → `dcm/architecture/credentials-and-auth/credentials.md` (one consolidated doc) +- *Credential Storage and Access Control* +- *Credential Generation Implementation* +- *Issuance Flow Orchestration* +- *Rotation Job Scheduling and Execution* +- *Revocation Enforcement Across Providers* +- *Consumer Delivery Mechanics* +- *Provider Authentication Validation* +- *Profile-Governed Constraints (enforcement)* +- *Integration with External Services* + +**Cross-ref:** dcm doc opens: *"Implements the credential contracts in [udlm/governance/credentials.md]."* + +**Resolution:** The historical 31a/31b split (management vs provider-model) was confusing — heavy overlap, unclear boundary. Wire-compat decision forced consolidation: peers must agree on a single credential model for interop. + +--- + +### 14. `32-authority-tier-model.md` + +**udlm sections** → `udlm/40-governance/32-authority-tier-model.md` +- *Core Authority Tier Model (auto, reviewed, verified, authorized)* +- *Decision Gravity Vocabulary* — what decisions require what tiers. +- *Custom Tier Definition and Contribution* — extension contract. +- *Tier Registry Change Impact Detection with Degradation Review Gate* — approval continuity contract. + +**dcm sections** → `dcm/architecture/credentials-and-auth/authority-enforcement.md` +- *Tier Evaluation Algorithm* +- *Approval Authority Mapping* +- *Profile Threshold Configuration* +- *DCMGroup Assignment* +- *Tier Enforcement at Decision Points* +- *Degradation Review Orchestration* + +**Cross-ref:** dcm doc: *"Enforces the authority tier contract in [udlm/40-governance/32-authority-tier-model.md]."* + +--- + +### 15. `37-scheduled-requests.md` + +**udlm sections** → `udlm/30-lifecycle/37-scheduled-requests.md` +- *Scheduling Model (immediate, at, window, recurring)* — deferral contract. +- *Request State During Deferral (SCHEDULED state, paused at ACKNOWLEDGED)* +- *Maintenance Windows* — coordination contract. +- *Deadline Enforcement* — scheduling deadline contract. + +**dcm sections** → `dcm/architecture/runtime-features/scheduling.md` +- *Request Scheduler Component* +- *Deferred Request Lifecycle Management* +- *Maintenance Window Scheduling Logic* +- *Deadline Evaluation and Timeout Enforcement* +- *Consumer API Additions* (endpoints) +- *New Events* (request.scheduled, request.activation_pending, etc.) +- *Profile-Governed Scheduling Constraints* + +**Cross-ref:** dcm doc: *"Implements the scheduling contract in [udlm/30-lifecycle/37-scheduled-requests.md]."* + +--- + +### 16. `38-request-dependency-graph.md` + +**udlm sections** → `udlm/30-lifecycle/38-request-dependency-graph.md` +- *Request Dependency Group Structure* (group_uuid, group_handle, member set) +- *wait_for Values (acknowledged, approved, dispatched, realized)* — activation contract. +- *Field Injection Mechanism* — propagation contract. +- *PENDING_DEPENDENCY Status* — blocked-state contract. +- *Failure Handling (on_failure: cancel_remaining | continue)* — propagation policy contract. +- *Group Timeout* — group-level deadline contract. +- *Relationship to composite service definitions* — scoping guidance. + +**dcm sections** → `dcm/architecture/convergence-engine/dependency-orchestration.md` +- *Request Dependency Graph Submission and Parsing* +- *Dependency Resolution and Dispatch Orchestration* +- *PENDING_DEPENDENCY State Lifecycle* +- *Failure Handling Execution* +- *Group Timeout Enforcement* +- *Consumer API Endpoints* +- *New Events* +- *Profile-Governed Constraints* + +**Cross-ref:** dcm doc cites this udlm doc as the multi-request coordination contract. + +**Note:** Agent's output referenced `30-meta-provider-model.md` which doesn't exist — actual file is `30-composite-service-model.md`. Reference is to **composite service model**, which is pre-defined ordering vs ad-hoc dependency groups. + +--- + +### 17. `40-standards-catalog.md` + +**udlm sections** → `udlm/90-reference/40-standards-catalog.md` +- All six normative standards groups (identity/access, auth protocols, crypto, data model/serialization, operational, compliance) — these are external standards we cite as substrate requirements. + +**dcm sections** → `dcm/reference/implementation-standards.md` +- *Cryptographic Implementation Details* (which algorithms chosen) +- *Certificate and Key Management Procedures* +- *Authentication Protocol Integration* (which OAuth/OIDC/LDAP impl) +- *OpenAPI Implementation* (endpoint design choices) +- *Observability Implementation* (Prometheus/OTel choices) +- *Kubernetes Integration* +- *Compliance Configuration* (which standards enforced per profile) + +**Cross-ref:** dcm doc: *"Selects implementations of the standards listed in [udlm/90-reference/40-standards-catalog.md]."* + +**Note:** udlm portion is reference-only (citations, not new substrate). Still belongs in udlm because peers consuming udlm need to know what standards apply. + +--- + +### 18. `43-provider-callback-auth.md` — **resolved: udlm contract is mechanism-neutral** + +**udlm sections** → `udlm/contracts/provider-callback-auth.md` (mechanism-neutral two-layer contract) +- *Two-Layer Authentication Contract* — abstract: any callback MUST be validated via two independent identity factors. Specific mechanisms (mTLS, JWT, signed assertions, etc.) are realization choices declared via schema-sharing. +- *Provider Identity Attestation Contract* — peers MUST attest provider identity at registration via a verifiable mechanism (the verification approach is realization-declared). +- *Callback Credential Lifecycle* — issuance, active, rotation, revocation states (technology-neutral). +- *Authentication-at-Callback-Time Contract* — every callback MUST present both factors; the receiving peer MUST validate both before accepting. +- *Entity-Level Authorization Contract* — credentials are scoped; peer MUST verify the provider's authorization to update the target entity. +- *Bootstrap Contract* — initial registration requires an authenticated single-use token; mechanism is realization-declared. +- *Credential Revocation Contract* — revocation is immediate; peers MUST recognize and reject revoked credentials. + +**dcm sections** → `dcm/architecture/credentials-and-auth/provider-callback.md` (DCM's specific mechanism: mTLS + interaction credential) +- *mTLS as Layer 1 — DCM's identity-attestation mechanism* +- *Interaction Credential as Layer 2 — DCM's credential mechanism* +- *Provider Certificate Storage and Validation* +- *Interaction Credential Issuance and Management* +- *mTLS Enforcement at Callback Endpoint* +- *Credential Validation Logic at Callback Time* +- *Entity Authorization Checks* +- *Registration Token Generation and Validation* +- *Revocation Enforcement* +- *Emergency Revocation Response* + +**Cross-ref:** dcm doc opens: *"Realizes the two-layer auth contract in [udlm/contracts/provider-callback-auth.md] using mTLS + interaction credential. The specific mechanism is declared in DCM's schema bundle per [udlm/contracts/schema-sharing.md]."* + +**Resolution:** Per user direction — udlm defines abstract contract; dcm picks specific mechanism. Peer realizations declare their chosen mechanism via the schema-sharing protocol so federation peers can interoperate. + +--- + +### 19. `48-location-topology-layers.md` — **resolved: tighten udlm to contract-only; hierarchy moves to dcm** + +**udlm sections** → `udlm/60-topology/48-location-topology-layers.md` (contract only) +- *Layered-Topology Contract* — "topology consists of layers; layers have parent/child relationships; layers carry typed fields" (abstract, not the specific 9 layers) +- *Location Layer Instance Format* — the data structure for representing a layer instance +- *Hierarchy Assembly Rules* — what makes a parent/child relationship valid (contract, not the specific tree) +- *Location Layer Lifecycle (active, deprecated, decommissioned)* +- *Custom/Extension Mechanism* — how new layer types are added + +**dcm sections** → split between: +1. `dcm/architecture/topology/canonical-9-layer-hierarchy.md` — the specific Country → Region → Zone → Site → Data Center → Hall → Cage → Rack → Unit hierarchy (DCM's canonical default; a peer realization could pick differently) +2. `dcm/architecture/topology/placement-and-priority-bands.md` + - *Location Topology Database and Query Interface* + - *Priority Band Allocation* (premium/standard/budget) + - *Consumer Selection Model* (preference matching) + - *Authority and Ownership Model* + - *Relationship to Placement Engine* + - *Location Layer Lifecycle Management* (operational draining, re-placement) + - *Profile-Governed Topology Constraints* + +**Cross-ref:** dcm hierarchy doc opens: *"Realizes the layered-topology contract in [udlm/60-topology/48-location-topology-layers.md] with DCM's canonical 9-layer scheme."* + +**Resolution:** Analogy made the call clear — "addresses are layered" is rule of the road; "the layers are Country, Region, Zone..." is the specific addressing scheme this jurisdiction picked. + +--- + +### 20. `51-infrastructure-optimization.md` — **resolved: PostgreSQL mandate moves to dcm** + +**udlm sections** → `udlm/70-design-principles/51-infrastructure-optimization.md` +- *Data Contracts vs Abstraction Layers* (the principle — no abstraction-hiding allowed) +- *Four Data Domains (Intent, Requested, Realized, Discovered)* — restatement as foundational domains. +- *Mandatory Persistence Requirement* — the contract that all four domains must be persistently queryable. **Note: persistence is required; the technology is not specified here.** + +**dcm sections** → split between: +1. `dcm/architecture/persistence/postgres-mandate.md` — the decision that this dcm realization mandates PostgreSQL (a dcm-level architectural choice; a peer realization could pick differently while honoring the udlm persistence contract) +2. `dcm/architecture/persistence/postgres-implementation.md` + - *Enforcement Mechanisms for Required Infrastructure* + - *Data Domain Implementation Details* (table structures, schema) + - *Query Optimization and Indexing* + - *Data Retention and Archival Policies* + +**Cross-ref:** dcm doc: *"Realizes the persistence contract in [udlm/70-design-principles/51-infrastructure-optimization.md] by mandating PostgreSQL for DCM."* + +**Resolution:** Per analogy — "must have a road" is rule of the road; "the road is paved asphalt with painted lines" is a jurisdictional infrastructure choice. + +--- + +### 21. `53-capability-discovery.md` + +**udlm sections** → `udlm/20-contracts/53-capability-discovery.md` +- *Problem Statement (types vs capabilities)* — modeling contract. +- *Unified Provider Model* — provider registration contract. +- *Capability Declaration Format and Semantics* — provider description contract. + +**dcm sections** → fold into `dcm/architecture/convergence-engine/overview.md` (or new file) +- *Provider Registry Implementation with Capabilities* +- *Capability Matching for Dispatch Decisions* +- *Backward Compatibility with Type-Based Model* +- *Capability Validation and Conflict Resolution* + +**Cross-ref:** dcm doc cites this udlm doc as the capability declaration contract. + +--- + +## Hardest calls — all resolved by wire-compatibility decision + +1. ~~**`04-examples.md`**~~ — **RESOLVED**: all examples kept clean in udlm; dcm gets its own `orchestration-scenarios.md`. No per-paragraph splitting. + +2. ~~**`28-federated-contribution-model.md`**~~ — **RESOLVED**: split as proposed. Wire-compatibility means contributor types + artifact formats ARE wire-level concerns peers must agree on; GitOps PR is dcm's transport for contributions, not the contract. + +3. ~~**`43-provider-callback-auth.md`**~~ — **RESOLVED**: udlm defines the **two-layer auth contract abstractly** (any peer must validate provider identity via two independent factors). dcm specifies **mTLS + interaction credential** as its specific mechanism. Peer realizations could pick different layers and still conform — provided they declare their auth mechanism via the schema-sharing protocol. + +4. ~~**`48-location-topology-layers.md`**~~ — **RESOLVED**: layered-topology contract + assembly rules + lifecycle → udlm. Specific 9-layer hierarchy → dcm canonical default. + +5. ~~**`51-infrastructure-optimization.md`**~~ — **RESOLVED**: PostgreSQL mandate moves to dcm. udlm keeps data-contract principle + four-domains contract + persistence-required contract (technology-neutral). + +6. ~~**`31-credential-management.md` + `31-credential-provider-model.md`**~~ — **RESOLVED**: merge to `udlm/governance/credentials.md` (udlm side) and `dcm/architecture/credentials-and-auth/credentials.md` (dcm side). + +--- + +## Newly identified udlm contracts (from "rules of the road" sweep) + +User clarification (a/b/c) and a follow-up sweep of existing docs surfaced +**7 net-new substrate documents** that were missing, partially specified, or +scattered. **All 7 have been drafted** in `architecture/data-model/` and will +migrate to udlm during the split. + +### Created (drafts in dcm/architecture/data-model/, target paths in udlm shown) + +| Doc | Target path | Draft location | Status | +|---|---|---|---| +| **Identifier scheme contract** | `udlm/contracts/identifier-scheme.md` | `architecture/data-model/identifier-scheme.md` | ✅ Drafted | +| **Time and clock model** | `udlm/contracts/time-and-clock.md` | `architecture/data-model/time-and-clock.md` | ✅ Drafted | +| **Error model contract** | `udlm/contracts/error-model.md` | `architecture/data-model/error-model.md` | ✅ Drafted (incl. `conformance.*` namespace) | +| **Retry semantics contract** | `udlm/contracts/retry-semantics.md` | `architecture/data-model/retry-semantics.md` | ✅ Drafted | +| **Rate limit + backpressure contract** | `udlm/contracts/rate-limit-and-backpressure.md` | `architecture/data-model/rate-limit-and-backpressure.md` | ✅ Drafted | +| **Schema sharing protocol** | `udlm/contracts/schema-sharing.md` | `architecture/data-model/schema-sharing.md` | ✅ Drafted | +| **Conformance specification** | `udlm/CONFORMANCE.md` | `architecture/data-model/CONFORMANCE.md` | ✅ Drafted | + +### Sufficient as-is (no new doc needed) + +- **Idempotency contract** — `33-event-catalog.md` already covers it at substrate quality (event_uuid as idempotency key, at-least-once semantics, consumer-supplied Idempotency-Key). Just needs to be cross-referenced from new related docs. + +### Perspective docs (the "handbooks") — authored + +Two complementary perspective docs, one per layer. **Authored** during the +split execution phase: + +| Doc | Target path | Purpose | Status | +|---|---|---|---| +| **Consumer perspective (driver's handbook)** | `udlm/docs/consumer-perspective.md` | How a consumer sees the system: onboarding, mental models, request lifecycle, common patterns, troubleshooting — written from the user's POV against the substrate | ✅ Done | +| **Operator perspective (DMV operator's manual)** | `dcm/architecture/operator-perspective.md` | How an implementer/operator sees the system: how DCM operationalizes udlm, where the realization choices live, deployment perspective, ops playbook entry point | ✅ Done | + +### Sweep findings — what was checked and verdict + +| Concept | Found | Verdict | +|---|---|---| +| Identifier scheme | Scattered across 06, 02, 33, A, 45 | Gap — needs new substrate doc | +| Idempotency | Thorough in 33, 18, 06, 23, 25, 31b, A | Substrate quality — keep | +| Backpressure / rate limit | Strong in 49, 18, 37, 53 | Mixed — extract substrate portion | +| Time / clock model | Scattered in 40, 33, 16, 12, 37, 52 | Gap — needs new substrate doc (UTC contract, ms precision, skew tolerance, total ordering) | +| Error model | Minimal in consumer-api-spec, 31b, B | Gap — needs new substrate doc with closed error vocabulary | +| Retry semantics | Operational in 24, 25, 7, 30, 37, 23, 50 | Partial — extract + new substrate framing | +| Consumer perspective | Minimal (04-examples is closest) | Gap — needs net-new driver's handbook | + +--- + +## Execution sequence (Phase 2-3) + +Mechanical work, in order: + +1. **Create empty `udlm` git repo** at `github.com/croadfeldt/udlm`. +2. **Use `git filter-repo`** to extract `architecture/data-model/` history into the new repo, preserving commits. Reorganize into the LOCKED udlm layout in a single restructure commit (drop all numeric prefixes; move files into their target directories). +3. **For each "both" file**: split into udlm + dcm fragments per the per-section blocks above. Apply the rename mapping at the same time. Single commit per file ("split N-foo.md: udlm/dcm portions"). +4. ~~Author the 6 new udlm contract docs + CONFORMANCE.md~~ ✅ **DONE** — drafted in `architecture/data-model/`. Execution moves them to their target paths in udlm during step 2. +5. ~~Author the two perspective docs~~ ✅ **DONE** — authored at `udlm/docs/consumer-perspective.md` and `dcm/architecture/operator-perspective.md`. +6. **Update cross-references**: add the `> Implements...` header to every dcm doc that has a udlm counterpart. Cross-link the new substrate docs from anywhere they're referenced. Update intra-doc references in the 7 new contract docs to drop their `(N-...)` legacy path hints. +7. ~~Add `conformance.version_deprecated` federation event~~ ✅ **DONE** — wired into `udlm/contracts/event-catalog.md` (introduced by `CONFORMANCE.md` §9.2). +8. **Delete migrated files from dcm** in one cleanup commit. +9. **Update dcm `README.md` and `project-overview`** to reference udlm as the substrate spec. +10. **Commit `00-layering-data-model-vs-dcm.md`** with concrete repo links now resolvable. + +## After split (Phase 4-5) — DAV plumbing + +- DAV MCP doc-fetcher gets two source repos (`udlm` + `dcm`). +- UC YAML `spec_refs` use namespaced paths: `udlm/governance/governance-matrix.md` and `dcm/architecture/convergence-engine/policy-evaluation.md`. +- Source ConfigMap split: separate mountpaths. +- Run one sample UC eval to validate cross-repo resolution. + +--- + +## Decision log + +All blocking decisions are settled. This section is the durable record of +what was decided and why. + +### Locked decisions + +- ✅ **Compatibility model**: wire-compatible at data/event/contract boundary (versioning rules apply). udlm is K8s-shaped: API + CRD wire-compatible across distributions; controllers not portable. +- ✅ **Repo location**: `github.com/croadfeldt/udlm` +- ✅ **udlm numbering**: dropped (directory structure carries ordering) +- ✅ **dcm numbering**: dropped (flat, organized by concern) +- ✅ **Split manifest**: kept as permanent contextual doc in dcm (helps future contributors understand the boundary) +- ✅ **Examples**: all-udlm + new dcm `orchestration-scenarios.md` +- ✅ **Location topology**: contract → udlm, specific 9-layer hierarchy → dcm canonical default +- ✅ **PostgreSQL mandate**: → dcm (substrate requires persistence; technology choice is dcm-level) +- ✅ **Credentials (31a + 31b)**: merged on both sides (`udlm/governance/credentials.md`, `dcm/architecture/credentials-and-auth/credentials.md`) +- ✅ **Federated contribution**: split (wire-compat makes contributor types + artifact format a peer contract; GitOps PR is dcm's transport) +- ✅ **Provider callback auth**: udlm = abstract two-layer auth contract; dcm = mTLS + interaction credential mechanism; peers declare their chosen mechanism via schema-sharing +- ✅ **7 new udlm substrate docs drafted**: identifier-scheme, time-and-clock, error-model (with `conformance.*` namespace), retry-semantics, rate-limit-and-backpressure, schema-sharing, CONFORMANCE +- ✅ **Consumer perspective + operator perspective**: paired narrative docs authored at `udlm/docs/consumer-perspective.md` and `dcm/architecture/operator-perspective.md` +- ✅ **Single narrative directory**: `docs/` (no separate `guides/`) + +### Open items (cosmetic / non-blocking) + +- [ ] Final read-through could flag any reclassifications on the pure-udlm and pure-dcm file lists. Default: trust the agent's classification + the analogy validation. + +### Items deferred to execution phase + +- Path rename: per-section split blocks (1-21 above) reference legacy numeric paths. Execution will apply the mapping uniformly (drop numeric prefixes from directories and filenames). +- ✅ `conformance.version_deprecated` federation event wired into `udlm/contracts/event-catalog.md` (introduced by CONFORMANCE.md §9.2). +- ✅ Two perspective docs authored (consumer-perspective.md, operator-perspective.md). diff --git a/architecture/DCM-Capabilities-Matrix.md b/architecture/DCM-Capabilities-Matrix.md new file mode 100644 index 0000000..78f65e9 --- /dev/null +++ b/architecture/DCM-Capabilities-Matrix.md @@ -0,0 +1,730 @@ +# DCM — Foundational Capabilities Matrix + +> **Purpose:** This document defines the core operational capabilities required for DCM to perform lifecycle management as defined by the data model. Each capability maps to a consumer/service provider perspective and will be used to drive implementation work in Jira. +> +> **How to read this document:** +> - **Capability Domain** — the architectural area the capability belongs to +> - **Capability** — a discrete operational function; the smallest unit of independently implementable behavior +> - **Consumer perspective** — what the end user / application team experiences +> - **Service Provider perspective** — what the Service Provider or platform component must implement +> - **Platform/Admin perspective** — what the platform engineer or SRE must configure or operate +> - **Depends on** — other capabilities that must exist first + +--- + +## 1. Identity and Access Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| IAM-001 | Actor Authentication | Authenticate to DCM via configured IdP | — | Register and configure Auth Providers; manage local user store | — | +| IAM-002 | Session Token Management | Receive and use session tokens; token refresh | — | Configure session TTL, failover chain | IAM-001 | +| IAM-003 | Role-Based Access Control | Receive role-appropriate service catalog and API responses | — | Declare role mappings; assign roles to actors | IAM-001 | +| IAM-004 | Group Membership Resolution | Group memberships automatically applied from IdP | — | Map IdP groups to DCM groups; declare group-role relationships | IAM-001, IAM-003 | +| IAM-005 | Multi-Factor Authentication | Satisfy per-session and step-up MFA challenges | — | Configure MFA methods; declare step-up operations | IAM-001 | +| IAM-006 | SCIM Automated Provisioning | Actor created/updated/deprovisioned from IdP automatically | — | Configure SCIM endpoint and attribute mappings | IAM-001 | +| IAM-007 | Tenant Scope Enforcement | Access restricted to authorized Tenants | — | Declare Tenant membership; configure cross-tenant policies | IAM-003, IAM-004 | +| AUTH-002 | Multi-Auth-Provider Routing | — | — | Register multiple Auth Providers simultaneously; ingress routes by authentication signal | IAM-001 | +| AUTH-003 | Auth Provider Trust Level Enforcement | Requests evaluated per provider trust level (authoritative / verified / advisory) | — | Configure trust level per registered Auth Provider | IAM-001 | +| AUTH-004 | Auth Provider Artifact Versioning | — | — | Manage role/tenant mapping versioning through standard DCM artifact lifecycle; activate/deprecate mappings | IAM-003 | +| AUTH-005 | Auth Provider Failover | Existing sessions remain valid on provider failure; new auth routes to failover chain | — | Configure failover chain; monitor provider health; manage session cache TTL | IAM-001 | +| AUTH-006 | Auth Context in Audit Trail | — | — | Auth Provider identity and ingress context automatically recorded in all audit records | IAM-001 | +| AUTH-007 | Auth Provider Credential Security | — | — | Enforce Auth Provider config credentials reference secrets management; no plaintext credentials | IAM-001 | +| AUTH-008 | No Anonymous Access | — | — | Enforce authenticated access at all ingress surfaces across all profiles | IAM-001 | +| AUTH-009 | Webhook and Message Bus Authentication | Authenticate webhook registrations | — | Enforce authentication on all inbound surfaces regardless of profile | IAM-001 | +| AUTH-010 | Per-Actor Rate Limiting | Receive 429 responses when rate limit exceeded | — | Configure rate limits per actor; manage burst allowances | IAM-001 | +| AUTH-011 | Git PR Identity Resolution | Git PR submissions resolve to same actor identity as API/UI login | — | Configure Auth Provider to trust Git server's identity assertion | IAM-001 | +| AUTH-012 | SCIM Automated Provisioning | Actor created/updated/deprovisioned from IdP automatically via SCIM | — | Configure SCIM 2.0 endpoint; manage suspension-on-deprovision policy | IAM-001 | +| AUTH-013 | In-Flight Request Continuity on Auth Failure | In-flight requests before auth failure are not interrupted | — | Configure session cache TTL; manage graceful degradation | IAM-001 | +| AUTH-014 | Two-Tier MFA Enforcement | Satisfy per-session MFA at login and step-up MFA for high-risk operations | — | Configure per-session and step-up MFA; declare step-up trigger operations | IAM-005 | +| AUTH-015 | Built-In Auth Provider Storage Backend | — | — | Configure built-in Auth Provider storage backend (SQLite for homelab/dev; PostgreSQL/MySQL for standard+) | IAM-001 | + +--- + +## 2. Service Catalog + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| CAT-001 | Service Catalog Presentation | Browse available services filtered by RBAC | Declare catalog items for offered resource types | Activate catalog items; configure catalog visibility policies | IAM-003, IAM-007 | +| CAT-002 | Service Schema Discovery | View field schemas, constraints, and edit constraints for a catalog item | Declare field schemas in Resource Type Spec | Configure constraint visibility level per profile | CAT-001 | +| CAT-003 | Catalog Item Search and Filter | Search catalog by keyword, resource type, tag | — | Configure Search Index for catalog | CAT-001 | +| CAT-004 | Catalog Item Versioning | Request a specific version of a catalog item | Publish new catalog item versions following semver | Manage version lifecycle; enforce deprecation timelines | CAT-001 | +| CAT-005 | Cost Estimation | Receive estimated cost before submitting a request | Declare cost metadata on provider registration | Configure Cost Analysis component | CAT-001 | +| CAT-006 | Dependency Visualization | See required dependencies for a catalog item before requesting | Declare dependency graph in Resource Type Spec | — | CAT-001 | +| CAT-007 | Catalog Item Deprecation | Receive deprecation warnings on deprecated catalog items | Declare successor types in deprecation notice | Manage deprecation lifecycle; notify consumers | CAT-004 | + +--- + +## 3. Request Lifecycle Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| REQ-001 | Submit Service Request | Submit a resource request via UI, API, or Git PR | — | Configure request ingress surfaces | IAM-007, CAT-001 | +| REQ-002 | Intent State Capture | Request stored as versioned GitOps artifact before processing | — | Configure Intent Store; manage Git repository structure | REQ-001 | +| REQ-003 | Layer Assembly | Request enriched with organizational defaults and context layers | Contribute Service Layers for resource types | Manage Core Layers; configure Layer Cache | REQ-002 | +| REQ-004 | Policy Evaluation | Request validated, transformed, and gated by applicable policies | Contribute provider-specific policies | Manage Policy Engine; configure Policy Groups and Profiles | REQ-003 | +| REQ-005 | Placement Engine Execution | Resource placed with the best available provider instance | Implement capacity reserve_query response | Configure placement constraints; manage provider priorities | REQ-004 | +| REQ-006 | Requested State Persistence | Assembled payload stored as authoritative GitOps record | — | Configure Requested Store; manage storage redundancy | REQ-005 | +| REQ-007 | Provider Dispatch | Request payload delivered to selected provider | Implement Services API to receive DCM payloads | Configure API Gateway and egress | REQ-006 | +| REQ-008 | Request Status Tracking | Monitor request status from submitted through realized | Report realization status back to DCM | Configure observability for request tracking | REQ-007 | +| REQ-009 | Request Cancellation | Cancel a pending request before realization | Handle cancellation payloads | Configure cancellation policies | REQ-002 | +| REQ-010 | Git PR Request Ingress | Submit requests via Git Pull Request with policy dry-run feedback | — | Configure Git Request Watcher; manage repository structure | REQ-001, IAM-001 | + +--- + +## 4. Provider Contract and Realization + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| PRV-001 | Provider Registration | — | Register provider with DCM: declare type, capabilities, sovereignty, cost metadata | Configure Provider Registry; validate sovereignty declarations | IAM-001 | +| PRV-002 | Naturalization | — | Convert DCM unified payload to provider-native format | — | PRV-001, REQ-007 | +| PRV-003 | Realization | — | Execute required actions to provision/configure/change resource | — | PRV-002 | +| PRV-004 | Denaturalization | — | Convert provider-native result back to DCM unified format | — | PRV-003 | +| PRV-005 | Realized State Reporting | — | Report realized payload and status to DCM API Gateway | Configure Realized State Store; manage Event Stream | PRV-004 | +| PRV-006 | Capacity Reporting | — | Respond to reserve_query with current capacity and availability | Configure placement engine; manage capacity confidence | PRV-001 | +| PRV-007 | Provider Health Reporting | — | Expose health check endpoint; report availability | Monitor provider health; configure trust score updates | PRV-001 | +| PRV-008 | Sovereignty Declaration Maintenance | — | Notify DCM when sovereignty data changes within declared SLA | Monitor sovereignty changes; trigger re-evaluation | PRV-001 | +| PRV-009 | Composite Service Composition | — | Register Composite Service catalog items declaring constituent resource types, dependencies, and delivery requirements | Configure Composite Service registration eligibility | PRV-001, PRV-003 | +| PRR-001 | OpenAPI Spec Declaration (GATE-SP-01) | — | Declare OpenAPI spec URL at registration; spec must be machine-readable and reachable | Validate spec URL reachability during approval pipeline | PRV-001 | +| PRR-002 | Healthy API at Activation (GATE-SP-02) | — | Health endpoint returns `{"status": "healthy"}` at activation time | Enforce health check as activation precondition | PRV-001, HLT-001 | +| PRR-003 | State Management Callback (GATE-SP-03) | — | Implement realized_state_push callback at all conformance levels | Validate callback endpoint reachability during approval | PRV-001 | +| PRR-004 | Tenant Metadata Endpoint (GATE-SP-04) | — | Implement GET /api/v1/tenants/{uuid}/metadata returning usage data | Require for standard+ profile activation; enforce quota integration | PRV-001 | +| PRR-005 | Prometheus Metrics (GATE-SP-05) | — | Expose required metric families at declared metrics_endpoint | Validate metric presence during approval; gate standard+ activation | PRV-001, HLT-005 | +| PRR-006 | AEP.DEV Linting (GATE-SP-06) | — | Pass AEP linter against OpenAPI spec with no errors before registration; include linting report URL | Gate standard+ activation on linting pass; block activation on errors | PRV-001 | + +| PRV-010 | Provider Sandbox/Test Mode | Submit test requests targeting sandbox providers via `_test_context.target_provider_uuid`; sandbox providers visible in registry with `status: sandbox` | Register with `sandbox_mode: true`; implement full OIS contract; graduate to production via standard approval | Manage sandbox provider registry; review graduation requests; sandbox providers excluded from production placement | PRV-001, GATE-SP-01 | +| PRR-007 | Multi-Tenant Dispatch (GATE-SP-07) | — | Accept tenant_uuid in all dispatch payloads; return tenant-scoped resources | Gate standard+ activation on multi-tenant compatibility test | PRV-001 | + +--- + +## 5. Resource Lifecycle Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| LCM-001 | Resource State Transitions | Trigger lifecycle actions: suspend, resume, decommission | Handle state transition payloads | Configure lifecycle policies; manage state machine | REQ-008 | +| LCM-002 | Post-Realization Field Updates | Update editable fields on realized resources (targeted delta) | Handle delta update payloads; apply partial changes | Configure editable field declarations; manage edit policies | PRV-005 | +| LCM-003 | Resource TTL Management | Declare and extend resource TTLs; receive expiry notifications | Handle TTL-triggered decommission payloads | Configure Lifecycle Constraint Enforcer; manage expiry policies | LCM-001 | +| LCM-004 | Ownership Transfer | Transfer resource ownership to a different Tenant | — | Authorize and execute ownership transfers; record transfer history | IAM-007, LCM-001 | +| LCM-005 | Rehydration | Replay a resource's intent state to a new provider or context | Receive and execute rehydration payloads | Manage rehydration leases; configure auth level requirements | REQ-002, PRV-003 | +| LCM-006 | Billing State Management | — | — | Configure billing state policies; integrate with Cost Analysis | LCM-001 | +| LCM-007 | Resource Decommission | Decommission resources individually or as part of group decommission | Handle decommission payloads; release resources | Manage decommission workflows; coordinate dependency teardown | LCM-001 | + +--- + +## 6. Drift Detection and Remediation + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| DRF-001 | Active Discovery | — | Expose discovery endpoint; respond to interrogation queries | Configure discovery schedules; manage Discovered Store | PRV-005 | +| DRF-002 | Drift Comparison | Receive drift notifications for owned resources | — | Configure drift detection policies; manage comparison logic | DRF-001, PRV-005 | +| DRF-003 | Drift Notification | Receive actionable drift alerts with field-level detail | — | Configure drift notification channels and escalation policies | DRF-002 | +| DRF-004 | Drift Remediation | Approve or reject automatic drift remediation | Execute remediation payloads | Configure remediation policies (revert/update/alert/escalate) | DRF-002, LCM-002 | +| DRF-005 | Unsanctioned Change Detection | Receive alerts on unauthorized resource modifications | Report all external state changes to DCM | Configure unsanctioned change policies | DRF-001 | + +--- + +## 7. Policy Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| POL-001 | Policy Authoring | — | Contribute provider-specific policy rules | Author and manage policies via API or GitOps ingress | IAM-003 | +| POL-002 | Policy Validation and Shadow Mode | View shadow evaluation results on own requests | — | Configure shadow mode; review shadow results | POL-001 | +| POL-003 | Policy Activation and Review | — | — | Manage policy review periods; authorize policy activation | POL-001, POL-002 | +| POL-004 | Policy Group Management | — | — | Compose Policy Groups; manage profile assignments | POL-003 | +| POL-005 | Profile Management | — | — | Configure deployment profiles; manage compliance domain groups | POL-004 | +| POL-006 | External Policy Evaluation | — | Register as external evaluation endpoint; implement BBQ-001–009 governance | Configure external evaluation trust levels; manage trust elevation | PRV-001, POL-001 | +| POL-007 | Policy Override and Constraint Visibility | View constraint details for service catalog fields | Declare constraint schemas on Resource Type Specs | Configure constraint visibility levels per profile | CAT-002, POL-003 | +| POL-008 | Constraint Type Registry | — | — | Register constraint types with OpenAPI v3 schemas; configure emittable_by/consumable_by; manage core and organization tiers | POL-001 | +| POL-009 | Evaluation Context and Multi-Pass Convergence | — | — | Configure max evaluation passes; monitor convergence; manage escalation for unresolvable conflicts | POL-001, POL-008 | +| POL-010 | Policy Templates | — | Contribute policy templates with parameterized Rego | Register templates (Gatekeeper ConstraintTemplate pattern); validate parameter schemas and constraint type references | POL-001, POL-008 | +| POL-011 | DCM Constraint Types Library | — | — | Manage auto-generated Rego library (data.dcm.constraint_types); sync with Constraint Type Registry | POL-008, POL-010 | +| POL-012 | Data-Driven Policy Matching | — | — | Configure match sources (request payload, operation context, evaluation context, entity metadata); validate match fields at activation | POL-001 | +| POL-013 | Lifecycle-Scoped Policy Evaluation | — | — | Configure lifecycle_scope per policy (which operation types trigger it); enforce profile minimums (fsi/sovereign require sovereignty policies on all operations); configure changed_field_filter for update/scale operations | POL-001, POL-012 | +| POL-014 | Override Policies | — | — | Author override policies targeting specific policies for defined scopes; enforce expiry and review dates; cannot target hard enforcement policies | POL-001, POL-003 | +| POL-015 | Exception Grants | — | — | Create time-bounded, scope-limited waivers with compensating controls; track usage count; enforce dual-approval for hard policies; manage renewal limits | POL-001, AUD-008 | +| POL-016 | Manual Override | Submit override request for blocked request with justification | — | Grant single-request overrides; enforce dual-approval for hard policies; manage override authority roles | POL-001, IAM-001 | +| POL-017 | Dual-Approval Escalation | — | — | Configure role separation requirements; manage approval workflow for hard policy overrides; enforce fsi/sovereign dual-approval on all overrides | POL-001 | +| POL-018 | Compensating Control Substitution | — | — | Define substitute control sets that satisfy policy intent through different mechanisms; require compliance officer validation | POL-001 | +| POL-019 | Override Approval Flow | Submit override request for blocked request; view pending overrides awaiting approval | — | Configure override notification routing (internal, webhook); configure timeout per profile; configure escalation rules; manage override authority roles | POL-016, IAM-001 | +| POL-020 | Override Notification Routing | — | — | Configure per-profile notification channels (LISTEN/NOTIFY, webhook to ServiceNow/Jira/Slack); configure routing by policy domain and enforcement level; configure escalation timeouts | POL-019 | +| POL-021 | Policy Block Resolution | View blocking details, resolution guidance with compliant values; choose resolution action (modify, request override, cancel, escalate) | — | Configure block timeout per profile; configure resolution guidance generation; manage escalation routing | POL-001 | + +--- + +## 8. Data Layer Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| LAY-001 | Core Layer Authoring | — | — | Author and manage Core and Organizational Layers in GitOps | IAM-003 | +| LAY-002 | Service Layer Contribution | — | Contribute Service Layers for offered resource types | Manage layer compatibility declarations | PRV-001, LAY-001 | +| LAY-003 | Layer Cache Management | — | — | Manage Layer Cache synchronization; handle cache invalidation | LAY-001, LAY-002 | +| LAY-004 | Layer Exclusion | Declare layer exclusions on specific requests | — | Configure which layers may be excluded; manage non-excludable declarations | REQ-003 | +| LAY-005 | Layer Versioning and Lifecycle | — | — | Manage layer versions; handle deprecation; enforce immutability | LAY-001 | + +--- + +## 9. Information and Data Integration + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| INF-001 | Information Provider Registration | — | Register Information Provider; declare authority scope and schema | Configure Information Provider Registry; manage authority layers | IAM-001 | +| INF-002 | Information Provider Push | — | Push field value updates to DCM; respond to conflict notifications | Configure ingestion pipeline; manage conflict resolution policies | INF-001 | +| INF-003 | Information Provider Pull / Discovery | — | Expose data query endpoint for DCM pull operations | Configure pull schedules; manage cache TTLs | INF-001 | +| INF-004 | Write-Back | — | Implement write-back endpoint to receive DCM-initiated updates | Configure write-back triggers via policy | INF-001, INF-002 | +| INF-005 | Confidence Score Visibility | View confidence bands on entity field values; query confidence aggregation API | — | Configure confidence scoring formula; manage trust score thresholds | INF-001 | +| INF-006 | Conflict Resolution Management | — | — | Review and resolve contested field values; manage conflict escalation | INF-002 | + +--- + +## 10. Ingestion and Brownfield Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ING-001 | Resource Discovery and Ingestion | — | Expose discovery endpoints for brownfield resources | Configure ingestion pipeline; manage __transitional__ Tenant | DRF-001 | +| ING-002 | Ingested Entity Review | — | — | Review ingested entities; resolve conflicts; promote to active Tenants | ING-001 | +| ING-003 | Bulk Promotion | — | — | Execute bulk entity promotions with preview and rollback | ING-002 | +| ING-004 | Catalog Item Association | — | — | Associate ingested entities with Resource Type Specs; create catalog items | ING-002, CAT-001 | + +--- + +## 11. Audit and Compliance + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| AUD-001 | Audit Trail Access | Query audit records for own resources | — | Configure Audit Store; manage retention policies | IAM-003 | +| AUD-002 | Compliance Reporting | — | — | Generate compliance reports; manage report schedules | AUD-001 | +| AUD-003 | Merkle Tree Verification | — | — | Run inclusion proofs, consistency proofs, request chain verification; manage integrity incidents | AUD-001 | +| AUD-004 | Cross-DCM Audit Correlation | — | — | Correlate audit records across DCM instances via correlation_id; authorize cross-DCM pulls | AUD-001, DCM-001 | +| AUD-005 | Audit Record Retention Management | — | — | Configure reference-based retention; manage post-lifecycle retention | AUD-001 | +| AUD-006 | Audit Granularity Configuration | — | — | Configure granularity level per profile (stage, mutation, field); enforce minimum for fsi/sovereign | AUD-001 | +| AUD-007 | Signed Tree Heads | — | — | Configure STH interval; manage audit signing keys; publish STH for external verification | AUD-001 | +| AUD-008 | Payload Chain of Custody | View chain-of-custody proof for own requests | Verify dispatched payload matches DCM's signed output | Verify full pipeline integrity via request chain verification API | AUD-003 | +| AUD-009 | Inter-Stage Verification | — | — | Configure verification mode per profile (synchronous, asynchronous, disabled) | AUD-001 | + +--- + +## 12. Observability and Operations + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| OBS-001 | Operational Dashboard | View health and status of own resources | — | Configure and manage observability dashboard | — | +| OBS-002 | Metrics and Telemetry Export | — | Expose resource-level metrics to DCM | Configure observability export; integrate enterprise observability platform, or deploy the packaged dcm-observability stack as the authoritative platform | — | +| OBS-003 | Curated Event Stream Subscription | Subscribe to observability event types via Message Bus | — | Configure event stream publication policies; manage subscriber roles | OBS-002 | +| OBS-004 | Alert and Notification Management | Receive resource and policy alerts via declared channels | — | Configure alert routing; manage notification channels and escalation | OBS-001 | + +| OBS-006 | SLA/SLO Declaration | View SLO status for owned resources (`GET /resources/{uuid}/slo-status`) | Declare resource_type SLOs in Resource Type Specification; report realization timing via callbacks | Configure SLO targets per resource type; view aggregate SLO performance report (`GET /admin/slo/report`) | RLM-001, LCM-001 | +| OBS-007 | SLO Breach Detection and Notification | Receive `slo.breach_approaching` and `slo.breach_detected` events | — | Configure SLO breach routing and escalation; review aggregate breach reports | OBS-006, EVT-001 | +| OBS-005 | Cost Analysis and Attribution | View cost estimates and actuals for owned resources | Provide cost metadata; report utilization | Configure Cost Analysis component; manage cost attribution policies | PRV-006 | +| OBS-008 | Group-Scoped Observability | View dashboards, reports, and alerts scoped to the business/operational groups (DCMGroup) own resources belong to | Attribute telemetry to entity UUIDs so group scoping resolves from resource definitions | Scope dashboards, reporting, alerting, and their management to DCMGroups; scoping derives from data in the resource definitions themselves (group membership, ownership), never side-channel configuration | OBS-001, OBS-002, OBS-004 | + +--- + +## 13. Storage and State Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| STO-001 | Data Store Management | — | — | Configure PostgreSQL data domains (intent, requested, realized, discovered); manage schema, RLS, and tenant isolation | — | +| STO-002 | Realized State Management | — | — | Configure realized_entities table; manage version retention and is_current flag | PRV-005 | +| STO-003 | Discovered State Management | — | — | Configure discovered_records table; manage retention policies per profile | DRF-001 | +| STO-004 | Search and Query | Use entity and catalog search | — | Configure materialized views and indexes; manage cache refresh | STO-001 | +| STO-005 | Backup and Recovery | — | — | Configure PostgreSQL backup (PITR); test recovery; verify audit Merkle-tree integrity (inclusion/consistency proofs) | STO-001 | +| STO-006 | Provenance Model Configuration | — | — | Select and configure provenance model (full_inline / deduplicated / tiered); manage tier transitions | STO-001 | +| STO-007 | Sovereignty Partitioning | — | Declare sovereignty constraints at registration | Configure separate PostgreSQL instances per sovereignty zone; manage cross-zone prohibition | STO-001, GOV-001 | +| STO-008 | Tenant-Scoped Storage Isolation | Data is isolated by tenant via RLS | — | Configure RLS policies per table; enforce STI-001 through STI-004 | STO-001, IAM-001 | +| STO-009 | Tenant-Scoped Encryption (fsi/sovereign) | — | — | Configure per-tenant AES-256-GCM encryption via secrets management; manage key rotation | STO-008, CPX-001 | +| STO-010 | Internal Secrets Management | — | — | Configure envelope encryption (KEK source: env var, K8s secret, or HSM); manage secrets table; optional Vault external backend | STO-001 | +| STO-011 | Pipeline Event Routing | — | — | Configure LISTEN/NOTIFY for pipeline events; optional Kafka for high-throughput; manage consumption tracking | STO-001 | +| STO-012 | Internal Authentication | — | — | Configure local actor accounts (argon2id hashes, DCM-issued JWT); optional OIDC/SAML external auth_provider | STO-001, IAM-001 | + +--- + +## 14. DCM Federation and Multi-Instance + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| FED-001 | DCM Provider Registration | Submit requests that are routed to peer DCMs | Register as DCM Provider in peer instances | Configure DCM Provider registrations; manage federation trust | PRV-001, IAM-001 | +| FED-002 | Federation Routing | Requests automatically routed to appropriate Regional/Sovereign DCM | Respond to reserve queries from Hub DCM | Configure federation placement policies; manage sovereignty pre-filters | FED-001, REQ-005 | +| FED-003 | Federation Trust Management | — | — | Manage mTLS certificates; monitor federation trust scores; handle cert rotation | FED-001 | +| FED-004 | Cross-DCM Drift Detection | Receive drift alerts for federated resources | Publish Discovered State events to federation Message Bus | Configure federated drift detection; manage alert-and-hold policies | FED-001, DRF-002 | +| FED-005 | DCM Export and Import | — | — | Export and import DCM state packages; verify import trust scores | STO-001, STO-002 | + +--- + +## 15. Platform Governance and Administration + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| GOV-001 | Tenant Management | — | — | Create, configure, and decommission Tenants; manage compliance overlays | IAM-007 | +| GOV-002 | Group Management | — | — | Create and manage DCM Groups; configure sovereignty rules; manage time-bounded memberships | IAM-003 | +| GOV-003 | Registry Management | — | Register and maintain Resource Type Specifications in organization registry | Manage registry sync; configure registry policies; manage Tier 3 types | PRV-001 | +| GOV-004 | Resource Type Lifecycle | — | Manage deprecation notices; declare successor types; maintain migration guidance | Enforce deprecation timelines; manage sunset periods | GOV-003 | +| GOV-005 | Platform Configuration Management | — | — | Manage platform-wide layers; configure profiles; manage deployment manifest | LAY-001, POL-005 | +| GOV-006 | Bootstrap and Self-Hosting | — | — | Manage DCM self-deployment; verify bootstrap manifest; handle repave scenarios; at first-run the built-in bootstrap account receives a `platform_admin` `role_assignment` (ADR-RBAC-001) so the admin gate exists before any capability admission | STO-001 | + +| GOV-008 | Tenant Onboarding Workflow | Trigger onboarding completion: receive `tenant.onboarding_complete` when first entity OPERATIONAL | — | Execute full provisioning sequence: tenant entity, default groups, quota, admin actor, GitOps namespace, audit stream; dispatch `tenant.created` and member invitation events | IAM-001, STO-008 || GOV-007 | Sovereign Deployment Management | — | — | Manage air-gapped DCM instances; configure signed bundle import; manage offline registry | FED-001, STO-001 | + +--- + +## 16. Accreditation Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ACC-001 | Accreditation Submission | — | Submit accreditation records (BAA, ISO 27001, FedRAMP, etc.) referencing external certificate evidence | Register accrediting bodies; configure minimum accreditation types per profile | PRV-001 | +| ACC-002 | Accreditation Review and Approval | — | Receive approval/rejection notification | Review submitted accreditations; verify certificate references; approve or reject via Admin API | ACC-001 | +| ACC-003 | Accreditation Lifecycle Monitoring | Receive notification when a provider's accreditation is nearing expiry or revoked | Renew accreditations before expiry; submit renewal documentation | Monitor expiry timelines; fire P90D renewal warnings; handle accreditation gaps | ACC-001 | +| ACC-004 | Accreditation Gap Response | Receive notification when a provider enters accreditation gap affecting owned resources | — | Configure Recovery Policy for accreditation gap events; manage affected entity remediation | ACC-003, POL-005 | +| ACC-005 | Data Classification Enforcement | Receive enforcement feedback when request payload contains data the selected provider cannot handle | Declare max_data_classification_accepted in capability registration | Configure classification immutability rules; manage phi/sovereign classification locks | ACC-001, PRV-001 | +| ACC-006 | DCM Deployment Accreditation | — | — | Register DCM deployment-level accreditations; expose to federation peers for trust verification | PRV-001, FED-001 | + +--- + +## 17. Zero Trust and Security Posture + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ZTS-001 | Mutual TLS Enforcement | All interactions authenticated via mTLS at the client side | Present valid mTLS certificate on every interaction; rotate certificates on declared schedule | Configure trust anchors; manage CA chain; enforce mTLS at all interaction boundaries | IAM-001 | +| ZTS-002 | Scoped Interaction Credentials | Receive scoped short-lived credentials for authorized operations | Validate credential scope before executing operations; reject out-of-scope credentials | Configure credential lifetime per profile; manage credential issuance via secrets management | IAM-001, PRV-001 | +| ZTS-003 | Certificate Rotation Management | — | Implement certificate rotation before expiry; use transition window to avoid downtime | Monitor certificate expiry; fire P14D rotation warnings; manage P7D transition window | ZTS-001 | +| ZTS-004 | Zero Trust Posture Configuration | — | — | Configure zero_trust_posture per profile (none/boundary/full/hardware_attested); manage posture overrides | POL-005 | +| ZTS-005 | Hardware Attestation (Sovereign Profile) | — | Present hardware-attested identity (TPM/HSM) for sovereign profile interactions | Configure hardware attestation requirements; manage HSM integration; enforce for sovereign profile | ZTS-001, ZTS-002 | + +| ZTS-007 | Provider OpenAPI Spec Signing (SEC-001) | — | Sign OpenAPI spec with mTLS private key at registration; rejected at GATE-SP-01 if unsigned | Verify signature during registration approval pipeline | PRV-001, ZTS-001 | +| ZTS-008 | GitOps Secrets Scanning (SEC-002) | Commits with detected secrets rejected with `SECRETS_DETECTED` audit record | Ensure service layer SCM does not contain plaintext secrets | Configure scanning ruleset; review and remediate detected secrets | GOV-001, AUD-001 | +| ZTS-009 | Software Bill of Materials (SBOM) Declaration (SEC-003) | — | Declare SBOM reference at registration (mandatory for fsi/sovereign) | Enforce SBOM requirement during registration approval for fsi/sovereign profiles | PRV-001, ACR-001 || ZTS-006 | Five-Check Boundary Enforcement | — | Pass all five boundary checks on every interaction: identity → authorization → accreditation → matrix → sovereignty | Monitor boundary check audit records; respond to INTERACTION_DENIED events | ZTS-001, ACC-001, GMX-001 | + +--- + +## 18. Unified Governance Matrix + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| GMX-001 | Governance Matrix Rule Authoring | — | — | Author governance matrix rules in GitOps; declare match conditions across four axes (subject/data/target/context); declare field permissions | POL-001 | +| GMX-002 | Boundary Enforcement Evaluation | Receive DENY response with governing rule_uuid and human-readable reason when a request crosses a prohibited boundary | Receive field-stripped or redacted payloads when STRIP_FIELD/REDACT decisions apply | Monitor GMX evaluation audit records; respond to DENY events | GMX-001, ZTS-006 | +| GMX-003 | Field-Level Data Control | Receive request feedback when specific payload fields are stripped or redacted by active matrix rules | Receive filtered payloads; handle missing optional fields gracefully | Configure allowlist/blocklist field permissions per rule; manage STRIP_FIELD vs REDACT vs DENY_REQUEST escalation | GMX-001 | +| GMX-004 | Sovereignty Zone Management | — | Declare operating sovereignty zones in provider registration | Register sovereignty zones; declare jurisdictions, regulatory frameworks, inter-zone agreements | PRV-001, GMX-001 | +| GMX-005 | Compliance Domain Matrix Activation | — | — | Activate compliance domain matrix rules (HIPAA, GDPR, etc.) by enabling compliance domain in profile; rules apply automatically | POL-005, GMX-001 | +| GMX-006 | Tenant and Resource-Type Matrix Overrides | — | — | Declare Tenant-level and resource-type-level matrix rules that tighten (never relax) platform defaults | GMX-001, GOV-001 | +| GMX-007 | Matrix Rule Lifecycle Management | — | — | Manage governance matrix rule lifecycle (developing → proposed → active); use shadow mode for safe validation before activation | GMX-001, POL-002 | + +--- + +## 19. Drift Reconciliation + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| DRC-001 | Drift Record Production | Receive drift records with field-level detail: realized value, discovered value, field criticality, severity, unsanctioned flag | — | Configure Drift Reconciliation Component; manage comparison algorithm and severity thresholds | DRF-001, PRV-005 | +| DRC-002 | Unsanctioned Change Classification | Receive elevated-severity alert when change has no corresponding Requested State record | — | Configure unsanctioned change detection; manage severity escalation rules | DRC-001 | +| DRC-003 | Drift Severity Classification | Receive severity-classified drift records (minor/significant/critical) based on field criticality × change magnitude | — | Declare field criticality in Resource Type Specifications; configure magnitude thresholds per profile | DRC-001, GOV-003 | +| DRC-004 | Drift Resolution Tracking | View drift record status (open/acknowledged/resolved/escalated); receive resolved notification when next discovery confirms clean state | — | Monitor drift resolution rates; configure escalation policies for aged-open drift records | DRC-001, DRF-004 | +| DRC-005 | Governance Matrix Drift Integration | — | — | Configure governance matrix check in drift comparison pipeline: expected provider changes are not flagged as drift | DRC-001, GMX-001 | + + +## 20. Federated Contribution Model + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| FCM-001 | Consumer Policy Authoring | Author and submit tenant-domain policies (Validation, Transformation, Recovery, Lifecycle, Orchestration Flow, Governance Matrix rules) via API or Flow GUI; receive PR URL and shadow mode results | — | Configure consumer policy authoring permissions (policy_author role); manage review requirements per profile | POL-001, IAM-003, IAM-007 | +| FCM-002 | Provider Resource Type Publication | — | Publish Resource Type Specifications and Catalog Items for offered resource types via provider contribution API; receive registry PR for platform admin review | Manage provider contribution registry; configure review requirements for provider specs; manage Organization-tier registry | PRV-001, GOV-003 | +| FCM-003 | Provider Service Layer Contribution | — | Contribute Service Layers for offered resource types; layers applied during request assembly for all consumers requesting that resource type | Review and activate provider-contributed layers; manage layer compatibility | PRV-001, LAY-002 | +| FCM-004 | Consumer Resource Group and Definition Contribution | Author and manage resource groups, notification subscriptions, webhook registrations, and cross-tenant authorization records within own Tenant | — | Configure contribution permissions per role; manage Tenant-scoped artifact lifecycle | IAM-007, GOV-002 | +| FCM-005 | Federation Contribution (Peer DCM) | — | Peer DCM contributes registry entries, policy templates, and service layers via federation channels | Manage federation contribution trust posture (verified/vouched/provisional); configure review requirements per trust posture; manage cross-DCM artifact lifecycle | FED-001, GOV-003, POL-003 | +| FCM-006 | Contribution Review and Lifecycle | View contribution status (proposed, pending_review, active, withdrawn); withdraw a pending contribution; receive notification when contribution is approved or rejected | Receive notification when provider contributions are reviewed | Review and approve/reject contributions via Admin API; manage shadow review periods; assign new owners to orphaned artifacts | POL-002, POL-003 | +| FCM-007 | Contributor Scope Enforcement | Receive clear DENY response when attempting to contribute outside permitted domain scope | Receive DENY when contributing specs for resource types not offered | Monitor Governance Matrix enforcement at contribution time; configure scope violation audit and notification | GMX-001, GMX-002 | + +--- + + +## 21. Scoring Model + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| SMX-001 | Operational Validation Policy Scoring | Receive risk score and score_drivers with request acknowledgment; understand why score is at its level | Declare `enforcement_class: operational` and `scoring_weight` on contributed validation policies | Configure operational validation policies with appropriate weights; manage per-policy enforcement class | POL-001, REQ-004 | +| SMX-002 | Advisory Validation and Completeness Score | Receive advisory_warnings list with request acknowledgment; understand what optional improvements exist | Declare `output_class: advisory` on advisory Validation policies | Configure advisory Validation policies; manage completeness score thresholds | POL-001, REQ-004 | +| SMX-003 | Actor Risk History Tracking | View own risk history score and contributing events via Consumer API | — | Monitor actor risk history; reset scores for trusted automation accounts; configure decay parameters | AUD-001, IAM-001 | +| SMX-004 | Quota Pressure Scoring | Receive quota_pressure as a score driver when approaching Tenant quota limits | — | Configure per-resource-type quota limits; manage free_threshold parameter | IAM-007, REQ-004 | +| SMX-005 | Provider Accreditation Richness Scoring | — | Benefit from lower risk contribution by maintaining rich accreditation portfolio | Configure accreditation richness weights; manage portfolio scoring | ACC-001, PRV-001 | +| SMX-006 | Profile Scoring Threshold Management | — | — | Configure approval routing thresholds per profile (auto/reviewed/verified/authorized + custom tiers via named-tier list); manage signal weights; enforce SMX-008 (max auto_approve_below: 50) | POL-005, REQ-004 | +| SMX-007 | Policy Enforcement Class Override | — | Contribute policies with declared enforcement_class; receive notification when profile overrides enforcement class | Declare per-profile enforcement class overrides; manage regulatory_mandate flag to protect compliance-class policies from demotion | POL-004, POL-005 | +| SMX-008 | Score Audit Trail | Query risk score and routing decision for own requests; view score_drivers and advisory_warnings | — | Query full Score Record detail including signal breakdown and actor risk history; manage score audit retention | AUD-001, REQ-004 | +| SMX-009 | Scoring Weight Range Enforcement | — | Declare operational Validation Policy scoring_weight between 1 and 100 | Enforce weight range at policy activation; reject out-of-range weights | SMX-001 | +| SMX-010 | Score Breakdown Audit Inclusion | View score breakdown in request audit record | — | Configure score breakdown storage in Audit Store for all scored requests | SMX-001, AUD-001 | + +--- + + +## 22. Composite Service Composition + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| CMP-001 | Composite Service Request | Request a Composite Service as a single catalog item; receive composite entity UUID; track composite execution status via standard request status endpoint | Register Composite Service with constituent specification; implement standard Services API for `self` constituents | Configure Composite Service registration; manage composite catalog items | CAT-001, REQ-007, PRV-001 | +| CMP-002 | Dependency-Ordered Constituent Execution | — | Implement standard Services API for `self` constituents (DCM derives ordering from depends_on declarations) | Configure composition model; monitor execution round progress via status events | CMP-001, PRV-003 | +| CMP-003 | Partial Delivery and DEGRADED State | Receive DEGRADED composite entity when partial delivery is accepted; choose to accept or reject degraded state | Declare partial_delivery_supported and required_for_delivery per constituent; return standard realized state per constituent | Configure accept_degraded_delivery per profile; manage degraded notification urgency | CMP-001, PRV-005 | +| CMP-004 | Composite Compensation | Receive notification and recovery decision when a Composite Service fails; approve or reject compensation | Implement standard decommission for `self` constituents (DCM dispatches in dependency-reverse order); guarantee idempotent decommission calls | Configure compensation timeout; manage PARTIALLY_COMPENSATED orphan detection | CMP-001, LCM-007, DRC-001 | +| CMP-005 | Transparent Constituent Visibility | Query and manage DCM-visible constituent entities independently (when transparency mode); receive constituent-level drift alerts | Declare composition_visibility mode; register transparent constituents with deterministic UUIDs | Configure visibility mode per composite resource type; manage constituent entity lifecycle policies | CMP-001, DRF-001 | +| CMP-006 | Composite Execution Status Tracking | Monitor composite execution round progress via request status; see component-level status during long-running compositions | Send intermediate status events to DCM during execution; declare status_reporting.interval | Monitor composite execution health; configure execution timeout alerts | CMP-001, REQ-008 | +| CMP-007 | Nested Composite Service Composition | Request high-order Composite Services composed of other Composite Services (max depth 3) | Register a Composite Service whose constituents include other Composite Services; declare max_nesting_depth | Configure nesting depth limits; manage nested compensation chains | CMP-001, PRV-009 | +| CMP-008 | Composite Service Nesting Depth Enforcement | — | Declare nesting depth in Composite Service registration | Enforce maximum nesting depth of 3 at placement time; reject deeper compositions | CMP-001 | + +--- + + +## 23. Credential Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| CPX-001 | Resource Credential Issuance | Receive credential metadata and retrieval URL with realized resource; retrieve credential value via authenticated endpoint | Declare credential requirements in Resource Type Spec; receive credential issuance confirmation | Register service_provider with Credential.* resource types; configure credential types and lifetimes per resource type; manage issuance policies | PRV-005, ZTS-002 | +| CPX-002 | DCM Interaction Credential Issuance | — | Validate scoped interaction credential on every DCM dispatch; reject interactions without valid scoped credential (CPX-002) | Configure interaction credential lifetime per profile; manage secrets table for DCM-internal credentials | ZTS-002, PRV-001 | +| CPX-003 | Credential Rotation | Receive rotation notification before old credential expires; retrieve new credential during transition window | Implement rotate endpoint; honor transition window; notify DCM when rotation is complete | Configure rotation schedules and transition windows per credential type; manage pre-expiry rotation warnings | CPX-001, IAM-001 | +| CPX-004 | Emergency Rotation and Security Event Response | Receive immediate notification on emergency rotation; retrieve new credential via fastest channel | Implement immediate revocation with no transition window on security_event trigger | Configure security event triggers; manage emergency rotation audit trail; notify platform admin | CPX-003, OBS-004 | +| CPX-005 | Credential Revocation | Receive revocation notification when credentials are revoked (actor deprovisioned, entity decommissioned); confirm transition to new credential | Implement revoke endpoint with declared SLA; invalidate value immediately on emergency revocation | Manage Credential Revocation Registry; configure revocation cache TTL per profile (PT1M standard, PT30S fsi/sovereign); enforce CPX-007 (decommission blocks on credential revocation) | CPX-001, LCM-007 | +| CPX-006 | Revocation Propagation | — | Refresh revocation cache within profile-governed TTL; validate credential UUID against cache at use time (not only at receipt) | Configure revocation cache TTL; monitor revocation propagation latency; alert on SLA violations | CPX-005, IAM-001 | +| CPX-007 | Audit Trail for Credential Lifecycle | View own credential record history (issue, rotate, revoke events); every value retrieval audited with retrieval_uuid | — | Query full credential audit trail including retrieval count; manage credential audit retention | CPX-001, AUD-001 | +| CPX-008 | IP-Bound Credentials for fsi/sovereign | — | — | Enforce IP binding (bound_to_ip) on all credentials issued for fsi and sovereign profiles | CPX-001, ZTS-002 | +| CPX-009 | Algorithm and Key Usage Declaration | — | Declare algorithm and key_usage on credential records at issuance | Enforce declaration at issuance; reject credentials without declared algorithm | CPX-001 | +| CPX-010 | Idle Credential Detection | Receive notification when credential reaches idle threshold | — | Configure idle_detection_threshold per profile; enforce alert-only action | CPX-001 | +| CPX-011 | Compliance Domain Additive Credential Requirements | — | — | Enforce additive credential requirements when compliance domains are active on a profile | CPX-001, POL-005 | +| CPX-012 | Credential Value Store Isolation (All Profiles) | — | — | Enforce credential values never stored in DCM stores in ALL profiles including homelab | CPX-001 | + +--- + + +## 24. Authority Tier Model + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ATM-001 | Authority Tier Registry | Reference approval decisions by tier name; tier weight resolved dynamically from ordered list | Implement approval workflows that reference tier names (not hardcoded weights) | Manage the ordered authority tier list; control tier positions and gravity values | POL-005 | +| ATM-002 | Custom Tier Definition | — | — | Contribute custom tiers between existing tiers; declare decision_gravity and dcm_gate semantics; requires verified-tier approval | ATM-001, FCM-001 | +| ATM-003 | Dynamic Threshold Configuration | View which tier an action routes to | — | Configure profile approval_routing as named-tier threshold list; adjust score ranges when new tiers inserted | ATM-001, SMX-001 | +| ATM-004 | Tier Registry Change Impact Detection | Receive notification when tier changes affect owned resources or pending approvals | — | Propose tier registry changes; receive tier impact diff report; review SECURITY_DEGRADATION and BROKEN_REFERENCE items; accept degradations via Admin API | ATM-001, AUD-001 | +| ATM-005 | Degradation Review Gate | — | — | Review and explicitly accept each SECURITY_DEGRADATION item before a registry change activates; provide compensating control rationale; must hold verified or authorized tier reviewer role | ATM-004, IAM-001 | +| ATM-006 | Profile Gap Detection | — | — | Receive PROFILE_GAP warnings when tier registry changes leave profile threshold lists incomplete; update threshold lists or acknowledge gap within approval window | ATM-003, ATM-004 | +| ATM-007 | Tier Registry Audit Trail | Query historical tier registry versions and impact reports | — | Access full audit trail of all tier registry changes: proposal, impact assessment, degradation acceptances, activation | ATM-004, AUD-001 | +| ATM-008 | Approval Record Tier Weight Snapshot | — | — | Store tier name and resolved weight at approval record creation; historical records retain weight for audit comparison across regime changes | ATM-001 | +| ATM-009 | Tier Registry Degradation Gate | Receive notification when tier change produces degradation affecting owned resources | — | Block tier registry activation on SECURITY_DEGRADATION items; require verified-tier acceptance per item | ATM-001 | +| ATM-010 | Broken Reference Gate | — | — | Block tier registry activation when BROKEN_REFERENCE items exist (removed tier still referenced in active config) | ATM-001 | +| ATM-011 | Tier Change Impact Report | — | — | Generate and store tier impact report in Audit Store at proposal and activation time | ATM-001, AUD-001 | +| ATM-012 | Profile Gap Warning on Tier Insertion | — | — | Detect PROFILE_GAP when new tier inserted but profile threshold list not updated; emit non-blocking warning | ATM-001, POL-005 | + +--- + + +## 25. Event Catalog + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| EVT-001 | Event Subscription | Subscribe to DCM events via notification service_provider or Message Bus; filter by event type, entity type, urgency; idempotency via event_uuid | Publish standard events when provider actions occur; use reverse-DNS prefix for non-standard events | Configure notification service_provider channels and audience routing | IAM-001, OBS-001 | +| EVT-002 | Request Pipeline Events | Receive real-time status of own requests (submitted → intent_captured → policies_evaluated → requires_approval → approved → dispatched → realized/failed) | — | Configure request event delivery per profile; manage urgency routing | REQ-001 | +| EVT-003 | Entity Lifecycle Events | Receive entity lifecycle events (realized, state_changed, ttl_warning, decommissioning, etc.) for owned entities and entities with stakes | — | Configure entity event delivery; manage stakeholder audience routing | LCM-001 | +| EVT-004 | Security and Critical Events | Receive critical security events (audit chain alerts, sovereignty violations, unsanctioned provider writes) regardless of subscription preferences | — | Configure non-suppressable event delivery; manage security team routing | AUD-001, ZTS-001 | +| EVT-005 | Approval Pipeline Events | Receive approval events (requires_approval, decision_recorded, quorum_reached, window_expiring, expired) for own requests and approvals | — | Configure reviewer notification routing; manage approval window alerts | ATM-001, IAM-001 | +| EVT-006 | Provider and Infrastructure Events | — | Publish provider health events (registered, healthy, unhealthy, degraded); publish provider_update events on entity changes | Monitor provider health events; configure provider degradation alerts | PRV-001 | +| EVT-007 | Tier Registry and Governance Events | — | — | Receive tier_registry events (proposed, impact_assessed, degradation_detected, activated); configure governance event routing | ATM-004 | + +--- + + +## 26. API Versioning + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| VER-001 | Version Discovery | Discover available API versions and their status via `GET /.well-known/dcm-api-versions`; learn current, supported, and deprecated versions; get changelog and migration guide URLs | Declare supported OIS version in capability registration | Monitor version adoption; manage sunset schedules | — | +| VER-002 | Breaking Change Governance | Receive at least the profile-governed deprecation notice period before a breaking change takes effect; continue using deprecated versions until sunset date | Receive OIS version deprecation notice; migrate to new OIS version before sunset | Declare new major versions; configure deprecation timeline per profile; ensure VER-002 (breaking change definition) is applied | VER-001 | +| VER-003 | Deprecation Headers | Receive `Deprecation`, `Sunset`, and `Link` headers on all responses from deprecated API versions (RFC 8594/RFC 9745); use these to drive migration priority | — | Configure header injection for deprecated versions; ensure headers are accurate | VER-002 | +| VER-004 | Migration Guide | Access machine-readable migration guide at `GET /api/v{N}/migration-guide`; understand all breaking changes from previous version with migration instructions | Access OIS migration guide at `GET /provider/api/v{N}/migration-guide` | Maintain migration guides for all new major versions (required by VER-008) | VER-002 | +| VER-005 | Preview Endpoints | Access preview endpoints at `/api/v{N}/preview/`; understand stability commitment is none; provide feedback before graduation | — | Mark endpoints as preview; graduate to stable in new major version | VER-001 | +| VER-006 | Latest Alias Production Warning | — | — | Support `latest` version alias; emit response header discouraging production use | VER-001 | +| VER-007 | Preview Endpoint Instability Declaration | — | — | Mark preview endpoints explicitly; may change or be removed without major version bump | VER-001 | +| VER-009 | Provider Dispatch Compatibility | — | Maintain backward compatibility with DCM dispatch payloads from supported prior versions | Manage provider dispatch versioning; maintain supported version matrix | VER-001 | + +--- + + +## 27. Session Revocation + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| SES-001 | Session Lifecycle Management | View own active sessions; logout single session (DELETE /api/v1/auth/session); logout all sessions; revoke specific session by UUID | — | Force revoke sessions for any actor; view session store health | IAM-001, AUTH-001 | +| SES-002 | Actor Deprovisioning Session Revocation | — | — | Parallel session + credential revocation on actor deprovisioning; deprovisioning not acknowledged until both complete (AUTH-016) | SES-001, CPX-005 | +| SES-003 | Emergency Session Revocation | Receive critical notification on security-event session revocation | — | Trigger emergency revocation (security_event); revocation propagates within profile SLA (PT5S sovereign to PT30S standard) | SES-001, EVT-001 | +| SES-004 | Token Introspection | — | Call POST /api/v1/auth/introspect to validate bearer tokens without maintaining own revocation cache | Configure introspection endpoint access; manage introspection scope grants | SES-001, IAM-001 | +| SES-005 | Concurrent Session Enforcement | Oldest session auto-revoked when new session exceeds concurrent limit; receive notification via notification service_provider | — | Configure concurrent_sessions limit per profile; monitor session counts | SES-001 | +| AUTH-017 | Session Revocation Propagation SLA | — | — | Propagate revocation to Session Revocation Registry within profile SLA (PT5S sovereign → PT30S standard) | SES-001 | +| AUTH-018 | Per-Request Revocation Registry Check | — | Check Session Revocation Registry on each request bearing a bearer token | Configure revocation registry query path; manage registry availability | SES-001 | +| AUTH-019 | Emergency Revocation No-Grace Period | Receive critical notification on emergency revocation | — | Emergency session revocation fires immediately with no grace period | SES-003 | +| AUTH-020 | Introspection Endpoint Authentication | — | Authenticate introspection calls using provider interaction credential | Configure introspection scope grants; manage endpoint access | SES-004 | +| AUTH-021 | Oldest Session Revocation on Limit | Receive notification when oldest session is auto-revoked | — | Configure concurrent_sessions limit; enforce oldest-first revocation order | SES-005 | +| AUTH-022 | Refresh Token Invalidation on Session Revoke | — | — | Invalidate refresh token when parent session is revoked; return REVOKED_SESSION error on use | SES-001 | + +--- + +## 28. Internal Component Authentication + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ICOM-001 | Component Identity and mTLS | — | — | Manage Internal CA; issue and rotate component certificates; all inter-component calls use mTLS (ICOM-001) | ZTS-001, CPX-001 | +| ICOM-002 | Component Bootstrap | — | — | Generate one-time bootstrap tokens (PT1H max lifetime); components acquire first certificate via bootstrap token; token invalidated after single use (ICOM-007) | ICOM-001 | +| ICOM-003 | Internal Call Authorization | — | — | Declare allowed_sources per internal endpoint; declare allowed_targets per component; unauthorized source calls rejected with ICOM_UNAUTHORIZED_SOURCE audit record (urgency: high) | ICOM-001, AUD-001 | +| ICOM-004 | Internal Interaction Credentials | — | — | Every internal call presents a scoped ZTS-002 interaction credential in addition to mTLS; credential scoped to specific operation and target component | ICOM-001, CPX-002, ZTS-002 | +| ICOM-005 | Component Certificate Revocation | — | — | Compromised component certificates added to Internal CA CRL immediately; CRL cache refresh within profile SLA (PT15S sovereign to PT1M standard); ICOM_CERT_COMPROMISED audit record (urgency: critical) | ICOM-001, AUD-001 | +| ICOM-006 | Component Certificate Maximum Validity | — | — | Issue internal component certificates with maximum validity P90D; enforce expiry and rotation | ICOM-001 | +| ICOM-008 | Compromised Certificate Immediate CRL | — | — | Add compromised internal component certificates to Internal CA CRL immediately; propagate within PT60S | ICOM-001 | +| ICOM-009 | Trust Anchor Registration | — | — | Register root or intermediate CA as trust anchor for internal mTLS; reject certificates not chaining to registered trust anchor | ICOM-001 | + +--- + + +## 29. Scheduled and Deferred Requests + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| SCH-001 | Request Scheduling | Submit requests with schedule.dispatch: at/window/recurring; SCHEDULED requests visible in GET /api/v1/requests; cancellable before dispatch; receive request.scheduled event | — | Manage Maintenance Windows; configure Request Scheduler; monitor scheduled queue depth | REQ-001 | +| SCH-002 | Maintenance Windows | Reference maintenance windows in scheduled requests; view available windows at GET /api/v1/maintenance-windows | — | Create/manage/suspend maintenance windows; approve window schedules; configure platform-wide windows | SCH-001, GOV-001 | +| SCH-003 | Dual Policy Evaluation | — | — | Understand that scheduled requests run compliance validation policies at declaration AND at dispatch; dispatch-time failure → FAILED with schedule_policy_rejection (SCH-003) | SCH-001, POL-001 | +| SCH-004 | Deadline Enforcement | Set not_after on scheduled requests; receive request.failed(schedule_deadline_missed) if deadline passes without dispatch | — | Monitor deadline miss rates; configure alerting on deadline misses | SCH-001, EVT-001 | +| SCH-005 | Not-After Expiry Failure | Receive FAILED status when scheduled request expires before dispatch | — | Configure not_after enforcement; manage SCHEDULE_EXPIRED recovery policy | SCH-001 | +| SCH-006 | Maintenance Window Platform Authorization | — | — | Require platform_admin or tenant_admin authority to create/modify Maintenance Windows | SCH-004 | + +--- + +## 30. Request Dependency Graph + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| RDG-001 | Dependency Group Submission | Submit POST /api/v1/request-groups with requests and depends_on declarations; local refs within submission; receive group_uuid and per-request entity_uuids | — | Monitor group queue depth; configure max group size | REQ-001 | +| RDG-002 | Field Injection | Declare inject_fields to pass realized output fields (e.g. IP address) from dependency into dependent request fields automatically at dispatch time | — | — | RDG-001, REQ-001 | +| RDG-003 | PENDING_DEPENDENCY Status | Track dependent requests in PENDING_DEPENDENCY status; cancel pending requests individually or cancel whole group; receive request.pending_dependency and request.dependency_met events | — | Monitor PENDING_DEPENDENCY queue depth; detect stalled groups | RDG-001, EVT-001 | +| RDG-004 | Group Failure Handling | Configure on_failure: cancel_remaining or continue; group-level timeout; group status via GET /api/v1/request-groups/{uuid} | — | Monitor group failure rates | RDG-001 | +| RDG-005 | Group-Level Timeout Enforcement | Receive TIMEOUT failure when group-level timeout elapses | — | Configure group_timeout independent of individual request timeouts | RDG-001 | +| RDG-006 | Single Group Membership Enforcement | — | — | Reject attempts to add a request to more than one dependency group | RDG-001 | + +--- + +## 31. DCM Self-Health + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| HLT-001 | Liveness Probe | — | — | GET /livez: fast liveness check (PT5S max, no external calls); Kubernetes restarts pod on failure; unauthenticated | — | +| HLT-002 | Readiness Probe | — | — | GET /readyz: checks Session Store, Audit Store, Policy Engine, Message Bus, Auth Provider connectivity; Kubernetes removes from LB on failure; startup sequence observable via readyz | — | +| HLT-003 | Component Health Detail | — | — | GET /api/v1/admin/health: per-component status (pass/warn/fail), metrics, queue depths, provider/auth summary; admin auth required | IAM-001 | +| HLT-004 | Prometheus Metrics | — | — | GET /metrics: Prometheus scrape endpoint; request pipeline, policy, session, drift, provider, internal CA metrics | OBS-001 | +| HLT-005 | Prometheus Metrics Endpoint | — | — | Expose Prometheus-compatible metrics at GET /metrics including request throughput, store latency, policy eval time, provider health counters | HLT-001 | +| HLT-006 | Startup Readiness via /readyz | — | — | Report PASSING on /readyz only after all required stores available and bootstrap complete | HLT-001 | + +--- + + +## 32. Operational Reference + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| OPS-001 | Data Store Partitioning | — | — | Declare partitioning strategy in deployment manifest; execute tenant-shard, per-tenant, or time-based archiving migration; configure shard routing and mirror lag monitoring | STO-001 | +| OPS-002 | Store Migration | — | — | Execute dual-write migration between store implementations; maintain audit chain continuity across cutover; enforce profile-governed burn-in before source decommission | STO-001, AUD-001 | +| OPS-003 | Disaster Recovery | — | — | Execute scenario-specific recovery procedures (component/store/full-CP/repave); meet profile-governed RTOs (PT1M–PT15M component, PT5M–PT2H store, PT5M–PT30M full-CP); complete post-recovery validation checklist | HLT-001, AUD-001 | +| OPS-004 | Backup Management | — | — | Configure PostgreSQL backup schedules (PITR for all data domains); enforce P365D minimum audit retention | STO-001 | + +--- + + +## 33. Web Interfaces + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| GUI-001 | Consumer Portal — Catalog, Requests, Resources | Browse catalog; live cost estimate; submit requests with scheduling and dependency groups; live SSE status with constituent tracking; cross-resource drift report; consumer-scoped audit trail with correlation ID trace | — | — | CAT-001, REQ-001, SCH-001, RDG-001, EVT-002 | +| GUI-002 | Consumer Portal — Live Request Status | Real-time status via SSE stream (status_change, progress_updated, approval events, heartbeat); constituent status for composite requests; approval flow inline; fallback to polling | — | — | REQ-001, EVT-002 | +| GUI-003 | Consumer Portal — Resource Management | View/filter owned resources by state and type; lifecycle state badges; drift indicator; resource detail with Overview/Drift/Audit/Cost/Credentials/Relationships/Groups tabs; state-sensitive action buttons; bulk operations | — | — | LCM-001, DRF-001, AUD-001 | +| GUI-004 | Consumer Portal — Session and Security | View active sessions; revoke individual or all other sessions; step-up MFA prompt for gated operations; tenant context selector; role-gated navigation (hide not disable) | — | — | SES-001, IAM-001 | +| GUI-005 | Admin Panel — Platform Dashboard | Control plane component health grid; provider health summary; pending approvals count; open drift records by severity; request throughput; all driven by GET /api/v1/admin/health | — | Platform Admins, SREs configure dashboard widgets; role-gated sections | HLT-003 | +| GUI-006 | Admin Panel — Governance and Approvals | Approval queue (all tenants); approval detail with risk score breakdown; authority tier registry editor (drag-and-drop reordering, impact report visualization, degradation acceptance flow); scoring threshold editor (auto_approve_below ≤ 50 hard-stop) | — | Policy Owners and Platform Admins | ATM-004, SMX-001 | +| GUI-007 | Admin Panel — Audit and Compliance | Platform-wide cross-tenant audit trail; pre-built compliance reports (SOC 2, FedRAMP, HIPAA); audit chain integrity status; correlation ID trace; session and security event feed | — | Auditors, Security team, Platform Admins | AUD-001, SES-003 | +| GUI-008 | Provider Management — Common Shell | Overview, configuration, health history, audit trail, and notification tabs per provider, organized by declared CAPABILITY (not a fixed 11-type taxonomy — ADR-005); provider owner role gates access; Platform Admins see all providers | — | Provider owners manage own providers; Platform Admins manage all | PRV-001, IAM-001 | +| GUI-009 | Provider Management — Type Extensions | Service Provider: capacity, managed entities, naturalization mapping, realization history; secrets management: inventory, rotation, revocation, external CA config, algorithm compliance; authentication capability: session stats, SCIM sync, connection status (auth is a capability, not a standalone provider type — ADR-005); external policy evaluation: trust level, contribution pipeline | — | Provider owners access type-specific tabs for their provider type | GUI-008, PRV-001 | +| GUI-011 | RHDH Plugin Suite | Use DCM capabilities within Red Hat Developer Hub (RHDH) or Backstage via Dynamic Plugins (@dcm/backstage-plugin-*); no RHDH rebuild required for updates | — | Configure RHDH app-config.yaml with DCM connection; configure Dynamic Plugin loading | +| GUI-012 | Scaffolder Template Auto-Generation | DCM catalog items automatically generate Backstage Software Templates; new resource types appear as templates without UI code; field schema → JSON Schema → Scaffolder form | — | Configure @dcm/backstage-plugin-catalog-backend; template generation is automatic | +| GUI-013 | DCM Entity Provider | DCMService (catalog items) and DCMResource (realized entities) appear in RHDH Software Catalog; entities sync every PT5M; search-indexed; tenancy enforced via namespace | — | Service account credential configuration; sync interval configuration | +| GUI-014 | ITSM Integration Bridge | View ITSM references (ServiceNow, Jira) on resource entity Overview tab; link change records to DCM requests; see ITSM-sourced approval votes in request status; CMDB reference on entity pages | ITSM systems receive DCM lifecycle events via notification service_provider and call Admin API to record approval votes; CMDB sync via webhook subscription | Configure ITSM notification service_provider; map DCM event → ITSM action; configure CMDB field mapping | EVT-001, GUI-002 | +| GUI-010 | Unified Shell | Single DCM web application with role-gated surfaces: Consumer Portal (all actors), Admin Panel (platform roles), Provider Management (provider_owner role), Flow GUI link (policy_owner/sre); one login, one session; no separate applications | — | Platform Admins configure which surfaces are available | IAM-001, SES-001 | + +--- + + +## 34. ITSM Integration + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| ITSM-001 | ITSM integration Registration | — | Register as ITSM integration with declared capabilities (supported_actions, itsm_system, field_mapping_ref, cmdb_ci_type_map); implement standard OIS health check | Register ITSM integrations; review and approve ITSM integration registrations; configure inbound webhook authentication | PRV-001, CPX-001 | +| ITSM-002 | Outbound ITSM Record Creation | View ITSM references on resource entities (change request, incident, CMDB CI links with deep links to ITSM system) | Receive action requests from DCM; create/update records in ITSM system; return record ID for storage on entity | Configure ITSM Policies (create_change_request, create_incident, update_cmdb_ci); configure block_until_created for compliance gates | ITSM-001, POL-001 | +| ITSM-003 | Inbound ITSM Approval Routing | — | Verify HMAC signature on inbound webhook; forward ITSM approval decisions to DCM Admin API approval vote endpoint | Configure inbound webhook secret (secrets management); monitor approval routing from ITSM systems (ServiceNow CAB, Jira workflow) | ITSM-001, CPX-001, ATM-001 | +| ITSM-004 | ITSM Policy Authoring | — | — | Author ITSM Action policies with template expressions; configure shadow validation; configure on_failure behavior; use block_until_created for pipeline gates (with mandatory timeout per ITSM-005) | ITSM-001, POL-001 | +| ITSM-005 | CMDB Synchronization | View CMDB CI reference on resource entities; CI auto-created on realization, auto-retired on decommission | Receive create_cmdb_ci and retire_cmdb_ci actions; maintain dcm_entity_uuid correlation on CMDB CI records | Configure CMDB CI type mapping per resource type; monitor CMDB sync failures | ITSM-001, ITSM-002 | +| ITSM-006 | ITSM Field Mapping Declaration | — | — | Declare field mappings between DCM entity fields and ITSM CI types in ITSM integration config; validate against Resource Type Specs | ITSM-001 | +| ITSM-007 | ITSM Policy Template Expression Validation | — | — | Validate template expressions in ITSM Policy action_payload at policy activation; reject unresolvable expressions | ITSM-001, POL-003 | + +--- + + +## 35. Provider Callback Authentication + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------| +| PCA-001 | Two-Layer Provider Callback Authentication | — | Present valid mTLS certificate (Layer 1) and provider callback credential (Layer 2) on every call to DCM callback endpoints | Configure DCM CA trust anchor; issue provider callback credentials at activation; enforce both layers | PRV-001, ZTS-001 | +| PCA-002 | Provider Callback Credential Scope Enforcement | — | Use callback credential scoped to own provider_uuid only; cannot act on other providers | Enforce credential scope at validation; reject cross-provider credential use | PRV-001 | +| PCA-003 | Entity-Level Callback Authorization | — | Receive 403 ENTITY_NOT_OWNED_BY_PROVIDER when pushing state for entities not dispatched to this provider | Enforce per-call entity ownership check independent of credential validity | PRV-001, REQ-007 | +| PCA-004 | Scope Violation Auto-Suspension | Receive critical notification when owned provider is suspended due to scope violations | — | Auto-suspend provider and notify platform admin after 5 consecutive scope violations within PT1H | PRV-001, ZTS-001 | +| PCA-005 | Callback Credential Issued by secrets management | — | Retrieve callback credential via secrets management at activation; not directly from API Gateway | Issue callback credentials exclusively through secrets management; reject direct credential issuance requests | PRV-001, CPX-001 | +| PCA-006 | Registration Token Single-Use Enforcement | — | Use registration token for initial registration only; obtain callback credential after activation | Invalidate registration token after first successful use regardless of expiry timestamp | PRV-001 | +| PCA-007 | Sovereignty Change Re-Registration | — | Submit new registration with new registration token when sovereignty declaration changes | Require new registration token and approval pipeline for sovereignty declaration changes | PRV-001, GMX-004 | +| PCA-008 | Callback Credential Pre-Expiry Rotation | — | Implement credential refresh; receive new credential before old expires during transition window | Initiate rotation before expiry; maintain transition window (50% of credential lifetime) | PRV-001, CPX-001 | +| PCA-009 | IP-Bound Callback Credentials for fsi/sovereign | — | Present callback calls from declared bound_to_ip address for fsi/sovereign profiles | Enforce IP binding on callback credentials for fsi and sovereign profiles | PCA-001, CPX-008 | +| PCA-010 | All Inbound Provider Calls Produce Audit Records | — | — | Write audit record for every inbound provider call including rejected calls; no silent failures | PCA-001, AUD-001 | + +--- + + +## 36. Workload Analysis + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------|| WLA-001 | Automated Workload Classification | View workload profile for owned resources; see archetype, resource type match, confidence | Report resource metadata via discovery for classification input | Configure classification ruleset version; review low-confidence classifications manually | DRC-001, INF-001 | +| WLA-002 | Migration Readiness Scoring | View containerization score and migration blockers for owned resources | Report workload characteristics that inform migration scoring | Configure migration readiness thresholds; integrate MTA Information Provider | WLA-001 | +| WLA-003 | MTA Information Provider Integration | — | Implement workload_analysis information type OR delegate to MTA | Register MTA as Information Provider; configure analysis trigger policies | WLA-001, INF-001 | +| WLA-004 | On-Demand Re-Analysis | Request re-analysis when resource role changes (`POST /resources/{uuid}/workload-profile:analyze`) | — | Trigger re-analysis for any resource; override archetype manually with reason | WLA-001 | +| WLA-005 | WorkloadProfile Audit Chain | View analysis history for owned resources | — | Query full analysis history including superseded profiles | WLA-001, AUD-001 | +| WLA-006 | Low-Confidence Manual Review Gate | Receive notification when owned resource requires manual classification | — | Review and resolve low-confidence classifications; unblock ingestion | WLA-001, LCM-001 | + +--- + + +## 37. Accreditation Monitoring + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------|| ACM-001 | Tier 1 External Registry Verification | — | Declare `external_registry_id` at registration for FedRAMP, CMMC, ISO 27001 | Configure registry poll intervals; review status change alerts; manage `external_registry_id` accuracy | PRV-001, ACR-001 | +| ACM-002 | Tier 2 Document Currency Verification | — | Maintain current `certificate_ref` and `audit_report_ref` URLs pointing to valid, accessible documents | Configure `max_age` per framework; review document expiry alerts; upload new reports when notified | PRV-001, ACR-001 | +| ACM-003 | Tier 3 Contract Webhook Integration | — | Configure contract management webhook for BAA and DoD IL accreditations | Register contract system; configure inbound webhook credential; receive BAA/contract lifecycle events | PRV-001, ACR-001 | +| ACM-004 | Verification Staleness Enforcement | — | Ensure monitoring infrastructure can reach DCM to deliver verification events | Configure stale_after thresholds and stale_action per profile; enforce sovereign/fsi minimum tier requirements | ACM-001, ACR-001 | +| ACM-005 | Immediate Revocation on External Revoke | Receive notification when provider accreditation is revoked; understand service impact | — | Review `accreditation.status_changed` events; confirm immediate revocations; trigger recovery policy | ACM-001, ACR-001 | +| ACM-006 | Verification Currency in Scoring | — | Maintain verification currency to maximize accreditation richness score | Monitor verification multiplier impact on provider placement; prioritize externally verified providers | ACM-001, SMX-001 | +| ACM-007 | Manual Override in Air-Gapped Mode | — | — | Manually update `last_verified_at` with justification in air-gapped deployments; maintain audit trail of manual verifications | ACR-001, AUD-001 | + +--- + + +## 38. Location Topology Management + +| ID | Capability | Consumer | Service Provider | Platform/Admin | Depends On | +|----|-----------|---------|---------|---------------|-----------|| LOC-001 | Location Type Registry | Browse available location types (standard and custom) | Declare supported locations in provider registration capability declaration | Register custom location types; manage standard type definitions; deprecate types | PRV-001 | +| LOC-002 | Location Node Management | Browse available locations via `GET /api/v1/locations`; filter by resource type, data classification, sovereignty zone | Declare which location nodes (DC, Zone, etc.) the provider serves at registration | Create, version, and retire location layer instances via GitOps; update mutable capacity fields | LOC-001 | +| LOC-003 | Location Selection at Request Time | Submit `location_uuid` or `location_handle` with service request; select at any level (Country through Rack); DCM refines to specific DC at placement | — | Configure default location selection rules; enforce location-based placement policies | LOC-001, LOC-002 | +| LOC-004 | Location Layer Assembly | Transparent — full location context injected into payload automatically | Receive full location context in dispatch payload (location.country_code, location.zone_code, location.dc_code, etc.) | Configure layer assembly order; define location-level field overrides | LOC-002, DLM-001 | +| LOC-005 | Location-Based Sovereignty Enforcement | See sovereignty zone and data residency on each location node | Declare sovereignty capabilities per served location | Configure `max_data_classification` per location; enforce cross-border policies at location layer | LOC-002, GOV-001 | +| LOC-006 | Location Hierarchy Navigation | Browse parent/child location relationships; query ancestors of a selected node | — | Manage location hierarchy; validate acyclicity on layer submission | LOC-001, LOC-002 | +| LOC-007 | Custom Location Types | Use custom location types in selection (e.g., Fleet/Ship in Navy context) | Declare support for resources at custom location types | Register and manage custom types; define level insertion point in hierarchy | LOC-001 | +| LOC-008 | Location Capacity Visibility | See `capacity_status` (available/limited/full) and `providers_available` count per location | Report capacity scoped to location during reserve query | Update mutable capacity fields (e.g., rack_units_available) without a new layer version | LOC-002, PRV-001 | + +--- + + + + +## 39. Subscription Management (SUB) + +| ID | Capability | Consumer | Provider | Platform/Admin | Dependencies | +|----|-----------|----------|----------|----------------|-------------| +| SUB-001 | Create subscription | Submit subscription request with tier and terms | Acknowledge subscription, provision initial entities | Approve subscription creation | CAT-001, REQ-001, POL-001 | +| SUB-002 | Manage subscription lifecycle | Suspend, resume, cancel own subscriptions | Report managed entity status | Force-cancel, view all subscriptions | LCM-001, AUD-001 | +| SUB-003 | Change subscription tier | Request tier upgrade/downgrade | Adjust capacity for managed entities | Approve tier changes requiring admin | POL-001, POL-003 | +| SUB-004 | Subscription renewal | Approve/decline renewal; view renewal status | Honor renewed terms | Configure renewal policies | SCH-001, POL-001 | +| SUB-005 | Provider-originated updates | Approve/reject non-auto updates | Submit updates via standard callback | View update audit trail | PRV-003, AUD-001 | +| SUB-006 | Update channel management | Configure auto_apply per channel | Declare available update channels | Set organization-wide channel policies | POL-001, POL-003 | +| SUB-007 | Subscription cost attribution | View subscription cost | Declare cost model per tier | Configure cost allocation rules | — | +| SUB-008 | Entitlement enforcement | View remaining capacity | Stay within entitlement bounds | Override entitlements in exceptional cases | POL-002 | +| SUB-009 | Grace period management | View grace period; request extension | Continue health reporting during grace | Configure grace period defaults | LCM-001 | +| SUB-010 | Subscription audit trail | View own subscription audit history | — | View all subscription audit records | AUD-001, AUD-002 | + +## Capability Count Summary + +| Domain | Capabilities | +|--------|-------------| +| Identity and Access Management | 21 | +| Service Catalog | 7 | +| Request Lifecycle Management | 10 | +| Provider Contract and Realization | 17 | +| Resource Lifecycle Management | 7 | +| Drift Detection and Remediation | 5 | +| Policy Management | 7 | +| Data Layer Management | 5 | +| Information and Data Integration | 6 | +| Ingestion and Brownfield Management | 4 | +| Audit and Compliance | 5 | +| Observability and Operations | 8 | +| Storage and State Management | 8 | +| DCM Federation and Multi-Instance | 5 | +| Platform Governance and Administration | 7 | +| Accreditation Management | 6 | +| Zero Trust and Security Posture | 8 | +| Unified Governance Matrix | 7 | +| Drift Reconciliation | 5 | +| Federated Contribution Model | 7 | +| Scoring Model | 10 | +| Composite Service Composition | 8 | +| Credential Management (CPX) | 12 | +| Authority Tier Model | 12 | +| Event Catalog | 7 | +| API Versioning | 8 | +| Session Revocation | 11 | +| Internal Component Authentication | 8 | +| Scheduled and Deferred Requests | 6 | +| Request Dependency Graph | 6 | +| DCM Self-Health | 6 | +| Operational Reference | 4 | +| Web Interfaces | 14 | +| ITSM Integration | 7 | +| Provider Callback Authentication | 10 | +| Workload Analysis | 5 | +| Accreditation Monitoring | 6 | +| Location Topology Management | 7 | +| Subscription Management | 10 | +| **Total** | **312** | +--- + +## Dependency Map — Critical Path Capabilities + +These capabilities block the most downstream work and should be implemented first: + +``` +IAM-001 (Actor Authentication) + └── IAM-002 (Session Tokens) → IAM-003 (RBAC) → IAM-007 (Tenant Scope) + └── CAT-001 (Service Catalog) + └── REQ-001 (Submit Request) + └── REQ-002 (Intent State) → REQ-003 (Layer Assembly) + └── REQ-004 (Policy Evaluation) → REQ-005 (Placement) + └── REQ-007 (Provider Dispatch) + └── PRV-003 (Realization) → PRV-005 (Realized State) + └── LCM-001 (State Transitions) + └── DRF-001 (Discovery) → DRF-002 (Drift) + +PRV-001 (Provider Registration) — parallel critical path + └── PRV-002 (Naturalization) → PRV-003 (Realization) + └── PRV-006 (Capacity Reporting) → REQ-005 (Placement) +``` + +**Minimum viable DCM capability set (to demonstrate end-to-end lifecycle):** + +IAM-001 → IAM-002 → IAM-003 → IAM-007 → CAT-001 → REQ-001 → REQ-002 → REQ-003 → REQ-004 → REQ-005 → REQ-006 → REQ-007 → PRV-001 → PRV-002 → PRV-003 → PRV-004 → PRV-005 → LCM-001 → DRF-001 → DRF-002 → AUD-001 + +### Findings-review additions (2026-07-07) — capability domains flagged as absent + +These domains have ADR backing but were missing from the matrix (consistency review). Rows are +intentionally concise pointers pending the DCM team's detailed capability breakdown: + +| ID | Capability | Description | ADR | +|----|-----------|-------------|-----| +| PLP-001 | Placement Policy | Declarative affinity/anti-affinity/spread/co-locate/pin over abstract Topology kinds; the 8th typed policy; engine evaluates + enforces portability | ADR-019 | +| MIG-001 | Migration & Operational Gating | Migration permission (Governance-Matrix) + sequence (Orchestration-Flow) + freshness gating + rehearsal scheduling | ADR-020 | +| TRU-001 | Trust Model / Introduction Grant | Five trust planes; credential-API selection; the Introduction Grant primitive for provider onboarding | ADR-022 | +| ING-005 | Ingestion — claim/adoption + backport | Reverse placement, provider claim (Discovered→Realized), correlation-id entity resolution, Intent backport | ADR-017 | + +**21 capabilities for a functional end-to-end demonstration.** + +**Note:** FCM-001 through FCM-007 (Federated Contribution Model) are not on the critical path — they extend DCM's multi-user capabilities but are not required for the initial end-to-end lifecycle demonstration. + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/DISCUSSION-TOPICS.md b/architecture/DISCUSSION-TOPICS.md new file mode 100644 index 0000000..e1c0d88 --- /dev/null +++ b/architecture/DISCUSSION-TOPICS.md @@ -0,0 +1,646 @@ +# DCM — Discussion Topics + +> **Status Update (2026-03):** The DCM architecture has been substantially completed. All original 19 architecture review items, 10 session-added items, and 21 community questions have been resolved. The items below represent topics for ongoing community discussion and future evolution. +> +> **Current state:** 0 unresolved architectural questions · 331 capabilities across 39 domains · 58 data model documents · 12 specifications · 4 OpenAPI schemas · 6 JSON schemas · unified provider model (5 capability types) · 2 policy evaluation modes · 9 control plane services +> +> **Foundation:** Three abstractions (Data · Provider · Policy) · Unified Governance Matrix · Federated Contribution Model · Full OPA/Rego validation complete + +## Open Community Discussion Topics + +### 1. Kubernetes / CNCF Strategy (community decisions) +- CNCF submission scope: Operator Interface Spec as specification project first; DCM project after Level 2 reference implementation +- SIG App Delivery and SIG Cluster Lifecycle engagement before Sandbox submission +- Named adopters and TOC sponsor: project team action items +- Level 2 conformance scope now formally defined + +### 2. Implementation Decisions (engineering decisions) +- KubeVirt reference implementation timeline → team estimates against defined Level 2 scope +- SDK language support beyond Go → community SDKs encouraged; Go SDK is reference implementation +- Non-Kubernetes container runtime support → implementation detail + +### 3. Future Evolution Topics +- Normative data specifications (JSON Schema / OpenAPI) — the code-generation layer +- AI/ML Provider type — as DCM becomes AI-ready, a dedicated ML workload provider type +- Billing Provider type — deeper integration with enterprise billing and showback systems +- CMDB Provider type — dedicated contract for CMDB integration +- Multi-cloud federation model — extending DCM federation to public cloud providers +- GitOps PR UX improvements — better tooling for policy review workflow +- **UDLM as the universal observability export** *(added 2026-06-07)* — make + UDLM-modeled data THE export surface for monitoring, alerting, logging, and + audit consumption: telemetry entities + the event catalog exposed with + discoverable schemas so *any* tool (metrics TSDB, log aggregator, SIEM, + alerting pipeline) subscribes through one uniform, policy-scoped, + retention-governed interface — no per-tool adapters. This is the UDLM goal + ("one universal way to export data, consumable by any tool") applied to the + observability domain; pairs OBS-002/OBS-003 with the UDLM event catalog and + schema-sharing contracts. Validation use case: + `dav/use-cases/observability/udlm-universal-telemetry-export.yaml`. + DCM does not have to be the arbiter of the data — but it MAY be: where no + platform exists or a canned solution is desired, a packaged + **dcm-observability** component serves as the authoritative + telemetry/monitoring platform (provider-contract §7 / PRV-007). + First consumer and reference-implementation test bed: a homelab + observability stack. +- **Brownfield inventory ingestion — adopt, don't recreate** *(added + 2026-06-07)* — migrating existing configuration-management estates (Ansible + inventory) into DCM. Patterns established from live homelab migrations: + (1) inventory groups → DCMGroups, host vars → entity attributes, with + brownfield provenance; (2) **adopt-in-place is a hard requirement** — a + Quay bucket migration to claim-based provisioning forced a data copy + because the provisioner could not adopt the existing bucket; at estate + scale that is prohibitive, so DCM ingestion must adopt resources where + they stand; (3) embedded plaintext credentials (real finding: inventory + carried jump-host/Pi/PiKVM passwords in cleartext) convert to vault-backed + credential resources and get flagged for rotation; (4) coexistence — + legacy-tool changes surface as drift, and ingestion stays reversible until + cutover. Validation use case: + `dav/use-cases/cross-domain/ansible-inventory-brownfield-ingestion.yaml`. + + +### 5. Kessel Integration (pre-implementation evaluation) +- Evaluation document written: `44-kessel-integration-evaluation.md` +- **Action required:** Discussion with Kessel development team to validate assumptions before any implementation work begins +- Key questions: API stability, sovereign/air-gapped deployment model, SpiceDB schema extensibility, resource type registry extensibility, HA/DR patterns +- **Do not implement until alignment confirmed** — document is for discussion only +- Two integration paths evaluated: Kessel Relations as Auth Provider (checks 1-2 of five-check model), Kessel Inventory as Discovered State data store +- 10 blocking items identified in doc 44 Section 10 + +### 4. Governance Questions +- Community governance model — how will the DCM project make decisions once public? +- Certified Profile Program — self-certified vs project-reviewed for compliance profiles +- Registry tier promotion — Tier 3 (Organization) to Tier 2 (Verified Community) pathway + +--- + +*See [00-foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md) for the three-abstraction model that resolved the major architectural questions.* + + +--- + +**Original discussion topics (archived):** + +# DCM — Topics for Discussion and Modification + +**Document Status:** 🔄 Active +**Purpose:** A living document capturing topics that require further discussion, design decisions that need revisiting, and new capabilities to be incorporated into the DCM architecture and data model. +**Process:** When a topic is resolved, move it to the appropriate architecture or data model document and mark it resolved here with a reference to where it was documented. + +--- + +## How to Use This Document + +- **Add** any topic that surfaces during design, review, or implementation that needs a decision or deeper discussion +- **Tag** each item with its area, priority, and status +- **Resolve** items by documenting the decision in the appropriate document and updating the status here +- **Never delete** resolved items — keep the full history for audit and traceability + +--- + +## Status Key + +| Status | Meaning | +|--------|---------| +| 🔴 Blocking | Must be resolved before dependent work can proceed | +| 🟡 Active | Under active discussion | +| 🟢 Resolved | Decision made — documented in referenced document | +| ⚪ Parked | Acknowledged but deferred — revisit later | + +--- + +## Priority Key + +| Priority | Meaning | +|----------|---------| +| P1 | Critical — affects foundational architecture | +| P2 | High — affects multiple components or documents | +| P3 | Medium — affects a specific component or document | +| P4 | Low — enhancement or refinement | + +--- + +## Open Topics + +--- + +### TOPIC-001 — Webhook Integration + +**Area:** Control Plane, Provider Contract, Egress +**Priority:** P2 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Notification Model (doc 23) supersedes standalone webhooks. Webhooks are one delivery channel within the notification model. Full event taxonomy in doc 33 (Event Catalog). See AI Prompt Sections 14, 25, 48. +**Raised:** 2026-03 + +#### Description + +DCM needs a webhook integration model that allows external systems to be notified of DCM events and state changes in real time. Webhooks are a standard integration pattern that complements the existing API-first model and Message Bus — they enable a push-based notification model for consumers, providers, and external systems that cannot or do not poll DCM. + +#### Use Cases + +**Consumer Notifications** +- Notify a consumer's CI/CD pipeline when a resource request transitions to REALIZED state +- Notify an application team when their Resource/Service Entity enters DEGRADED state +- Notify a Tenant owner when an ownership transfer is initiated or completed +- Notify consumers when a dependency graph node fails during realization + +**Provider Notifications** +- Notify a provider when a new request payload is dispatched to them +- Notify a provider when DCM initiates a discovery request +- Notify a provider when a decommission is requested for one of their entities + +**External System Integration** +- Notify an external ITSM system (ServiceNow, Jira) when a request is created, updated, or completed +- Notify a monitoring system when Entity lifecycle state changes +- Notify a FinOps platform when new Resource/Service Entities are realized or decommissioned +- Notify a compliance system when compliance validation policies fire or sovereignty constraints are applied + +**Operational Notifications** +- Notify SRE teams when provider capacity falls below threshold +- Notify security teams when unsanctioned changes are detected +- Notify auditors when specific policy types are triggered + +#### Design Questions to Resolve + +1. **Webhook registration model** — how do consumers, providers, and external systems register webhooks with DCM? Is registration via the Consumer API, Provider Registration, or a dedicated Webhook API? + +2. **Event taxonomy** — what is the full list of events DCM can emit via webhook? Should this be an extensible registry similar to the Resource Type Registry? + +3. **Payload format** — should webhook payloads use the DCM unified data model format, or a simplified event notification format? Should the full state payload be included or just a reference + event type? + +4. **Authentication and security** — how does DCM authenticate outbound webhook calls? Options include: HMAC signatures, OAuth tokens, mTLS, API keys. How does the receiving system verify the webhook is genuinely from DCM? + +5. **Retry and reliability** — what is DCM's obligation if a webhook delivery fails? Should DCM retry? How many times? With what backoff strategy? What happens if a webhook endpoint is consistently unavailable? + +6. **Ordering guarantees** — are webhook events delivered in order? What happens if events arrive out of order at the receiving end? + +7. **Filtering** — can webhook registrations declare filters — only receive events of specific types, for specific Resource Types, for specific Tenants, or for specific Resource Groups? + +8. **Policy Engine integration** — should Validation Policy and other policy types be able to trigger webhook notifications as a policy action? This would make webhooks a first-class policy response alongside ALERT, REVERT, etc. + +9. **Provider webhook obligations** — should providers be required to support webhook endpoints as part of their Provider Contract? Or is webhook support optional for providers? + +10. **Tenant scoping** — should webhook registrations be scoped to a Tenant, meaning a webhook can only receive events for resources owned by the registering Tenant? Or should there be platform-level webhooks that span Tenants (for SRE/Audit personas)? + +11. **Webhook versioning** — as DCM evolves, webhook payload schemas will change. How are webhook payload versions managed? Should webhook registrations declare which payload schema version they expect? + +12. **Relationship to Message Bus** — DCM already has a Message Bus component. What is the distinction between webhook integration and Message Bus integration? Are webhooks the outbound consumer-facing layer on top of the Message Bus? + +#### Initial Design Thoughts + +Webhooks fit naturally as an **Egress capability** — they are outbound notifications from DCM to external systems, which is consistent with the existing Egress zone in the architecture (Messaging Protocol, Interoperability API). + +The webhook registration model should likely be part of the **Consumer API** for consumer-facing webhooks and part of the **Provider Registration** for provider-facing webhooks. + +Webhook events should be **typed and versioned** — consistent with DCM's universal versioning model. An event type like `entity.state.changed` should have a version, and webhook registrations should declare which version they support. + +Webhook payloads should carry **provenance information** — the event payload should include enough context to trace back to the originating request, entity, and policy that caused the event. This is consistent with DCM's auditability requirements. + +**Policy Engine integration** is particularly interesting — if the Policy Engine can fire webhooks as a response action, it enables real-time governance notifications without requiring consumers to poll DCM. This aligns with the DCM goal of getting actionable information to the right people as fast as possible. + +#### References +- [Resource/Service Entities](https://github.com/croadfeldt/udlm/blob/main/entities/resource-service-entities.md) — provider lifecycle events +- [Service Dependencies](https://github.com/croadfeldt/udlm/blob/main/entities/service-dependencies.md) — dependency failure notifications +- Architecture: Egress zone, Message Bus, API Gateway + +--- + +### TOPIC-002 — Intent Store and Intent Payload Structure + +**Area:** Data Model +**Priority:** P1 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Intent State fully specified in doc 02 (Four States), doc 04 (Examples), consumer-api-spec. Resource type referenced by FQN or UUID (both accepted). See AI Prompt Sections 4, 5, 80. +**Raised:** 2026-03 + +#### Description + +The Intent State is captured when a consumer submits a request but the exact structure of the Intent payload has not been formally specified. The Intent payload is the consumer's raw declared desire — what they asked for before any processing, enrichment, or policy application. It needs a formal definition that is consistent with the four-state model and the layering model. + +#### Questions to Resolve + +1. What fields are required in an Intent payload vs optional? +2. How does the Intent payload reference a Resource Type — by UUID, by fully qualified name, or both? +3. How does the Intent payload declare its Tenant membership? +4. How does the Intent payload declare dependency requirements at the intent level? +5. How does the Intent payload declare group memberships? +6. How is the Intent payload versioned — does it carry a version or is it always a snapshot? + +#### References +- [Context and Purpose](https://github.com/croadfeldt/udlm/blob/main/foundations/context-and-purpose.md) — four states +- [Layering and Versioning](https://github.com/croadfeldt/udlm/blob/main/foundations/layering-and-versioning.md) — Request Layer +- [Resource Grouping](https://github.com/croadfeldt/udlm/blob/main/entities/resource-grouping.md) — Tenant and group membership + +--- + +### TOPIC-003 — Gating Policy vs Validation Policy Distinction + +> **⚠️ Superseded (2026-06-30):** this topic's 2026-03 resolution predates the decision to **merge Gating Policy into Validation Policy**. There is no longer a separate "Gating Policy" type — "gating" is now the `enforcement_class: compliance` property of a Validation Policy. The historical text below is retained as a record of the pre-merge distinction; see `taxonomy/DCM-Taxonomy.md` and `AGENTS.md` for the current model. + +**Area:** Policy Engine +**Priority:** P2 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Gating Policy has enforcement_class (compliance=boolean deny, operational=risk score). Validation has output_class (structural=boolean fail, advisory=completeness score). Gating Policy can both block AND modify. See doc 29 (Scoring Model), AI Prompt Sections 17, 61. +**Raised:** 2026-03 + +#### Description + +The distinction between Gating Policy and Validation policy categories needs better examples and a clearer formal definition. Both involve checking data against rules, but Gating Policy has override authority while Validation is pass/fail only. The boundary between them needs to be unambiguous. + +#### Questions to Resolve + +1. What is the precise trigger condition that makes a policy a Gating Policy vs a Validation policy? +2. Can a Gating policy both block AND modify in the same execution? +3. Are there cases where Validation and Gating Policy would produce different outcomes for the same rule? +4. Should Gating policies require explicit authorization (e.g., only CISO-owned policies can be Gating Policy)? + +#### References +- [Layering and Versioning](https://github.com/croadfeldt/udlm/blob/main/foundations/layering-and-versioning.md) — Policy Layer section + +--- + +### TOPIC-004 — Audit vs Observability Component Separation + +**Area:** Control Plane +**Priority:** P3 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Definitively separate (AUD-013). Opposite trade-offs: audit is 100% accuracy, append-only, 7+ year retention; observability is statistical sampling, downsampled, operational window. Cannot be combined. See doc 16, AI Prompt Section 37.3. +**Raised:** 2026-03 + +#### Description + +The original architecture documents noted that Audit and Observability were separated for a reason that was later forgotten. This needs to be formally resolved — are they truly separate components with distinct responsibilities, or should they be merged? + +#### Initial Thinking + +**Audit** — focused on compliance evidence and transaction traceability. Reads provenance data intrinsic to data objects. Produces compliance reports, audit trails, and interrogation capability for Auditors, Security teams, and SRE personas. Historical record oriented. + +**Observability** — focused on operational visibility — metrics, health, performance, real-time monitoring. Consumes provider lifecycle events, Entity state changes, and system health data. Operational present-state oriented. + +These are likely genuinely separate concerns. Audit is about what happened and why. Observability is about what is happening now. + +#### Questions to Resolve + +1. Are Audit and Observability separate Atomic Components with separate APIs? +2. Do they share a data store or maintain separate stores? +3. How do provider lifecycle events flow to both components? + +--- + +### TOPIC-005 — Message Bus Consumer Ingress Question + +**Area:** Control Plane, Consumer Ingress +**Priority:** P3 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Message Bus supports bidirectional integration. Inbound messages processed as authenticated API calls via registered webhook actor identity. Same Policy Engine evaluation as any API call. See doc 18, AI Prompt Section 25. +**Raised:** 2026-03 + +#### Description + +The original architecture documents raised an unresolved question: should the Message Bus be offered as consumer ingress (inbound) in addition to egress (outbound)? The API-first principle suggests it should be egress only, but there are valid integration scenarios where external systems need to push data into DCM asynchronously. + +#### Questions to Resolve + +1. Should consumers be able to submit requests via the Message Bus, or only via the Consumer API? +2. If Message Bus ingress is supported, how are requests authenticated and authorized? +3. How does Message Bus ingress interact with the Intent State capture — is the message treated as an Intent payload? + +--- + +### TOPIC-006 — Cache Architecture + +**Area:** Data Model, Control Plane +**Priority:** P2 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Hybrid push-pull model. Caches placed closest to consumption, subject to sovereignty. GitOps and Event Streams always authoritative; caches are derived projections. Sovereign DCM uses signed bundles only. See CACHE-001 through CACHE-004, AI Prompt Section 40.2-40.4. +**Raised:** 2026-03 + +#### Description + +Several unresolved questions exist about where data caches live, how they are synchronized, and which cache is authoritative when caches diverge. This has implications for distributed DCM deployments and sovereignty scenarios. + +#### Questions to Resolve + +1. Where should data caches live? Hub DCM? Regional DCM? Sovereign DCM? All locations? +2. Should cache synchronization be push, pull, or both? +3. Which cache is authoritative when caches diverge? +4. What mechanism maintains consistency across distributed caches? +5. How do cache architecture decisions interact with sovereignty constraints — can cached data cross sovereignty boundaries? + +--- + +### TOPIC-007 — Cross-Tenant Dependencies + +**Area:** Data Model, Multi-Tenancy +**Priority:** P2 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Relationship nature governs cross-tenant: constituent=never, operational=dual authorization, informational=unless deny_all. explicit_only is default stance. cross_tenant_authorization DCMGroup required. See doc 09, REL-010/011/012, XTA-001-005, AI Prompt Sections 11.13-11.14, 21.6. +**Raised:** 2026-03 + +#### Description + +The dependency model currently assumes dependencies are resolved within a single Tenant. However, real-world deployments will have cross-tenant dependencies — a Payments Tenant application that depends on a shared DNS service owned by a Platform Tenant. The data model needs to formally address how cross-tenant dependencies are declared, resolved, and governed. + +#### Questions to Resolve + +1. How is a cross-tenant dependency declared — does it reference the Tenant UUID of the dependency owner? +2. What authorization is required for a cross-tenant dependency? Does the owning Tenant need to approve? +3. How does cost attribution work for cross-tenant service consumption? +4. How does drift detection work when a dependency is in another Tenant? +5. Can a compliance validation policy block cross-tenant dependencies? + +--- + +### TOPIC-008 — Provider Trust Validation Mechanism + +**Area:** Service Providers, Provider Contract +**Priority:** P1 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Registration spec defines full approval pipeline (auto/reviewed/verified/authorized). Provider Callback Auth (doc 43, PCA-001-010) specifies two-layer mTLS + credential model. Trust levels: trusted/verified/untrusted. Accreditation model (doc 26) governs ongoing trust. See AI Prompt Sections 53.2, 76. +**Raised:** 2026-03 + +#### Description + +The Provider Contract includes a Trust Contract — providers must be validated and certified to participate in the DCM ecosystem. The mechanism for establishing, maintaining, and revoking trust has not been designed. + +#### Questions to Resolve + +1. What is the certification process for a new provider? +2. What technical mechanism validates trust at request time? +3. How is trust revoked if a provider violates their contract? +4. Should trust be per-provider or per-catalog-item? +5. How does the trust chain interact with sovereignty requirements? + +--- + +### TOPIC-009 — Physical Shared Infrastructure Tenancy + +**Area:** Data Model, Tenancy +**Priority:** P2 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** Three ownership patterns: Whole Allocation (indivisible, provider retains), Allocation (pool yields sub-resources), Shareable (one resource, multiple stakeholders). Physical infrastructure owned by __platform__ Tenant. See doc 04b, OWN-001 through OWN-008, AI Prompt Sections 8.3, 46.2. +**Raised:** 2026-03 + +#### Description + +The current model assigns every Resource/Service Entity to exactly one DCM Tenant. This works cleanly for most cases but edge cases exist for truly shared physical infrastructure — a rack, a network switch, a power circuit — where the ownership model may be genuinely joint or ambiguous. + +#### Questions to Resolve + +1. Is jointly-owned physical infrastructure a valid DCM use case, or is it always owned by a Platform/Infrastructure Tenant? +2. If joint ownership is valid, how is it modeled without breaking the single-Tenant rule? +3. Does the Whole Allocation model cover all physical infrastructure scenarios? + +--- + +### TOPIC-010 — Embedded Technology-Specific Data Bundles + +**Area:** Data Model +**Priority:** P3 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** native_passthrough field sanctioned for genuinely untranslatable provider-specific data. Always audit-logged (content if transparent, hash if opaque). Opaque passthrough blocked in fsi/sovereign by default. See DATA-001, AI Prompt Section 40.5. +**Raised:** 2026-03 + +#### Description + +The original data model discussion raised the question of whether the data model should allow embedded target-technology-specific data bundles — for example, a Terraform HCL block or an Ansible vars file embedded directly in an entity definition. The authors noted "not convinced this is something we want." + +#### Questions to Resolve + +1. Should technology-specific data bundles be allowed in entity definitions? +2. If allowed, must they be in clear text and appropriate for Git storage? +3. How would embedded bundles interact with the portability model — they would clearly be portability-breaking? +4. Is there a better mechanism — such as a provider-specific extension field with an appropriate portability classification? + +--- + +## Resolved Topics + +--- + +### TOPIC-R001 — Field Override Control Mechanism + +**Area:** Data Model, Policy Engine, Layering +**Priority:** P1 +**Status:** 🟢 Resolved +**Raised:** 2026-03 +**Resolved:** 2026-03 + +#### Decision + +Field override control is implemented as a **standard Policy Engine mechanism** using a graduated three-level model: Level 1 (no declaration — fully overridable), Level 2 (simple `override: allow|constrained|immutable`), Level 3 (full actor matrix with per-actor permissions, trusted grants, and expansion rules). The Policy Engine is the sole authority for setting override control. The Request Payload Processor enforces structural layer rules only. + +#### Documented In +- [Data Layers and Assembly Process](https://github.com/croadfeldt/udlm/blob/main/foundations/layering-and-versioning.md) — Section 5a +- [Context and Purpose](https://github.com/croadfeldt/udlm/blob/main/foundations/context-and-purpose.md) — Section 4.4 + +--- + +### TOPIC-R002 — Storage/Networking Bundling vs Dependency Model (Q53) + +**Area:** Data Model, Entity Relationships, Service Dependencies +**Priority:** P1 +**Status:** 🟢 Resolved +**Raised:** 2026-03 +**Resolved:** 2026-03 + +#### Decision + +The conflict between the enhancement documents (storage bundled in VM schema) and the data model (separate first-class entities) is resolved through a **universal Entity Relationship model**: + +- Bundled consumer declarations are expanded by the Request Payload Processor into first-class Resource/Service Entities with their own UUIDs +- The relationship between parent and child entities is expressed using the universal relationship model — bidirectional, UUID-keyed, with lifecycle policies +- The same relationship model is used for ALL entity relationships — compute to storage, application to web server, resource to business unit — minimizing variance +- Lifecycle policies (destroy|retain|detach|notify on parent destroy/suspend/modify) replace the ephemeral/persistent classification +- Expansion rules live in the Resource Type Specification — portable and declarative +- The dependency graph concept is unified into the Entity Relationship Graph + +#### Documented In +- [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) — complete relationship model +- [Information Providers](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers.md) — external data relationships +- [Service Dependencies](https://github.com/croadfeldt/udlm/blob/main/entities/service-dependencies.md) — updated to reference entity relationships + +--- + +### TOPIC-R003 — Information Provider Model + +**Area:** Data Model, Provider Contract +**Priority:** P1 +**Status:** 🟢 Resolved +**Raised:** 2026-03 +**Resolved:** 2026-03 + +#### Decision + +Information Providers are a first-class provider type in DCM. They follow the same registration, health check, trust, and contract model as Service Providers where applicable. Key decisions: + +- Information types live in the same DCM registry as Resource Types — distinguished by category prefix (Business.*, Identity.*, Compliance.*, Operations.*) +- Standard vs extended data — DCM only relies on standard fields for operational decisions; organizations can extend with domain-specific fields +- Stable external key model — DCM UUID wraps external UUID; if external system changes its UUID, only the reference record changes +- Three-mode verification — scheduled (Mode 1), provider push (Mode 2, contractual obligation), on-demand (Mode 3, fallback) +- Internal business data follows the standard resource entity model when DCM owns it; external references use the Information Provider model + +#### Documented In +- [Information Providers](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers.md) +- [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) — external relationship structure +- [Resource Type Hierarchy](https://github.com/croadfeldt/udlm/blob/main/entities/resource-type-hierarchy.md) — information type categories added + +--- + +--- + +### TOPIC-011 — Enhancement Document Compatibility Findings + +**Area:** Data Model, Provider Contract, Policy Engine, Catalog +**Priority:** P1 +**Status:** 🟢 Resolved +**Resolved:** 2026-03 +**Resolution:** All cross-cutting gaps resolved: field-level provenance (doc 00, Section 4.3), UUIDs (universal identity requirement), portability classification (doc 05), sovereignty declarations (doc A provider contract), tenancy (doc 15 universal groups), versioning (semver throughout), deprecation (five-status lifecycle). Enhancement docs superseded by current architecture. +**Raised:** 2026-03 + +#### Description + +A compatibility review of six DCM enhancement documents against the data model identified areas of strong alignment and cross-cutting gaps that need resolution before the enhancements can be considered fully aligned with the data model. + +#### Cross-Cutting Gaps (affect all or most enhancements) + +1. **Field-Level Provenance** — absent in all enhancement documents. The Policy Engine spec mutates request payloads without recording provenance. The KubeVirt SP returns minimal status rather than full DCM-format realized payloads. + +2. **UUIDs as Primary Identifiers** — most specs use `name` as the natural key rather than UUID as the primary key. The data model requires UUIDs as primary identifiers on all entities. + +3. **Portability Classification** — absent from all provider and catalog specs. Every field must declare `universal|conditional|provider-specific|exclusive` classification. + +4. **Sovereignty Declarations** — entirely absent from SP Registration and KubeVirt SP. Provider registration must include sovereignty capability declarations as a contractual obligation. + +5. **Tenant Support** — explicitly deferred in V1 in the Policy Engine spec. The data model treats Tenant as mandatory and non-overridable. A clear migration path from V1 to Tenant support is needed. + +6. **Universal Versioning** — catalog items use `apiVersion: v1alpha1` rather than Major.Minor.Revision. All definitions require universal versioning. + +7. **Deprecation Model** — absent from all enhancement documents. All definitions require the `active → deprecated → retired` lifecycle. + +#### Document-Specific Gaps + +**SP Registration Flow:** +- Registration payload missing: sovereignty declarations, ownership model declaration, dependency declarations, capacity model mode declaration +- Uses `name` as natural key rather than UUID as primary key +- Capacity data (totalCpu, totalMemory) treated as static rather than dynamic — needs reconciliation with three-mode capacity model + +**Service Type Definitions:** +- `providerHints` not formally marked as portability-breaking — silently bypasses portability enforcement +- Storage and networking bundled with compute — conflicts with the service dependency model which treats these as separate dependent services with their own lifecycle +- No UUIDs on field definitions, no provenance metadata, no versioning beyond v1alpha1 + +**Service Provider Health Check:** +- Covers liveness only — insufficient for full Provider Lifecycle Events contract +- Does not address DEGRADATION, MAINTENANCE, UNSANCTIONED_CHANGE, CAPACITY_CHANGE events +- Binary 200/non-200 model needs to coexist with structured event payload model + +**Policy Engine:** +- `selected_provider` as a direct policy output conflicts with the data model's specificity narrowing model for provider selection +- No policy versioning or GitOps integration — data model requires policies maintained via GitOps +- No provenance recording on field mutations — data model requires provenance on every modification +- Constraint immutability model is compatible and can be mapped to `override_preference` field metadata + +**Catalog Item Schema:** +- No UUIDs, no Tenant scoping, no portability classification, no deprecation model, no provenance +- `editable` field concept is valuable and not explicitly covered in data model — worth incorporating into the Resource Type Specification field definition +- `dependsOn` conditional field pattern maps to `conditional` portability classification but needs formal alignment + +**KubeVirt Service Provider:** +- Realized payload returns minimal status rather than complete DCM-format payload — Denaturalization requirement not met +- No unsanctioned change detection — only VMI phase changes reported +- Registration gaps same as SP Registration Flow spec +- `namespace` in response payload is provider-native concept with no DCM equivalent + +#### Questions to Resolve + +1. Should the enhancement documents be updated to align with the data model, or should the data model be adjusted where the enhancements reveal practical implementation constraints? +2. For the storage/networking bundling vs. dependency model tension — is this a V1 simplification that gets resolved in V2, or does the data model need a "monolithic service" concept? +3. For `selected_provider` in the Policy Engine — should provider selection remain a policy output, or should it be moved to a dedicated placement component that consumes narrowed field sets from the Policy Engine? +4. What is the migration path from V1 (no Tenant support) to Tenant-mandatory? +5. Should the `editable` field concept from the Catalog Item Schema be formally incorporated into the Resource Type Specification? + +#### References +- SP Registration: https://github.com/dcm-project/enhancements/blob/main/enhancements/sp-registration-flow/sp-registration-flow.md +- Service Types: https://github.com/dcm-project/enhancements/blob/main/enhancements/service-type-definitions/service-type-definitions.md +- Health Check: https://github.com/dcm-project/enhancements/blob/main/enhancements/service-provider-health-check/service-provider-health-check.md +- Policy Engine: https://github.com/dcm-project/enhancements/blob/main/enhancements/policy-engine/policy-engine.md +- Catalog Item: https://github.com/dcm-project/enhancements/blob/main/enhancements/catalog-item-schema/catalog-item-schema.md +- KubeVirt SP: https://github.com/dcm-project/enhancements/blob/main/enhancements/kubevirt-sp/kubevirt-sp.md + +--- + +Copy the following template and fill in the fields: + +```markdown +### TOPIC-NNN — Title + +**Area:** +**Priority:** +**Status:** 🟡 Active +**Raised:** + +#### Description + + + +#### Questions to Resolve + +1. + +#### References +- +``` + +--- + +*This document is maintained by the DCM Project team. Add topics freely — no topic is too small if it needs a decision.* + +--- + +### 6. Universal Lightspeed Interface for Operations (future concept) + +**Note captured for future exploration.** + +A universal operations interface concept — working title "Lightspeed" — for DCM. Intent is a unified, high-velocity operational surface for all DCM actions regardless of the underlying provider, resource type, or lifecycle stage. Think of it as the operational equivalent of what the unified data model is to data: a single consistent interaction model for humans and automation alike across the full DCM estate. + +**Initial thoughts to explore:** +- Single interface for any operation on any resource managed by DCM — no provider-specific tooling, no context switching +- "Lightspeed" implies minimal friction: operations that currently take multiple steps, approvals, and tool handoffs should be expressible and executable in a single interaction +- Applicable to both human operators (Web UI / CLI) and automation (agentic workflows, AIOps) +- Likely surfaces DCM's existing policy engine, lifecycle model, and unified data model as the execution layer — the interface is the innovation, not new backend capability +- Could be the primary surface for DCM's "AI Ready" design principle (README): agentic workflows operating over DCM's control plane via a natural-language-capable interface that still enforces all policy, sovereignty, and accreditation constraints + +**Questions to answer when this gets scoped:** +- Is this a new GUI surface, a CLI, an AI agent interface, or all three? +- How does it relate to the existing Web UI spec and Flow GUI spec? +- What does "lightspeed" mean operationally — sub-second execution, zero-confirmation for pre-approved patterns, predictive pre-staging? +- How does it interact with the Authority Tier model — can it auto-route approval gates without interrupting the operator's flow? +- Is this the primary interface for the AIOps layer referenced in the README? + +**Status:** Concept note — no design work started. Capture for roadmap planning. + +### 7. Spectral AEP Linter + CI Workflow Adoption (PR #18 review) + +**Source:** [PR #18](https://github.com/dcm-project/dcm/pull/18) by Fale — first draft of Interoperability API and Service Spec design guidelines. + +**PR #18 disposition:** The interoperability API and object design RFC are superseded by the current architecture (4 canonical OpenAPI specs, 58 data model docs). Several conventions in PR #18 (camelCase fields, PascalCase enums, YAML content type, K8s apiVersion/kind/spec/status structure) contradict the team's established patterns (snake_case fields, UPPER_SNAKE_CASE enums, JSON, flat REST entities). PR should be closed with acknowledgment and pointer to current architecture. + +**What to adopt:** The `.spectral.yaml` AEP linter config and the GitHub Actions CI workflow that validates OpenAPI specs against AEP rules. This enforces PRR-006 (AEP.DEV Linting) in CI. + +**Blockers before adoption:** +- The team's greenfield specs use OpenAPI 3.1.0; the AEP Spectral ruleset was built for 3.0.x. Must test for false positives on 3.1 constructs before enabling CI enforcement. +- Determine scope: lint only greenfield specs in `src/api/v1alpha1/`, or also the canonical specs in `dcm/schemas/openapi/`? +- Decide if this goes in dcm-examples only, or also in the dcm architecture repo for the canonical specs. + +**Status:** Future — pending OpenAPI 3.1 compatibility verification. + diff --git a/architecture/WALKTHROUGH.md b/architecture/WALKTHROUGH.md new file mode 100644 index 0000000..4801950 --- /dev/null +++ b/architecture/WALKTHROUGH.md @@ -0,0 +1,581 @@ +# DCM End-to-End Walkthrough — VM Provision + +**Purpose:** Trace a single request through the entire DCM pipeline with real data structures at every stage. This is the single document to read if you want to understand how DCM works in practice. + +**Time to read:** ~20 minutes +**Prerequisites:** None — this document is self-contained. + +--- + +## The Scenario + +A developer on the AppTeam tenant requests a standard Linux VM for a payments API server. The request must comply with EU data residency requirements. The VM requires an IP address — DCM automatically resolves this dependency by requesting an IP from the appropriate IPAM provider, governed by core sovereignty policies and service-specific subnet policies. We follow this request from the consumer's API call through to a running VM and its first discovery cycle. + +--- + +## Stage 1: Consumer Submits Intent + +The developer calls the Consumer API: + +``` +POST /api/v1/requests +Authorization: Bearer +Content-Type: application/json + +{ + "catalog_item_uuid": "compute-vm-standard-uuid", + "fields": { + "cpu_count": 4, + "memory_gb": 8, + "storage_gb": 100, + "os_family": "rhel", + "environment": "production", + "name": "payments-api-server-01" + } +} +``` + +**What the consumer declares:** What they need. Not where it runs, not which provider, not which datacenter. Just the desired outcome. + +**What DCM creates — Intent State:** + +```yaml +entity_uuid: f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0 +resource_type: Compute.VirtualMachine +tenant_uuid: a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6 # AppTeam +submitted_by: b2c3d4e5-actor-uuid +submitted_at: 2026-03-15T09:00:00Z + +fields: + cpu_count: 4 + memory_gb: 8 + storage_gb: 100 + os_family: rhel + environment: production + name: "payments-api-server-01" +``` + +**What happens immediately:** +- Authorization check: actor has `request:compute:vm` permission in AppTeam tenant +- Sovereignty check: AppTeam's tenant has `data_residency: EU-WEST` — this will constrain placement later +- The `entity_uuid` is assigned now and will never change — it links every stage of this request's lifecycle + +**Audit leaf written:** `INTENT_CAPTURED` — SHA-256 hash of the intent payload, signed by the API gateway service. + +> **Key concept:** The consumer never chooses a provider or a datacenter. The control plane handles that. + +--- + +## Stage 2: Layer Assembly + +The Request Processor assembles the full payload by merging data layers in precedence order. Layers are organizational data — datacenter configs, environment defaults, tenant overrides, compliance requirements. + +**Layer chain resolved (highest to lowest precedence):** + +``` +1. system/core/datacenter-layer.yaml → data_center: "EU-WEST-DC1" +2. system/core/environment-layer.yaml → monitoring defaults, log retention +3. system/compliance/eu-west-layer.yaml → backup_policy: "daily-30d-eu-west" +4. org/appteam-defaults-layer.yaml → monitoring_agent: "datadog-agent:7.42" +5. providers/openstack/vm-defaults-layer.yaml → provider-specific defaults +6. Consumer intent → cpu_count: 4, memory_gb: 8, ... +``` + +**Resulting merged payload (selected fields with provenance):** + +```yaml +cpu_count: + value: 4 + provenance: { source_type: consumer, source_uuid: f5e6d7c8-entity } + +memory_gb: + value: 8 + provenance: { source_type: consumer } + +data_center: + value: "EU-WEST-DC1" + provenance: { source_type: base_layer, source_uuid: dc-layer-uuid } + +environment: + value: production + provenance: + origin: { source_type: intermediate_layer, value: dev } + modifications: + - previous: dev → modified: production, source_type: consumer # consumer override + +monitoring_agent: + value: "datadog-agent:7.42" + provenance: { source_type: intermediate_layer, source_uuid: appteam-defaults-uuid } + +backup_policy: + value: "daily-30d-eu-west" + provenance: { source_type: intermediate_layer, source_uuid: eu-west-compliance-uuid } +``` + +**Key concept:** The consumer declared 6 fields. After assembly, there are 10+ fields. The extra fields come from layers — organizational data that the consumer doesn't need to know about but that provisioning requires. + +**Key concept:** Every field carries provenance — where the value came from and what modified it. This is how auditors trace any value back to its origin. + +**Audit leaf written:** `ASSEMBLY_COMPLETE` — hash of the assembled payload with layer chain reference. + +--- + +## Stage 3: Policy Evaluation + +The Policy Engine evaluates all matching policies against the assembled payload. Evaluation follows a three-phase model: + +### Phase 1: Validation Policy (compliance check, pass/fail, no mutations) + +``` +Policy: vm-size-limits (Validation Policy, compliance-class) + Match: resource_type = Compute.VirtualMachine, lifecycle_scope = initial_provisioning + Result: APPROVED — 4 CPU within AppTeam's 16 CPU quota + +Policy: approved-os-images (Validation Policy, tenant-scoped) + Match: resource_type = Compute.VirtualMachine, tenant_uuid = AppTeam + Result: APPROVED — rhel is in AppTeam's approved images list + +Policy: eu-data-residency (Validation Policy, compliance-class, hard enforcement) + Match: data_residency = EU-WEST + Result: APPROVED — data_center value "EU-WEST-DC1" is within EU-WEST zone +``` + +### Phase 2: Transformation (mutations applied) + +``` +Policy: inject-monitoring-endpoint (Transformation) + Match: environment = production, has monitoring_agent + Action: INJECT field monitoring_endpoint + Result: + monitoring_endpoint: + value: "https://metrics.internal.eu-west.example.com" + provenance: { source_type: policy, source_uuid: inject-monitoring-policy-uuid, + reason: "Standard endpoint for EU-WEST production resources" } +``` + +The transformation pass runs again to check for convergence. No new mutations — converged in 1 pass. + +### Phase 3: Post-mutation Validation Policy + +``` +Policy: vm-size-limits — re-evaluated after transformations + Result: APPROVED (no relevant mutations occurred) +``` + +**All policies pass. Request proceeds to placement.** + +**Audit leaves written:** One per policy evaluation with result, constraint emissions, and hash of the payload at evaluation time. + +> **What if a policy blocked?** The request would enter `POLICY_BLOCKED` state and the consumer would receive resolution guidance — compliant values, override options, cancel, or escalate. See ADR-009. + +--- + +## Stage 4: Dependency Resolution + +Before placement can proceed, DCM checks the Resource Type Specification for `Compute.VirtualMachine` and finds a **type-level dependency**: + +```yaml +# From the Resource Type Specification for Compute.VirtualMachine +resource_type: Compute.VirtualMachine +type_level_dependencies: + - required_resource_type: Network.IPAddress + strength: hard + cardinality: one_to_one + description: "Every VM requires exactly one IP address" + payload_fields: # fields to inject from the realized IP into the VM payload + - source: "address" + target: "assigned_ip_address" + - source: "subnet" + target: "network_subnet" + - source: "gateway" + target: "network_gateway" +``` + +The VM cannot be dispatched until this dependency is satisfied. DCM creates a **sub-request** for the IP address — the consumer never sees this; it's an internal orchestration step driven by the resource type definition. + +**IP Address sub-request created:** + +```yaml +entity_uuid: aabb1122-ip-uuid +resource_type: Network.IPAddress +tenant_uuid: a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6 # same tenant as parent VM +parent_entity_uuid: f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0 # linked to the VM + +fields: + address_family: IPv4 + purpose: vm_interface + environment: production + attachment_ref: f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0 # the VM this IP is for +``` + +> **Key concept:** The consumer requested a VM. DCM knows a VM requires an IP address because the resource type spec declares it. The consumer doesn't manage IP allocation — DCM does. + +**Audit leaf written:** `DEPENDENCY_CREATED` — hash of IP sub-request + parent VM entity_uuid. + +--- + +## Stage 5: IP Address — Policy Evaluation and Provider Selection + +The IP sub-request goes through its own policy evaluation pipeline — the same three-phase model as the parent VM, but with policies scoped to `Network.IPAddress`. + +### Core Policies (system-scoped, apply to all IP allocations) + +``` +Policy: ip-sovereignty-zone (Validation Policy, compliance-class, hard enforcement) + Match: resource_type = Network.IPAddress, data_residency = EU-WEST + Check: IP pool must be in EU-WEST sovereignty zone + Result: APPROVED — only EU-WEST pools will be considered + +Policy: ip-subnet-isolation (Validation Policy, system-scoped) + Match: resource_type = Network.IPAddress, environment = production + Check: Production IPs must come from production-designated subnets + Result: APPROVED — filters candidate pools to production subnets only +``` + +### Service Provider Policies (provider-scoped, specific to IPAM capabilities) + +``` +Policy: ipam-pool-selection (Transformation, provider-scoped) + Match: resource_type = Network.IPAddress, purpose = vm_interface + Action: Enriches request with pool selection criteria + Result: INJECT pool_selector field + pool_selector: + value: { zone: "eu-west", environment: "production", address_family: "IPv4" } + provenance: { source_type: policy, source_uuid: ipam-pool-selection-uuid, + reason: "Production VM interfaces use production pool in matching zone" } + +Policy: ip-address-format (Validation, provider-scoped) + Match: resource_type = Network.IPAddress + Check: address_family is valid (IPv4 or IPv6), purpose is recognized + Result: PASSED +``` + +### Placement selects IP Provider + +``` +Sovereignty pre-filter: + Eligible IPAM providers must satisfy data_residency: EU-WEST + → 2 IPAM providers in EU-WEST zone + +Pool capacity query: + EU-WEST-IPAM-1 (InfoBlox): pool 10.1.0.0/16, 65,420 available → confidence 98% + EU-WEST-IPAM-2 (NetBox): pool 10.2.0.0/16, 12,100 available → confidence 91% + +Selection: EU-WEST-IPAM-1 (highest confidence, largest available pool) +``` + +**Audit leaves written:** One per IP policy evaluation + `IP_PLACEMENT_COMPLETE`. + +--- + +## Stage 6: IP Address Realization + +The IP provider (InfoBlox IPAM) receives the sub-request, naturalizes it to its native API, and allocates an address: + +``` +DCM sub-request → InfoBlox API call: + +POST /wapi/v2.12/record:host +{ + "name": "payments-api-server-01.eu-west.internal", + "ipv4addrs": [{ "ipv4addr": "func:nextavailableip:10.1.0.0/16" }], + "comment": "DCM entity aabb1122-ip-uuid, tenant AppTeam" +} +``` + +**IP provider callback:** + +```yaml +entity_uuid: aabb1122-ip-uuid +status: OPERATIONAL +provider_entity_id: "record:host/ZG5z:10.1.45.23" + +realized_fields: + address: "10.1.45.23" + subnet: "10.1.0.0/16" + gateway: "10.1.0.1" + dns_name: "payments-api-server-01.eu-west.internal" + lease_type: static + pool_ref: "10.1.0.0/16" +``` + +> **Key concept:** The IP address is now a first-class DCM entity. It has its own entity_uuid, its own realized state, its own audit trail. When the VM is decommissioned, DCM knows to release this IP back to the pool. + +**Audit leaf written:** `IP_REALIZED` — hash of realized IP state + IPAM provider signature. + +--- + +## Stage 7: Dependency Injection and VM Placement + +Now the IP dependency is satisfied. DCM injects the realized IP data into the VM's payload via **dependency payload passing**: + +```yaml +# VM payload enriched with dependency data +fields: + cpu_count: { value: 4, provenance: {...} } + memory_gb: { value: 8, provenance: {...} } + storage_gb: { value: 100, provenance: {...} } + # ... all previously assembled fields ... + + # Injected from realized IP address dependency + assigned_ip_address: + value: "10.1.45.23" + provenance: + source_type: dependency_payload + source_uuid: aabb1122-ip-uuid # the IP entity + required_resource_type: Network.IPAddress + timestamp: 2026-03-15T09:01:15Z + network_subnet: + value: "10.1.0.0/16" + provenance: { source_type: dependency_payload, source_uuid: aabb1122-ip-uuid } + network_gateway: + value: "10.1.0.1" + provenance: { source_type: dependency_payload, source_uuid: aabb1122-ip-uuid } + +dependencies_satisfied: + - required_resource_type: Network.IPAddress + entity_uuid: aabb1122-ip-uuid + status: SATISFIED + satisfied_at: 2026-03-15T09:01:15Z +``` + +The Placement Engine now scores VM providers (same as before, but the payload now includes the IP): + +``` +Sovereignty pre-filter: + Eligible VM providers must satisfy data_residency: EU-WEST + → 3 OpenStack instances in EU-WEST zone + +Reserve query results: + EU-WEST-Prod-1: capacity available, confidence 94% + EU-WEST-Prod-2: capacity available, confidence 87% + EU-WEST-Prod-3: insufficient capacity — excluded + +Selection: EU-WEST-Prod-1 (highest confidence score) +``` + +**Requested State committed (write-once):** + +```yaml +entity_uuid: f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0 +resource_type: Compute.VirtualMachine +assembled_at: 2026-03-15T09:01:18Z + +fields: + cpu_count: { value: 4, provenance: {...} } + memory_gb: { value: 8, provenance: {...} } + storage_gb: { value: 100, provenance: {...} } + os_family: { value: rhel, provenance: {...} } + environment: { value: production, provenance: {...} } + name: { value: "payments-api-server-01", provenance: {...} } + data_center: { value: "EU-WEST-DC1", provenance: {...} } + monitoring_agent: { value: "datadog-agent:7.42", provenance: {...} } + backup_policy: { value: "daily-30d-eu-west", provenance: {...} } + monitoring_endpoint: { value: "https://metrics...", provenance: {...} } + assigned_ip_address: { value: "10.1.45.23", provenance: { source_type: dependency_payload } } + network_subnet: { value: "10.1.0.0/16", provenance: { source_type: dependency_payload } } + network_gateway: { value: "10.1.0.1", provenance: { source_type: dependency_payload } } + +placement: + selected_provider_uuid: eu-west-prod-1-provider-uuid + sovereignty_satisfied: true + +dependencies: + - type: Network.IPAddress + entity_uuid: aabb1122-ip-uuid + status: SATISFIED +``` + +**Audit leaf written:** `PLACEMENT_COMPLETE` — hash of requested state + placement decision + dependency references. + +> **Key concept:** Requested State is write-once. It now includes both the assembled fields AND the dependency data from the IP allocation. This is the permanent auditable record of exactly what was approved for provisioning. + +--- + +## Stage 8: VM Provider Dispatch (Naturalization) + +The Request Orchestrator sends the enriched payload to the selected VM provider. The provider **naturalizes** the DCM unified payload — including the dependency-injected IP address — into its native API format: + +``` +DCM unified payload → OpenStack Nova API call: + +POST /servers +{ + "server": { + "name": "payments-api-server-01", + "flavorRef": "m1.xlarge", ← resolved from cpu_count: 4 + memory_gb: 8 + "imageRef": "rhel-9.3-latest", ← resolved from os_family: rhel + "networks": [{ + "uuid": "eu-west-net-uuid", + "fixed_ip": "10.1.45.23" ← from dependency: Network.IPAddress + }], + "availability_zone": "eu-west-az1", + "metadata": { + "dcm_entity_uuid": "f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0", + "dcm_tenant": "AppTeam", + "dcm_ip_entity_uuid": "aabb1122-ip-uuid", + "backup_policy": "daily-30d-eu-west" + } + } +} +``` + +**Key concept:** The VM provider receives the IP address as a known field in the payload — it doesn't call the IPAM system itself. DCM resolved the dependency, allocated the IP through the proper provider with full policy evaluation, and injected the result. The VM provider just uses it. + +**Audit leaf written:** `DISPATCH_SENT` — hash of naturalized payload + provider identity. + +--- + +## Stage 9: VM Provider Callback (Denaturalization) + +The provider provisions the VM with the pre-allocated IP, then **denaturalizes** the result back into DCM's unified format: + +``` +POST /api/v1/provider/entities/f5e6d7c8-.../status +Authorization: Bearer + +{ + "operation_uuid": "...", + "status": "OPERATIONAL", + "provider_entity_id": "vm-0a1b2c3d", + "realized_fields": { + "cpu_count": 4, + "memory_gb": 8, + "storage_gb": 102, ← actual (rounded up) + "assigned_ip_address": "10.1.45.23", ← confirmed: matches dependency + "hypervisor_host": "compute-node-07", ← provider-assigned + "console_url": "https://console.eu-west.example.com/vm/0a1b2c3d" + } +} +``` + +**Realized State recorded:** + +```yaml +entity_uuid: f5e6d7c8-e9f0-a1b2-c3d4-e5f6a7b8c9d0 +realized_at: 2026-03-15T09:03:12Z +lifecycle_state: OPERATIONAL + +fields: + cpu_count: 4 + memory_gb: 8 + storage_gb: 102 + assigned_ip_address: "10.1.45.23" + hypervisor_host: "compute-node-07" + console_url: "https://console.eu-west..." + provider_entity_id: "vm-0a1b2c3d" + +dependencies: + - type: Network.IPAddress + entity_uuid: aabb1122-ip-uuid + realized_value: "10.1.45.23" +``` + +**Consumer receives status update via webhook/polling.** + +**Audit leaf written:** `REALIZED` — hash of realized state + provider signature. + +--- + +## Stage 10: Discovery Cycle (Drift Detection) + +24 hours later, the Discovery service polls the provider: + +```yaml +discovered_at: 2026-03-16T09:00:00Z +provider_entity_id: "vm-0a1b2c3d" +status: ACTIVE + +cpu_count: 4 # matches Realized — no drift +memory_gb: 8 # matches +storage_gb: 102 # matches +assigned_ip_address: "10.1.45.23" # matches +``` + +**Drift comparison: Realized ≡ Discovered. No drift detected.** + +Discovery also polls the IP entity independently: + +```yaml +discovered_at: 2026-03-16T09:00:05Z +provider_entity_id: "record:host/ZG5z:10.1.45.23" +status: ACTIVE + +address: "10.1.45.23" # matches IP Realized State +lease_type: static # matches +``` + +If someone had manually changed the VM's IP outside DCM, the discovered IP on the VM would differ from the dependency record — that's drift on both the VM entity and the IP entity. + +> **Key concept:** Both the VM and the IP are independently discoverable DCM entities. Drift detection runs on each. The dependency relationship means drift on the IP triggers review of the VM too. + +--- + +## Stage 11: The Audit Trail + +At the end of this lifecycle, the Merkle tree contains these leaves (at mutation granularity). Note how both the VM and the IP dependency have their own complete audit chains: + +``` +VM Request: + Leaf 1: INTENT_CAPTURED hash(intent_payload) signed by api-gateway + Leaf 2: ASSEMBLY_COMPLETE hash(assembled_payload) signed by request-processor + Leaf 3: POLICY_EVAL:vm-size hash(eval_context + result) signed by policy-engine + Leaf 4: POLICY_EVAL:os-images hash(eval_context + result) signed by policy-engine + Leaf 5: POLICY_EVAL:residency hash(eval_context + result) signed by policy-engine + Leaf 6: POLICY_EVAL:monitoring hash(eval_context + mutation) signed by policy-engine + +IP Dependency Sub-Request: + Leaf 7: DEPENDENCY_CREATED hash(ip_sub_request + parent_vm_uuid) signed by orchestrator + Leaf 8: IP_POLICY:sovereignty hash(eval_context + result) signed by policy-engine + Leaf 9: IP_POLICY:subnet-iso hash(eval_context + result) signed by policy-engine + Leaf 10: IP_POLICY:pool-select hash(eval_context + mutation) signed by policy-engine + Leaf 11: IP_PLACEMENT_COMPLETE hash(ip_requested_state + ipam_selection) signed by placement + Leaf 12: IP_DISPATCH_SENT hash(naturalized_infoblox_request) signed by orchestrator + Leaf 13: IP_REALIZED hash(ip_realized_state) signed by ipam-provider + +VM Continues After Dependency Satisfied: + Leaf 14: DEPENDENCY_SATISFIED hash(ip_realized_fields + vm_entity_uuid) signed by orchestrator + Leaf 15: PLACEMENT_COMPLETE hash(vm_requested_state + placement + deps) signed by placement + Leaf 16: DISPATCH_SENT hash(naturalized_nova_payload) signed by orchestrator + Leaf 17: REALIZED hash(vm_realized_state) signed by vm-provider +``` + +Any auditor can: +- **Inclusion proof:** Verify leaf 8 (IP sovereignty check) exists in the tree +- **Consistency proof:** Verify the tree has only grown since the last signed tree head +- **Dependency chain:** Follow leaf 7→13 for the complete IP allocation audit, then leaf 14→17 for the VM +- **Policy provenance:** Trace why the IP came from InfoBlox pool 10.1.0.0/16 (leaves 9-11: subnet isolation + pool selection policies) +- **Non-repudiation:** Every leaf is signed by the service that produced it (Ed25519) + +--- + +## Summary: What Happened in 3 Minutes + +| Time | Stage | What happened | +|------|-------|---------------| +| T+0s | 1. Intent | Consumer declared 6 fields via API | +| T+0.5s | 2. Assembly | 5 layers merged in, provenance tracked for all 10+ fields | +| T+1s | 3. Policy (VM) | 4 policies evaluated — all passed. 1 field injected by transformation | +| T+1.1s | 4. Dependencies | VM requires Network.IPAddress — sub-request created automatically | +| T+1.3s | 5. Policy (IP) | 4 IP policies evaluated — sovereignty, subnet isolation, pool selection, format | +| T+1.5s | 6. IP Realized | IPAM provider allocated 10.1.45.23 from EU-WEST production pool | +| T+1.6s | 7. Injection | Realized IP injected into VM payload via dependency payload passing | +| T+2s | 7. Placement (VM) | 3 VM providers scored, 1 selected based on sovereignty + confidence | +| T+3s | 8. Dispatch | Payload naturalized to OpenStack Nova API with pre-allocated IP | +| T+192s | 9. Callback | Provider returned realized state — VM running at 10.1.45.23 | +| T+86400s | 10. Discovery | Both VM and IP confirmed matching realized state — no drift | + +**Consumer declared 6 fields. DCM handled everything else:** layer assembly, policy validation, data enrichment, IP allocation through the proper IPAM provider with full sovereignty and subnet policies, provider selection, API translation, state tracking, audit trail (17 Merkle leaves across 2 entities), and drift detection on both the VM and its IP dependency. + +--- + +## Where to Go Next + +- **To understand layers:** Doc 03 (Layering and Versioning) +- **To understand policies:** Doc B (Policy Contract) — start with §1-7 +- **To understand providers:** Doc A (Provider Contract) — start with §1-5 +- **To understand dependencies:** Doc 07 (Service Dependencies) — type-level deps, payload passing, resolution order +- **To understand audit:** Doc 16 (Universal Audit) — start with §1-3, then §8 for Merkle tree +- **To see the three-tier app example:** Doc 04 §8 (composite resource type specification with binding fields) +- **To see the IP allocation example:** Doc 04 §4 (IP Address Allocation with pool model) +- **To see all ADRs:** [Architecture Decision Records](adr/README.md) diff --git a/architecture/adopted-standards-dcm.md b/architecture/adopted-standards-dcm.md new file mode 100644 index 0000000..7e0f1b4 --- /dev/null +++ b/architecture/adopted-standards-dcm.md @@ -0,0 +1,94 @@ +# DCM — Adopted External Standards (the DCM-domain requirements) + +UDLM defines the **Adopt** disposition: when a credible external standard already models a domain's data +(FOCUS for cost/usage, OpenCost for k8s allocation, OSCAL for compliance, SCIM for identity), the data +substrate carries only *identity*, a *version-pinned conformance reference*, and the *binding* — never +the standard's schema. See UDLM `design-principles/core-tenets.md` **T5** and +`design-principles/adopted-standards.md`. + +> **License verdicts & the full adoption ledger live in UDLM** — `registry/standards-adoption-register.md` (what/why/where/when/who + license-compatibility verdict per standard, CI-enforced by ADOPT-001). This document does not restate verdicts; it states the DCM RUNTIME requirements. + +That is the **Data** half. This document states the **DCM** half: the requirements DCM must implement +and enable so adopted standards actually work at runtime. It is an application of the +**Data ⇄ Policy boundary** (`data-policy-boundary.md`): **the data declares which standard versions are +in play; DCM (Policy/Provider runtime) negotiates, enforces, and translates between them.** + +DCM **MUST** adhere to T5: it does not absorb an external standard's schema into its own persistence, +and does not become the system of record for adopted data — that data is referenced via an Information +Provider, lookup-only (`contracts/information-providers.md`). + +> **Scope — Tier 2 only.** These requirements apply to **record/schema** standards (FOCUS, OpenCost, +> OSCAL, SCIM), which version in ways that change their shape. **Value/codelist** standards (ISO 4217, +> ISO 8601, RFC 4122) are adopted as a plain referenced field constraint — *no* support matrix, *no* +> version negotiation, *no* ADS requirements. Route by kind first (UDLM `adopted-standards.md` §1a). + +## Requirements (ADS — Adopted Standards) + +### Registration & discovery +- **ADS-001 — Provider support matrix.** DCM **MUST** accept and validate a provider's + `adopted_standard_support[]` declaration at registration: for each adopted standard, the supported + version range, a `preferred` version, and `direction` (`emit` | `consume` | `both`). It is validated + and trust-stamped like any other provider capability. +- **ADS-002 — Compatibility discovery.** DCM **MUST** expose, for discovery, which providers can serve + which standard versions, so an implementor can select a compatible provider before binding. Silent + incompatibility is not permitted. + +### Negotiation, translation, enforcement (Policy) +- **ADS-003 — Version negotiation.** At binding time DCM **MUST** resolve the intersection of the + consumer/type **required** version range and the provider's **supported** range, selecting the + effective version (highest common, or `preferred` when inside the overlap). +- **ADS-004 — Translation as Policy.** When required and supported do not directly overlap, DCM **MAY** + translate between versions via a **registered, deterministic** mapping — preferably the standard's own + published migration. Translation is a **Policy act** (transformation is Policy, UDLM T2), evaluated and + **audited**; it is never an evaluator embedded in the portable data. +- **ADS-005 — Enforcement / reject.** If there is no compatible version **and** no registered + translation path, DCM **MUST** reject the binding as non-conformant and **surface** it — never + silently drop or downgrade. +- **ADS-006 — Implementor-bounded parameters.** Parameters defined by the standard but constrained by + the implementor (e.g. cost rate ranges, markup minimums, budget ceilings) **MUST** be enforced via + **policy-as-code** (OPA/Rego), not hard-coded — consistent with DCM's policy-governance model. + +### Identity, provenance, audit (recording the decision) +- **ADS-007 — Identity join, no ownership.** DCM **MUST** resolve an adopted-standard binding using the + UDLM identity ↔ standard-column join (e.g. resource `uuid` ↔ FOCUS `ResourceId`) and **MUST NOT** + cache or persist the external records as a system of record — the data is served by the Information + Provider with the freshness/authority the IP contract provides + (`contracts/information-providers-advanced.md`). +- **ADS-008 — Effective-version provenance.** DCM **MUST** record the negotiated **effective version** + (and, when it translated, the source version + translation reference) as **provenance** on the + realized entity, and **MUST** lower confidence/authority for translated/derived values. +- **ADS-009 — Auditability.** The negotiation outcome (accept / translate / reject) and any translation + **MUST** be written to the tamper-evident audit log (`AUD-001/002`) as a decision — reproducible from + the immutable record. + +### Lifecycle +- **ADS-010 — Standard version lifecycle.** DCM **MUST** track adopted-standard version deprecation and + allow a configured **minimum** and **preferred** version per environment, so an operator can require, + e.g., "FOCUS ≥ 1.3" platform-wide and let negotiation/translation satisfy older providers. + +## How this maps to the boundary + +| Step | Data (UDLM) — the noun | DCM (Policy/Provider) — the verb | +|---|---|---| +| Provider declares support | `adopted_standard_support[]` record | validate + register (ADS-001) | +| Consumer/type needs a version | `adopts[].version` range | — | +| Pick the version | — | **negotiate** required ∩ supported (ADS-003) | +| Versions don't match | registered migration reference | **translate** (ADS-004) or **reject** (ADS-005) | +| Bind to the resource | identity ↔ standard column | **resolve** join, IP lookup (ADS-007) | +| Record what happened | effective-version provenance slot | **write** provenance + audit (ADS-008/009) | +| Constrain parameters | the declared parameter values | **enforce** via Rego (ADS-006) | + +> Test (same as the boundary doc): a **noun** (a support record, a version pin, a join key, a provenance +> slot) is UDLM's; a **verb** (negotiate, translate, enforce, reject, resolve, record) is DCM's. + +## Worked example — cost (FOCUS + OpenCost) + +The Cost Management Service Provider (`dcm-project/enhancements#57`) is the reference case: it declares +`adopted_standard_support` for FOCUS (`≥1.2 <2.0`, preferred 1.4, emit) and OpenCost (`1.x`); DCM +negotiates against a chargeback view that requires FOCUS ≥ 1.3 (allocation columns); rate ranges and +budgets are enforced by Rego (ADS-006); the effective version is recorded as provenance (ADS-008). The +cost data itself is **never** modeled in DCM/UDLM — it conforms to FOCUS and is served via the SP's +Information-Provider query API. This is what "adopt, don't absorb" looks like end-to-end. + +See also: `data-policy-boundary.md`, UDLM `design-principles/adopted-standards.md` (the Data-side +contract), `dcm-platform-requirements.md` (the broader requirement set). diff --git a/architecture/control-plane/api-versioning.md b/architecture/control-plane/api-versioning.md new file mode 100644 index 0000000..6f323a3 --- /dev/null +++ b/architecture/control-plane/api-versioning.md @@ -0,0 +1,402 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — API Versioning and Lifecycle +Maps to: udlm/governance/registry-governance.md +--- + +# DCM Data Model — API Versioning Strategy + +> **Implements contracts defined in UDLM**: +> [udlm/governance/registry-governance.md](https://github.com/croadfeldt/udlm/blob/main/governance/registry-governance.md) +> and [udlm/contracts/event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md). +> UDLM defines the registry-governance versioning and deprecation lifecycle and +> the event-catalog event-versioning contract. DCM operationalizes a concrete +> versioning strategy across every public API surface — Consumer, Admin, +> Operator Interface (Provider), and Flow GUI — including URL major versioning, +> deprecation lead time, and event schema versioning. + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference — API Versioning and Lifecycle +**Related Documents:** [Consumer API Specification](../../docs/specifications/consumer-api-spec.md) | [Admin API Specification](../../docs/specifications/dcm-admin-api-spec.md) | [Operator Interface Specification](../../docs/specifications/dcm-operator-interface-spec.md) | [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md) | [Registry Governance](https://github.com/croadfeldt/udlm/blob/main/governance/registry-governance.md) | [Design Priorities](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md) + +> **This document governs all DCM API surfaces.** Every public API endpoint — Consumer, Admin, Operator Interface (Provider), Flow GUI — follows this versioning strategy. The strategy is designed to make the secure, compatible path the easy path: clients that do nothing get the version they requested; breaking changes are announced with sufficient lead time; the newest version is always the supported version. + +--- + +## 1. Versioning Model + +### 1.1 URL-Based Major Version + +DCM APIs use **URL path versioning** for major versions. The version is the first path segment after the API surface prefix: + +``` +Consumer API: https://{dcm-instance}/api/v1/ +Admin API: https://{dcm-instance}/api/v1/admin/ +Provider API (OIS):https://{dcm-instance}/provider/api/v1/ +Flow GUI API: https://{dcm-instance}/flow/api/v1/ +``` + +The version segment (`v1`, `v2`, etc.) represents a major version. It increments only on breaking changes. Multiple major versions may coexist during a transition window (see Section 4). + +The four surfaces are separate because they are distinct **trust zones** — Consumer, Admin, Provider (OIS), and Flow each carry their own authentication and RBAC, and each versions independently (the Consumer API can be at `v2` while the Provider/OIS surface is still `v1`). + +### 1.2 Version Granularity — Per-API, Not Per-Endpoint + +Versioning is **per-API surface**, not per-endpoint. When a breaking change occurs to any endpoint within an API surface, the entire surface increments to the next major version. This means: + +- `v2` of the Consumer API is a complete API surface, not a patchwork of versioned endpoints +- All endpoints within a surface version are internally consistent +- Clients target a single version for all their interactions with that surface + +Individual endpoints are not independently versioned. If a single endpoint needs a breaking change, the API surface version increments and all other endpoints continue unchanged under the new version. + +### 1.3 Minor and Revision Changes + +DCM follows [Semantic Versioning](https://semver.org/) — that baseline (major = breaking, minor = additive, patch/revision = fixes) is assumed and not restated here. This section records only the **DCM-specific** application of it. + +Non-breaking changes within a major version are documented in the API changelog but do not change the URL. Clients do not need to take any action for non-breaking changes. + +- **Minor change**: new optional fields, new endpoints, expanded enum values with version-compatible defaults +- **Revision**: documentation corrections, clarifications, non-functional specification updates + +--- + +## 2. Breaking Change Definition + +A change is **breaking** if it requires any existing client to modify its code or configuration to continue working correctly. The general taxonomy below is standard REST practice — it is enumerated here not as novel guidance but as DCM's **explicit, authoritative checklist** so that "is this breaking?" has one answer across every surface. The DCM-specific entries to note are idempotency semantics, the response-envelope structure, authentication-method removal, and enum-value handling (clients MUST tolerate unknown enum values): + +**Request changes:** +- Removing a field that was previously accepted +- Changing a field from optional to required +- Changing a field's type (e.g. string → integer) +- Removing an accepted enum value +- Changing URL path structure (endpoint rename or restructure) +- Changing HTTP method for an existing operation +- Removing an endpoint + +**Response changes:** +- Removing a field from any response +- Changing a field's type in any response +- Changing a field's name in any response +- Removing a previously returned enum value +- Changing HTTP status code semantics (e.g. 200 → 202, or changing when 4xx vs 5xx is returned) +- Changing the response envelope structure + +**Behavior changes:** +- Changing default values in ways that alter existing behavior +- Changing idempotency semantics +- Removing a previously supported authentication method +- Tightening validation (rejecting previously accepted inputs) +- Changing pagination behavior in ways that break existing cursor patterns + +**The following are NOT breaking changes:** +- Adding new optional request fields (with sensible defaults) +- Adding new response fields (existing clients safely ignore unknown fields) +- Adding new endpoints +- Expanding an enum with new values (clients must handle unknown enum values gracefully) +- Relaxing validation (accepting previously rejected inputs) +- Adding new error codes (clients that handle errors generically are unaffected) +- Performance improvements, infrastructure changes, security patches +- Documentation improvements + +--- + +## 3. Version Discovery + +Clients can discover available API versions and their status without prior knowledge: + +### 3.1 Well-Known Discovery Endpoint + +``` +GET https://{dcm-instance}/.well-known/dcm-api-versions + +Response 200: +{ + "dcm_version": "1.2.0", + "api_surfaces": { + "consumer": { + "current": "v2", + "supported": ["v1", "v2"], + "versions": { + "v1": { + "status": "deprecated", + "sunset_date": "2027-06-01", + "deprecation_date": "2026-06-01", + "base_url": "/api/v1/", + "changelog_url": "/api/v1/changelog" + }, + "v2": { + "status": "stable", + "released_date": "2026-06-01", + "base_url": "/api/v2/", + "changelog_url": "/api/v2/changelog" + } + } + }, + "admin": { + "current": "v1", + "supported": ["v1"], + "versions": { + "v1": { "status": "stable", "base_url": "/api/v1/admin/" } + } + }, + "provider": { + "current": "v1", + "supported": ["v1"], + "versions": { + "v1": { "status": "stable", "base_url": "/provider/api/v1/" } + } + } + } +} +``` + +### 3.2 Per-Version Changelog + +``` +GET /api/v1/changelog + +Response 200: +{ + "version": "v1", + "changes": [ + { + "date": "2026-01-15", + "type": "minor", + "description": "Added optional `score_drivers` field to request status response", + "affected_endpoints": ["GET /api/v1/requests/{uuid}/status"] + } + ] +} +``` + +--- + +## 4. Deprecation and Sunset Lifecycle + +### 4.1 Deprecation Timeline + +When a new major version is released, the previous version enters a **deprecation period**. The deprecation timeline is profile-governed — production deployments require longer support windows than development environments: + +```yaml +api_version_support_lifecycle: + minimal: + deprecation_notice_period: P90D # 90 days notice before sunset + deprecated_version_support: P180D # old version supported 180 days after deprecation + + dev: + deprecation_notice_period: P0D # none — dev has no deprecation guarantee + deprecated_version_support: P0D # old versions may be removed immediately + # Dev is for iteration, not stable consumers; versions can break without a + # support window. Use minimal+ if you need any deprecation lead time. + + standard: + deprecation_notice_period: P180D + deprecated_version_support: P365D # 1 year + + prod: + deprecation_notice_period: P365D # 1 year notice + deprecated_version_support: P730D # 2 years support after deprecation + + fsi: + deprecation_notice_period: P548D # 18 months notice + deprecated_version_support: P1095D # 3 years support after deprecation + + sovereign: + deprecation_notice_period: P730D # 2 years notice + deprecated_version_support: P1460D # 4 years support after deprecation +``` + +**Deprecation ≠ Sunset.** A deprecated version continues to function. Sunset is when it stops working. The deprecation period is the window between "we recommend you migrate" and "you must migrate." + +### 4.2 Deprecation Headers + +When a client calls a deprecated API version, the response includes standard deprecation headers (per [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) and [RFC 9745](https://datatracker.ietf.org/doc/html/rfc9745)): + +```http +HTTP/1.1 200 OK +Deprecation: @1749340800 # Unix timestamp when this version was deprecated +Sunset: @1781049600 # Unix timestamp when this version will stop working +Link: ; rel="successor-version" +Link: ; rel="deprecation" +``` + +### 4.3 Deprecation Events + +When a version is deprecated or sunsetted, DCM fires notification events: + +- `governance.api_version_deprecated` — version entered deprecation; Sunset header begins appearing +- `governance.api_version_sunset_warning` — 30 days before sunset; high urgency +- `governance.api_version_sunset` — version has reached sunset date; calls now return 410 Gone + +Platform admins should configure notification routing for these events to ensure API consumers receive timely warning. + +### 4.4 Sunset Behavior + +After the sunset date, calls to the deprecated version return: + +```http +HTTP/1.1 410 Gone +Content-Type: application/json + +{ + "error": "api_version_sunset", + "message": "API version v1 reached its sunset date on 2027-06-01. Migrate to v2.", + "successor_version": "v2", + "migration_guide_url": "/api/v2/migration-guide", + "sunset_date": "2027-06-01" +} +``` + +--- + +## 5. Version Negotiation + +### 5.1 How Clients Specify a Version + +The URL path is the primary versioning mechanism. No headers or query parameters are required — the URL is authoritative: + +``` +GET /api/v1/resources → Consumer API v1 +GET /api/v2/resources → Consumer API v2 (when available) +``` + +### 5.2 Version Preference Header (Optional) + +For clients that need to pin to a specific version or test against a new version before migrating, an optional `DCM-API-Version` header is supported: + +```http +GET /api/v1/resources +DCM-API-Version: v1 # explicit pin; returns 406 if v1 is sunsetted +``` + +If the header specifies a sunsetted version, the response is `406 Not Acceptable` with a migration guide reference. + +### 5.3 Latest-Version Alias + +``` +GET /api/latest/resources # always routes to current stable version +``` + +The `latest` alias is provided for development and testing. It is **not recommended for production** — production clients should pin to a specific version to avoid inadvertent breaking changes when a new major version becomes `latest`. + +--- + +## 6. Beta and Preview Endpoints + +New capabilities that are not yet stable may be released as **preview endpoints** within the current major version: + +``` +GET /api/v1/preview/new-feature +``` + +Preview endpoints: +- Are not covered by the stability guarantees of the parent version +- May change or be removed without a major version increment +- Are marked in the API changelog and discovery endpoint as `status: preview` +- Must not be used in production automation without explicit acknowledgment of instability + +```yaml +# Discovery response for a preview endpoint +"new-feature": { + "status": "preview", + "stability_commitment": "none", + "planned_graduation": "v2", + "feedback_url": "https://github.com/dcm-project/discussions" +} +``` + +Preview endpoints graduate to stable when they are included in a new major version release. + +--- + +## 7. Provider API (OIS) Versioning + +The Operator Interface Specification (OIS) governs how DCM calls providers. Provider implementations must support the version of the OIS they declare in their capability registration. + +### 7.1 OIS Version in Capability Registration + +```yaml +provider_registration: + ois_version: "1.0" # which OIS version this provider implements + ois_version_min: "1.0" # minimum OIS version supported + ois_version_max: "1.x" # maximum OIS version supported (x = any minor) +``` + +### 7.2 OIS Compatibility + +DCM maintains version-compatible with registered OIS versions during the support lifecycle. A DCM instance running OIS v2 must continue to dispatch to providers registered on OIS v1 until the version is sunset. + +When the OIS version is incremented: +1. DCM announces the new OIS version via the event `governance.ois_version_released` +2. Providers have the deprecation notice period to upgrade their implementation +3. DCM dispatches using the appropriate OIS version per the provider's declared capability +4. After sunset, providers still on deprecated OIS versions receive `410 Gone` on dispatch + +**Why an event, not a REST response (step 1).** A version/capability change is a **one-to-many** announcement — every registered provider and every interested subscriber needs to learn about it, and they are not in the middle of a request when it happens. That is a fan-out, so it is published on the event bus (CloudEvents), not returned synchronously. By contrast, an actual **dispatch** (step 3) is **point-to-point** and needs an immediate result, so it stays a synchronous REST call. The rule across DCM: state/capability *changes* broadcast as events; *operations* that need a result are REST. + +### 7.3 Provider-Initiated API Versioning + +Providers that expose their own management APIs (beyond the standard OIS surface) are responsible for their own versioning. DCM does not version-manage provider-internal APIs. Providers should follow the same breaking-change definition (Section 2) and announce breaking changes via `provider_update.submitted` events. + +--- + +## 8. Version Upgrade Path + +When a new major API version is published, a machine-readable change log is available at: + +``` +GET /api/v{N}/migration-guide +``` + +This endpoint returns all breaking changes from the previous major version: + +```json +{ + "from_version": "v1", + "to_version": "v2", + "breaking_changes": [ + { + "change_id": "BC-001", + "type": "field_removed", + "endpoint": "GET /api/v2/resources/{uuid}", + "description": "Field 'legacy_id' removed — use 'entity_uuid' instead" + } + ], + "new_capabilities": [] +} +``` + +Clients declare the API version they target via the `Accept-Version` header or URL prefix. DCM supports all non-sunset major versions simultaneously. When a version reaches sunset, responses include `Deprecation` and `Sunset` headers (RFC 8594) before support is withdrawn. + +--- + + +## 9. Internal API Versioning + +DCM internal component APIs (Control Plane components communicating with each other) follow a simpler model: + +- Internal APIs are not exposed externally and not subject to the external versioning lifecycle +- Internal breaking changes require a coordinated deployment of all affected components +- DCM release versions (e.g. `1.2.0`) cover the complete set of internal APIs for that release +- Operators upgrading DCM must upgrade all components together per the release upgrade guide + +--- + +## 10. System Policies + +| Policy | Rule | +|--------|------| +| `VER-001` | All DCM public API surfaces use URL path versioning. The version path segment is the only authoritative version indicator. | +| `VER-002` | A change is breaking if any existing client must modify code or configuration to continue working. When in doubt, treat a change as breaking. | +| `VER-003` | Deprecated API versions must return `Deprecation`, `Sunset`, and `Link` headers on every response during the deprecation period (per RFC 8594 / RFC 9745). | +| `VER-004` | Deprecated versions must remain fully functional until the sunset date. Bugs in deprecated versions are fixed; new features are not backported. | +| `VER-005` | The deprecation notice period and deprecated version support window are profile-governed. Production deployments require longer windows than development. See Section 4.1. | +| `VER-006` | The `latest` version alias is available but must not be recommended for production use. Production clients must pin to a specific version. | +| `VER-007` | Preview endpoints are not stable. They may change or be removed without a major version increment. They are identified by the `/preview/` path segment. | +| `VER-008` | Every new major version must publish a machine-readable migration guide at `/api/v{N}/migration-guide`. | +| `VER-009` | DCM must maintain dispatch compatibility with providers registered on supported OIS versions until the OIS version is sunset. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/control-plane/components.md b/architecture/control-plane/components.md new file mode 100644 index 0000000..3002044 --- /dev/null +++ b/architecture/control-plane/components.md @@ -0,0 +1,852 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference +--- + +# DCM Data Model — Control Plane Components + +> **DCM-native control-plane runtime; no single UDLM contract counterpart.** +> The control-plane components are runtime implementations of the three UDLM +> abstractions (Data, Provider, Policy) — not a fourth abstraction and not a +> distinct UDLM contract. A peer DCM realization could decompose its control +> plane differently and still satisfy every UDLM contract. + + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [udlm/foundations/foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: RUNTIME** +> +> Runtime implementations of the three abstractions — not a fourth abstraction + + +**Related Documents:** [Internal Component Authentication](internal-component-auth.md) | [Context and Purpose](https://github.com/croadfeldt/udlm/blob/main/foundations/context-and-purpose.md) | [Four States](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md) | [Resource/Service Entities](https://github.com/croadfeldt/udlm/blob/main/entities/resource-service-entities.md) | [Operational Models](https://github.com/croadfeldt/udlm/blob/main/lifecycle/operational-models.md) | [Policy Profiles](../governance-enforcement/policy-profiles.md) + +--- + + +The DCM Control Plane consists of **nine components** that implement the three foundational abstractions at runtime. + +## 1. Purpose + +> **Internal component authentication:** See [Internal Component Authentication](internal-component-auth.md) for the mTLS and interaction credential model governing all component-to-component calls within the DCM control plane. + + +This document formally defines the DCM control plane components that are referenced throughout the data model documents. Two components are defined here: + +1. **The Request Orchestrator** — the event bus and coordinator of the request lifecycle pipeline +2. **The Cost Analysis Component** — the internal DCM component that provides cost signals for placement, catalog, and attribution + +--- + +## 2. The Request Orchestrator + +### 2.1 Role + +The Request Orchestrator is the **event bus and pipeline coordinator** for all DCM request lifecycle operations. It does not perform any pipeline work itself — it listens for events, evaluates which components need to act on them, and routes work to the appropriate components. + +The Request Orchestrator embodies DCM's **data-driven, policy-triggered orchestration model**: the pipeline is not a fixed procedural sequence. It is a cascade of event-condition-action responses, where policies define what happens when specific payload states are observed. + +### 2.2 Data-Driven Orchestration Principle + +**Policies ARE the orchestration.** The Request Orchestrator does not contain hardcoded pipeline logic. It publishes events to the Policy Engine; policies match on payload type and state; policy actions produce new payload states; those new states trigger further policy evaluations. + +This means: +- Adding a new pipeline step = writing a new policy (no code change) +- Removing a step = deactivating a policy +- Changing when a step fires = changing a policy condition +- A static workflow (e.g., always require human approval for prod VMs) = a policy that always matches for those conditions +- A dynamic workflow (e.g., route to different approval processes based on cost) = a policy with conditional logic + +Static and dynamic flows compose naturally — a static policy defines a guaranteed step; a dynamic policy defines a conditional step. Both are expressed as policies, evaluated by the same engine, producing deterministic outcomes. + +**Determinism guarantee:** Dynamic execution remains deterministic because: +- The payload type vocabulary is a closed set +- Policy evaluation order within a domain level is deterministic (domain precedence) +- The payload mutation model is immutable (each policy produces a new payload version) +- The same input state always produces the same output state + +### 2.3 The Payload Type Vocabulary + +Every event in DCM carries a payload with a declared type. Policies pattern-match on these types. The payload type vocabulary is the foundational contract of the orchestration model. + +```yaml +payload_types: + # Request lifecycle + request.initiated: # consumer submitted a request + request.intent_captured: # Intent State written + request.layers_assembled: # layer assembly complete + request.policies_evaluated: # all active policies evaluated + request.placement_complete: # provider selected + request.dispatched: # sent to provider + request.realized: # provider confirmed realization + request.failed: # terminal failure + request.cancelled: # cancelled + + # Provider update + provider_update.received: # provider submitted update notification + provider_update.evaluated: # policy evaluation complete + provider_update.accepted: # accepted; Realized State updating + provider_update.rejected: # rejected; becomes drift + + # Drift and discovery + discovery.cycle_complete: + drift.detected: + drift.resolved: + + # Recovery + recovery.timeout_fired: + recovery.late_response: + recovery.compensation_triggered: + + # Governance + policy.activated: + layer.updated: + profile.changed: +``` + +### 2.4 Event Routing Model + +``` +Event published: { type: "request.initiated", payload: {...}, entity_uuid: X } + │ + ▼ Request Orchestrator receives event + │ Routes to Policy Engine: "evaluate all policies matching request.initiated" + │ + ▼ Policy Engine evaluates in domain precedence order + │ Matching policies fire; payload mutations accumulated + │ New payload state produced: { type: "request.layers_assembled", ... } + │ + ▼ Request Orchestrator receives new event + │ Routes to Policy Engine for next evaluation cycle + │ (parallel if no data dependencies between active policies) + │ + ▼ Continues until terminal state (request.realized or request.failed) +``` + +**Parallel execution:** Policies that have no data dependencies on each other evaluate concurrently. The Request Orchestrator tracks dependency declarations between policies and executes in parallel where safe. + +**Why policy evaluation is event-driven but provider dispatch is synchronous REST.** Policy evaluation is **one-to-many**: a single lifecycle event (e.g. `request.layers_assembled`) may match any number of policies — gating, transformation, recovery — that the orchestrator does not know in advance and that fire independently. That fan-out is exactly what an event bus is for; modeling it as a chain of REST calls would hard-wire a caller↔callee coupling the policy model is designed to avoid. **Provider dispatch is the opposite shape**: it is **point-to-point** (one entity → one selected provider) and the orchestrator needs the realized result *before* it can advance the request, so it is a synchronous REST call ([Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md)). The rule across the control plane: *fan-out / "who cares about this happening?"* → events; *point-to-point / "I need this result to continue"* → REST. + +### 2.5 Static Flow Support + +Organizations that require guaranteed sequential flows express them as ordered policy sets: + +```yaml +static_flow_policy_group: + handle: "org/flows/prod-vm-approval-flow" + concern_type: orchestration_flow + ordered: true # policies execute in declared sequence, not parallel + policies: + - step: 1 + handle: "org/policies/cost-check" + condition: "request.initiated AND resource_type=Compute.VirtualMachine AND tenant.profile=prod" + on_fail: halt + - step: 2 + handle: "org/policies/manager-approval" + condition: "request.cost_estimated > 500" + on_fail: halt + - step: 3 + handle: "org/policies/security-review" + condition: "always" + on_fail: halt +``` + +A static flow is a Policy Group with `concern_type: orchestration_flow` and `ordered: true`. The Request Orchestrator respects the declared order. Static flows integrate with dynamic policies — a dynamic policy can fire alongside the static flow steps. + +#### Match-condition grammar + +The `condition:` field above is a **match-condition expression** — the same grammar every policy uses to decide whether it fires. It is a boolean expression over the current payload and context: + +``` +condition := term (("AND" | "OR") term)* | "always" +term := | +operand := | +comparator:= "=" | "!=" | ">" | ">=" | "<" | "<=" | "IN" +``` + +- **`always`** is a **reserved keyword** meaning the condition is unconditionally true — the policy fires on every evaluation of its event. It is the explicit form of "no guard"; use it instead of a tautology so the intent is visible. (Other reserved tokens: `AND`, `OR`, `IN`, `true`, `false`.) +- **Variable paths** (e.g. `resource_type`, `tenant.profile`, `request.cost_estimated`) resolve **by reference** against the current payload + request context at evaluation time. A bare `request.initiated`-style token is an **event-type match** (true when the triggering event is that type). +- **Literals** are values, not references: an unquoted number (`500`) or boolean is a literal; `resource_type=Compute.VirtualMachine` compares the resolved `resource_type` variable to the literal enum value. To compare two variables, both sides are paths (`a.x = b.y`); to compare a variable to a fixed value, the right side is a literal. Quote a literal that contains spaces. +- Evaluation is **side-effect-free** and short-circuits left to right; an unresolvable variable path evaluates the term to `false` (it never errors the request). + +### 2.5a Named Workflows vs Dynamic Policies — How They Compose + +The Request Orchestrator does not distinguish between named workflows and dynamic policies — both arrive as events and are routed to the Policy Engine. The distinction is in *how they are declared*: + +**Named Workflow Artifacts** (Orchestration Flow Policies with `ordered: true`) declare an explicit step sequence. An operator reading the workflow can see every step in order. Steps reference payload types from the closed vocabulary. Named workflows are the *explicit, visible skeleton* of a process. + +**Dynamic Policies** (Validation, Transformation, Recovery) fire when their match conditions are satisfied, regardless of workflow position. They are not declared in the workflow artifact. They are the *conditional behavior* that fills in the skeleton. + +**Example — request lifecycle:** +``` +Named workflow "system/workflows/request-lifecycle" declares: + Step 1: request.initiated → capture intent + Step 2: request.intent_captured → run layer assembly + Step 3: request.layers_assembled → run placement + Step 4: request.placement_complete → dispatch + +Dynamic policies also fire: + Validation Policy (compliance) "vm-size-limits" fires on request.layers_assembled + if cpu_count > 32 → deny + Transformation "inject-monitoring" fires on request.layers_assembled + → adds monitoring_endpoint field + Recovery "notify-on-timeout" fires on recovery.timeout_fired + → NOTIFY_AND_WAIT action +``` + +The named workflow and the dynamic policies are independent artifacts. Adding a new Validation Policy does not modify the workflow. Modifying the workflow does not affect dynamic policies. They compose through the same Policy Engine evaluation on the same events. + +### 2.6 Request Orchestrator Responsibilities + +| Responsibility | Description | +|----------------|-------------| +| Event routing | Receive all request lifecycle events; route to appropriate components | +| Pipeline coordination | Sequence component interactions per data dependencies | +| Timeout monitoring | Track dispatch_timeout and assembly_timeout; fire recovery triggers | +| Dependency resolution | For composite services, sequence component provisioning per dependency graph | +| Status tracking | Maintain current status of all in-flight requests; respond to status queries | +| Recovery coordination | On timeout/failure, invoke Recovery Policy evaluation | + +--- + +## 3. The Cost Analysis Component + +### 3.1 Role + +The Cost Analysis Component is an **internal DCM control plane component** that provides cost signals to other components. It is not a billing system and not a provider type. It does not manage financial transactions, produce invoices, or serve as the authoritative financial record. It provides cost *signals* that DCM uses for placement decisions, pre-request estimation, and ongoing attribution. + +The authoritative billing record lives in the organization's financial system. A billing system can register as an Information Provider to push authoritative cost data back into DCM for attribution records. + +### 3.2 Three Cost Functions + +**Function 1 — Pre-request cost estimation:** +Given a catalog item and assembled field values, compute the estimated lifecycle cost. Used by: +- Service Catalog describe endpoint (consumer sees cost before requesting) +- CI pipeline pre-validation (cost estimate in PR comment) +- Placement engine tie-breaker step 4 (cheapest eligible provider) + +**Function 2 — Placement cost input:** +During Step 6 placement, provide current cost data per eligible provider for the requested resource type. If Cost Analysis data is unavailable, the placement engine falls back to static declared costs per REG-011. + +**Function 3 — Ongoing cost attribution:** +For realized entities, track ongoing consumption and attribute costs to the owning Tenant. Consumed by OBS-005 (consumer cost view) and the resource describe endpoint (`estimated_cost_per_hour` field). + +### 3.3 Cost Data Sources + +The Cost Analysis Component ingests cost data from two sources, following the REG-011 hybrid model: + +```yaml +cost_data_sources: + static: + source: provider_registration # declared at provider registration time + update_frequency: manual # updated when rates change + fields: [capex_per_unit, opex_per_unit_per_hour, currency] + + dynamic: + source: external_cost_api # external billing API or cloud pricing API + registered_as: information_provider + query_interval: PT1H + fallback: static # use static if dynamic unavailable + fallback_max_age: PT24H +``` + +### 3.4 Cost Estimation Model + +```yaml +cost_estimation_request: + catalog_item_uuid: + assembled_fields: + cpu_count: 4 + memory_gb: 8 + storage_gb: 100 + tenant_uuid: + requested_duration: P30D # optional; lifecycle estimate + +cost_estimation_response: + estimated_cost: + per_hour: 0.32 + per_month: 230.40 + lifecycle_estimate: 691.20 # if requested_duration provided + currency: USD + confidence: high # high: current Cost Analysis data + # medium: data > PT1H old + # low: static fallback + breakdown: + - component: compute + per_hour: 0.28 + - component: ip_allocation + per_hour: 0.04 + cost_data_timestamp: +``` + +**Field reference.** `assembled_fields` — the post-layer-assembly resource spec the estimate is computed from (the cost-relevant subset; provider-agnostic). `requested_duration` — optional ISO-8601 duration; when present, `lifecycle_estimate` = projected total over that duration. `confidence` — provenance of the figure, not a probability: `high` = live Cost Analysis data, `medium` = data older than PT1H, `low` = static fallback table. `breakdown[]` — per-cost-component contribution (`component` is a free-form cost driver such as `compute`/`ip_allocation`; the sum reconciles to `per_hour`). `cost_data_timestamp` — when the underlying rate data was sourced (drives the `confidence` downgrade). On the attribution side, `billing_state` governs whether accrual is charged (`billable` | `non_billable` | `reduced_rate`) and `cost_data_source` records whether the rate came from live `cost_analysis`, a `static` table, or is `unknown`. + +### 3.5 Cost Attribution for Realized Entities + +```yaml +entity_cost_attribution: + entity_uuid: + tenant_uuid: + billing_state: billable # billable | non_billable | reduced_rate + current_rate: + per_hour: 0.32 + currency: USD + rate_effective_since: + monthly_accrual: 230.40 + cost_data_source: cost_analysis # cost_analysis | static | unknown +``` + +### 3.6 Integration with Placement Engine + +The placement engine queries Cost Analysis at step 4 of the tie-breaking hierarchy: + +``` +Step 4 — Cost Analysis (if available and determinable): + Query Cost Analysis for each eligible provider + Cost Analysis returns: estimated cost per unit per provider + Placement engine prefers lowest cost among equally-ranked candidates + If Cost Analysis unavailable: skip step 4; proceed to step 5 + # Cost Analysis unavailability never blocks placement +``` + +--- + +## 4. Related Policies + +| Policy | Rule | +|--------|------| +| `CTL-001` | The Request Orchestrator is the single event bus for all request lifecycle events. No component communicates directly with another component outside of events published to the Request Orchestrator. | +| `CTL-002` | Policies ARE the orchestration. The Request Orchestrator does not contain hardcoded pipeline logic. Pipeline behavior is modified by adding, removing, or changing policies — not by changing the orchestrator. | +| `CTL-003` | Dynamic and static flows compose naturally. Static flows are Policy Groups with concern_type: orchestration_flow and ordered: true. Both types are evaluated by the same Policy Engine. | +| `CTL-004` | Cost Analysis is not a billing system. It provides cost signals for placement and attribution. The authoritative billing record lives in the organization's financial system, which may register as an Information Provider. | +| `CTL-005` | Cost Analysis unavailability never blocks placement. The placement engine falls back to static declared costs per REG-011 and skips the Cost Analysis tie-breaking step. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* + +--- + +## 4. The Placement Engine + +### 4.1 Role + +The Placement Engine selects the specific Service Provider that will fulfill a resource request. It runs as step 6 of the Request Payload Processor assembly pipeline and is invoked by the Request Orchestrator after all policies have been evaluated and the assembled payload is ready for dispatch. + +The Placement Engine does not make business decisions — those are made by policies (which inject constraints and preferences). The Placement Engine applies those constraints to find eligible providers, then deterministically resolves ties using a declared hierarchy. + +### 4.2 Input and Output + +**Input:** +- Assembled payload with full field-level provenance +- Sovereignty constraints (from compliance domain profile and any policy-injected constraints) +- Accreditation requirements (from Data/Capability Authorization Matrix) +- Preference scores or preferred provider UUIDs (if injected by Transformation policy) +- Tenant affinity declarations + +**Output:** +- Selected provider UUID +- Placement reason (why this provider was selected) +- Sovereignty satisfaction record (which constraints were checked and passed) +- Reserve confirmation (provider confirmed it has capacity for this specific request) + +This output is written to `placement.yaml` in the Requested Store directory. + +### 4.3 Placement Algorithm — Six Steps + +``` +Step 1: Sovereignty Pre-Filter + Eliminate any provider whose sovereignty_declaration does not satisfy + the request's sovereignty constraints. + → Providers that fail this step are never contacted. + → If zero providers remain: RESERVE_QUERY_ALL_EXHAUSTED recovery trigger fires. + +Step 2: Accreditation Filter + Eliminate any provider that does not hold the required accreditations + for the data classifications present in the assembled payload. + → Checked against the active Data/Capability Authorization Matrix. + → Providers with accreditation gaps are excluded. + +Step 3: Capability Filter + Eliminate any provider that does not declare support for the + requested resource type and all required capabilities. + → Based on provider registration catalog item declarations. + +Step 4: Reserve Query + Send a reserve query to each remaining candidate provider in parallel. + → Providers have reserve_query_timeout to respond (profile-governed: PT5–30S). + → Providers that do not respond within timeout: excluded from this placement cycle. + → Providers that respond with INSUFFICIENT_CAPACITY: excluded; DCM updates + internal capacity rating for that provider. + → Providers that confirm capacity: advance to tie-breaking. + +Step 5: Tie-Breaking (deterministic hierarchy) + Applied when multiple providers confirmed capacity in Step 4: + + Priority 1: Policy preference + A Transformation policy injected a preference_score or preferred_provider_uuid. + Highest preference_score wins. preferred_provider_uuid is absolute — skips Steps 2-6. + + Priority 2: Provider declared priority + Providers declare a numeric priority at registration (default: 50). + Higher value = preferred when all else equal. + + Priority 3: Tenant affinity + Tenant's Policy Group declares preferred providers for specific resource types. + Affinity preference is a soft preference — does not override accreditation or sovereignty. + + Priority 4: Cost Analysis + Cost Analysis component provides current cost per unit per candidate provider. + Prefer lower total cost (CapEx + OpEx + licensing). + Skip if: Cost Analysis unavailable, data stale > PT1H, or cost difference < 5%. + + Priority 5: Least loaded + Prefer provider with lower current capacity utilization from reserve_query response. + Skip if: utilization difference < 10%, or utilization data not returned. + + Priority 6: Consistent hash (final tiebreaker — always resolves) + SHA-256(request_uuid + resource_type + sorted_candidate_uuids) + Deterministic — same request always resolves to the same provider in a stable cluster. + Never round-robin. + +Step 6: Reserve Confirmation + Notify the selected provider that its reservation is confirmed. + Other providers that responded to the reserve query receive a reservation release. + → Prevents capacity holds from accumulating across providers for the same request. +``` + +### 4.4 Reserve Query Protocol + +```yaml +reserve_query: + query_uuid: # idempotency key + entity_uuid: + resource_type: Compute.VirtualMachine + resource_type_spec_version: "2.1.0" + requested_fields: + cpu_count: 4 + memory_gb: 8 + storage_gb: 100 + sovereignty_requirements: + data_residency: EU + reservation_hold_ttl: PT5M # provider holds capacity for this duration + # released when: confirmed, rejected, or TTL expires +``` + +```yaml +reserve_query_response: + query_uuid: + provider_uuid: + status: confirmed | insufficient_capacity | capability_not_supported + capacity_held_until: # if confirmed + utilization_pct: 42 # current load; used for Step 5 tiebreaking + cost_per_hour: 0.32 # if Cost Analysis integration enabled + currency: USD +``` + +### 4.5 Placement Configuration + +```yaml +placement_engine_config: + reserve_query_timeout: PT10S # profile-governed default + parallel_reserve_queries: true # always true; all candidates queried simultaneously + max_candidates_per_placement: 10 # cap on parallel reserve queries + cost_freshness_max: PT1H + cost_difference_threshold: 0.05 # 5% — skip cost step if within this band + utilization_difference_threshold: 0.10 # 10% — skip utilization step if within this band + reservation_hold_ttl: PT5M +``` + +### 4.6 Placement Failure and Recovery + +When placement cannot find an eligible provider: + +| Failure Reason | Recovery Trigger | +|---------------|-----------------| +| All providers fail sovereignty filter | `RESERVE_QUERY_ALL_EXHAUSTED` | +| All providers fail accreditation filter | `RESERVE_QUERY_ALL_EXHAUSTED` | +| All providers respond INSUFFICIENT_CAPACITY | `RESERVE_QUERY_ALL_EXHAUSTED` | +| All reserve queries time out | `RESERVE_QUERY_ALL_EXHAUSTED` | + +The `RESERVE_QUERY_ALL_EXHAUSTED` trigger fires the active Recovery Policy. Default action per profile: standard/prod → `NOTIFY_AND_WAIT`; dev → `RETRY` with exponential backoff. + +### 4.7 Placement System Policies + +| Policy | Rule | +|--------|------| +| `PLC-001` | Sovereignty pre-filter runs before any provider is contacted. Providers that fail sovereignty constraints never receive reserve queries. | +| `PLC-002` | Accreditation filter runs before reserve queries. Providers without required accreditations for the payload's data classifications are excluded. | +| `PLC-003` | Reserve queries are sent in parallel to all eligible candidates. Sequential querying is not permitted — it introduces latency and prevents fair capacity comparison. | +| `PLC-004` | The consistent hash tiebreaker is always the final tiebreaker. It ensures deterministic provider selection for identical inputs without round-robin non-determinism. | +| `PLC-005` | A confirmed reservation hold must be released when not used — either by confirmation dispatch or by explicit release on timeout. Capacity holds must not accumulate silently. | +| `PLC-006` | The Placement Engine never selects a provider based solely on network position or co-location. Every selection is based on declared constraints, policies, and the tie-breaking hierarchy. | + +--- + +## 5. The Lifecycle Constraint Enforcer + +### 5.1 Role + +The Lifecycle Constraint Enforcer is a DCM control plane component that monitors all realized entities against their declared lifecycle constraints and fires expiry actions when constraints are reached. It is the authoritative enforcer of TTL, expiry date, and maximum execution time declarations. + +Lifecycle constraint enforcement is a DCM concern — not a provider concern. The provider does not need to know about or implement any TTL logic. + +### 5.2 What It Monitors + +The Lifecycle Constraint Enforcer monitors three categories of constraint: + +**Category 1 — Entity TTL:** +Duration-based: entity expires T duration after a reference point (realization, creation, last modification). + +**Category 2 — Entity Expiry Date:** +Calendar-based: entity expires at an absolute timestamp. + +**Category 3 — Process Resource Maximum Execution Time:** +Process Resources must declare `max_execution_time`. The Enforcer monitors all executing Process Resources and fires `on_max_exceeded` when the limit is reached. + +### 5.3 Monitoring Loop + +``` +Lifecycle Constraint Enforcer runs continuously: + + Every cycle (interval: PT1M for standard/prod; PT5M for homelab/dev): + + Query Realized Store for entities with: + lifecycle_state IN [OPERATIONAL, SUSPENDED, EXECUTING] + AND lifecycle_constraints declared + AND NOT already in terminal state + + For each entity: + Compute time_remaining = constraint_expiry - now() + + If time_remaining <= warn_before_expiry: + If warn_not_yet_sent: + Emit: entity.ttl_warning notification + Record: WARNING_EMITTED in entity provenance + + If time_remaining <= 0: + Execute on_expiry action (see Section 5.4) +``` + +### 5.4 Expiry Action Execution + +When a lifecycle constraint fires, the Enforcer executes the declared `on_expiry` action: + +| Action | Behavior | +|--------|---------| +| `decommission` | Submit a decommission request through the standard pipeline — produces Requested State, dispatches to provider, full audit trail | +| `suspend` | Submit a suspend request through the standard pipeline | +| `notify` | Fire `entity.ttl_expired` notification to entity owner; no automated action | +| `review` | Entity enters PENDING_EXPIRY_ACTION state; Platform Admin and owner notified | +| `escalate` | Immediately escalate to Platform Admin; entity enters PENDING_EXPIRY_ACTION state | + +**Grace period:** Expiry actions are not immediate. The Enforcer respects the declared `grace_period` (default PT1H) — the action fires `grace_period` after the constraint expires, giving human operators a window to intervene. + +**Action failure:** If the expiry action fails to execute (provider unreachable, dependency conflict), the entity enters `PENDING_EXPIRY_ACTION` state (LTC-005). The Enforcer retries per the active Recovery Policy. Platform Admin is notified with urgency: high. + +### 5.5 Expiry Audit Records + +Every expiry-related event produces an audit record: + +```yaml +audit_record: + action: EXPIRY_WARNING | EXPIRY_ACTION_FIRED | EXPIRY_ACTION_FAILED | + PENDING_EXPIRY_ACTION_ENTERED + actor: + type: system + system_actor: + component: lifecycle_constraint_enforcer + trigger: ttl_reached | expires_at_reached | max_execution_time_reached + entity_uuid: + details: + constraint_type: ttl | expires_at | max_execution_time + constraint_value: + action_taken: decommission | suspend | notify | review | escalate + grace_period_remaining: +``` + +### 5.6 Process Resource Enforcement + +Process Resources require `max_execution_time` (mandatory). The Enforcer monitors all EXECUTING Process Resources: + +``` +Process Resource enters EXECUTING state + │ + ▼ Enforcer records: execution_started_at; computes execution_timeout_at + │ + ▼ On every monitoring cycle: + │ If now() >= execution_timeout_at: + │ Emit: PROCESS_TIMEOUT event + │ Entity state → FAILED + │ Recovery Policy: COMPENSATION_FAILED trigger if resources were modified + │ Notification: entity owner + Platform Admin (urgency: high) +``` + +### 5.7 Lifecycle Constraint Enforcer Policies + +| Policy | Rule | +|--------|------| +| `LCE-001` | The Lifecycle Constraint Enforcer runs as a continuous monitor. It does not rely on provider callbacks or event triggers for expiry detection — it polls based on declared constraints. | +| `LCE-002` | Expiry actions are submitted through the standard DCM request pipeline. Decommission-on-expiry produces a Requested State record with `actor: system/lifecycle-constraint-enforcer`. | +| `LCE-003` | The Enforcer respects the declared grace_period before firing expiry actions. Grace period gives human operators a window to intervene before automated action. | +| `LCE-004` | Process Resource max_execution_time enforcement fires immediately on breach — no grace period. Hung processes are failed immediately to prevent resource leaks. | +| `LCE-005` | Expiry action failures enter PENDING_EXPIRY_ACTION state. The Enforcer retries per the active Recovery Policy. Indefinite retry without escalation is not permitted. | + +--- + +## 6. The Search Index + +### 6.1 Role + +The Search Index is a **non-authoritative, queryable projection** of the GitOps stores (Intent Store and Requested Store). It enables millisecond-latency queries against stored entities without traversing Git history, while the GitOps stores remain the authoritative source of truth. + +The Search Index is a PostgreSQL store contract. It has its own registration, health check, and sovereignty declaration. It is never the source of truth — if the Search Index and the GitOps store disagree, the GitOps store wins unconditionally. + +### 6.2 What It Indexes + +The Search Index maintains a projection of key fields from Intent State and Requested State records, enabling queries without retrieving full payloads from Git: + +```yaml +search_index_record: + entity_uuid: + entity_handle: + resource_type: Compute.VirtualMachine + resource_type_category: Compute + tenant_uuid: + lifecycle_state: OPERATIONAL + drift_status: clean + provider_uuid: + deployment_posture: prod + compliance_domains: [hipaa] + data_classifications: [restricted] # highest classification in entity + created_at: + updated_at: + cost_per_hour: 0.32 + currency: USD + git_path: intent-store/tenant-uuid/Compute/VirtualMachine/entity-uuid/intent.yaml + # git_path is the pointer back to the authoritative record + tags: { environment: production, team: payments } +``` + +### 6.3 Required Query Operations + +| Operation | Description | +|-----------|-------------| +| `find_by_uuid(entity_uuid)` | Return index record for a single entity | +| `find_by_tenant(tenant_uuid, filters)` | Return all entities for a Tenant with optional field filters | +| `find_by_resource_type(fqn, filters)` | Return all entities of a resource type | +| `find_by_provider(provider_uuid, filters)` | Return all entities hosted at a provider | +| `find_by_lifecycle_state(state, tenant_uuid)` | Return entities in a given lifecycle state | +| `find_by_drift_status(status, tenant_uuid)` | Return drifted or clean entities | +| `find_by_data_classification(classification)` | Return entities containing data of a given classification | +| `full_text_search(query, tenant_uuid)` | Full-text search across handle, display_name, tags | + +All queries return the `git_path` — consumers fetch the full payload from Git if needed. + +### 6.4 Consistency Model + +The Search Index is **eventually consistent** with the GitOps stores. There is a defined maximum staleness: + +```yaml +search_index_consistency: + max_staleness: PT5M # index must be within 5 minutes of GitOps store + profile_overrides: + prod: PT2M + fsi: PT1M + sovereign: PT1M + on_staleness_exceeded: + action: degrade_with_warning # serve results with staleness warning + alert: platform_admin # alert on staleness exceeding 2× max + rebuild_on_recovery: true # full index rebuild from Git history on failure + rebuild_max_duration: PT4H # must complete within 4 hours for standard+ +``` + +### 6.5 Unavailability Behavior + +If the Search Index is unavailable: +- DCM degrades search operations gracefully: returns a `503 Service Degraded` response with a reference to the authoritative Git store +- Writes are not affected — GitOps stores are written directly; the index is updated asynchronously +- On recovery: the Search Index rebuilds from Git history +- No data is lost if the index is lost — it is always reconstructable from Git + +### 6.6 Search Index Policies + +| Policy | Rule | +|--------|------| +| `SIX-001` | The Search Index is non-authoritative. GitOps stores win on any disagreement. Consumers must be prepared to receive a git_path and fetch from the authoritative store. | +| `SIX-002` | The Search Index must be rebuildable from Git history at any time. Implementations that cannot perform a full index rebuild are non-conformant. | +| `SIX-003` | Search Index staleness beyond the profile-governed maximum triggers a platform admin alert. Staleness is surfaced in query responses — consumers are never served stale data silently. | +| `SIX-004` | Search Index unavailability degrades queries without impacting writes. Write operations proceed directly to the authoritative GitOps stores regardless of Search Index availability. | + +--- + + +--- + +## 7. The Drift Reconciliation Component + +### 7.1 Role + +The Drift Reconciliation Component compares the Discovered State of entities against their Realized State to detect, classify, and respond to drift. It is the consumer of Discovered Store data and the producer of drift records that feed into the Policy Engine for response evaluation. + +Drift Reconciliation is purely a read-and-compare component — it never writes to the Realized Store. It reads Discovered State, reads Realized State, computes differences, classifies severity, and fires events into the Request Orchestrator. The Policy Engine and Recovery Policies determine what happens next. + +### 7.2 Inputs and Outputs + +**Inputs:** +- Discovered State snapshots (from Discovered Store, written by Discovery Scheduler) +- Realized State snapshots (from Realized Store) +- Resource Type Specifications (for field criticality declarations used in severity classification) +- Active governance profile (for magnitude thresholds used in severity classification) + +**Outputs:** +- Drift records (written to Drift Record Store — a lightweight operational store) +- Drift events published to the Request Orchestrator: `drift.detected`, `drift.resolved`, `drift.severity_escalated` +- Unsanctioned change events: `unsanctioned_change.detected` + +### 7.3 Comparison Algorithm + +``` +Discovery cycle completes → Discovered State snapshot written + │ + ▼ Drift Reconciliation Component receives discovery.cycle_complete event + │ + ▼ For each entity UUID in the discovery snapshot: + │ + │ Load: latest Realized State snapshot for entity UUID + │ Load: Discovered State snapshot (just written) + │ Load: Resource Type Specification (field criticality per field) + │ + ▼ Field-by-field comparison: + │ For each field in Realized State: + │ Does Discovered State contain this field? + │ If yes: are the values equal? + │ If no: field is absent — severity based on field criticality + │ For each field in Discovered State not in Realized State: + │ New field appeared — severity based on field criticality + │ + ▼ Severity classification (per field): + │ Field criticality (from Resource Type Spec) × Change magnitude (profile-governed) + │ → severity matrix → minor | significant | critical + │ Unsanctioned? → elevate one level + │ Multiple drifted fields? → overall = highest individual severity + │ + ▼ Unsanctioned check: + │ Is there a Requested State record that explains this change? + │ If yes: sanctioned change (may still be drift if realization didn't match) + │ If no: unsanctioned_change.detected event fired (in addition to drift.detected) + │ + ├── No drift detected: + │ Update entity.last_discovered_at + │ Update entity.drift_status = clean + │ No drift record created + │ + └── Drift detected: + Create drift record + Publish drift.detected to Request Orchestrator + Policy Engine evaluates → response action +``` + +### 7.4 Drift Record Structure + +```yaml +drift_record: + uuid: + entity_uuid: + detected_at: + discovery_snapshot_uuid: # the Discovered State snapshot that triggered this + realized_state_uuid: # the Realized State snapshot compared against + + overall_severity: minor | significant | critical + unsanctioned: true | false # true if no corresponding Requested State record + + drifted_fields: + - field_path: "fields.memory_gb" + realized_value: 8 + discovered_value: 16 + field_criticality: medium # from Resource Type Spec + change_magnitude: significant # 100% increase, threshold: standard 10-50% + field_severity: significant + elevated_for_unsanctioned: true # elevated from significant → critical + + status: open | acknowledged | resolved | escalated + resolution: + resolved_at: + resolution_type: reverted | updated_definition | accepted | escalated | null + resolved_by_requested_state_uuid: +``` + +### 7.5 Drift Resolution Tracking + +Drift records are not resolved by the Drift Reconciliation Component — they are resolved by the Policy Engine's response actions. The Drift Reconciliation Component monitors for resolution: + +``` +REVERT action taken: + New Requested State submitted → provider reverts → new Realized State written + Next discovery cycle: Discovered State matches new Realized State + Drift Reconciliation: no drift detected → drift_record.status = resolved + drift.resolved event published + +UPDATE_DEFINITION action taken: + Consumer submits UPDATE_DEFINITION → new Realized State written with discovered values + Next discovery cycle: Discovered State matches new Realized State + Drift record.status = resolved with resolution_type: updated_definition + +Entity decommissioned: + Drift record.status = resolved with resolution_type: decommissioned +``` + +### 7.6 Governance Matrix Integration + +Before classifying a discovered change as drift, the Drift Reconciliation Component evaluates the governance matrix to determine if the change is expected: + +``` +Field value in Discovered State differs from Realized State + │ + ▼ Check: Is there a governance matrix rule that permits this provider + │ to make this type of change to this field? + │ + ├── Yes → This may be a Provider Update Notification that wasn't submitted + │ DCM logs a warning: "Provider changed field without submitting update notification" + │ Still treated as drift — provider should have submitted update notification + │ + └── No → Standard drift detection; severity classification runs +``` + +### 7.7 Drift Reconciliation Policies + +| Policy | Rule | +|--------|------| +| `DRC-001` | The Drift Reconciliation Component never writes to the Realized Store. It produces drift records and events only. | +| `DRC-002` | Drift detection runs after every discovery cycle. An entity with no corresponding Realized State record is an orphan candidate — not a drift event. | +| `DRC-003` | Unsanctioned changes are always elevated one severity level above the matrix classification. An unsanctioned significant drift is reported as critical. | +| `DRC-004` | Drift records are retained until the entity is decommissioned plus the configured audit retention period. They are not deleted on resolution — resolution is recorded within the record. | +| `DRC-005` | Drift detection produces events into the Request Orchestrator. The Policy Engine determines the response action. The Drift Reconciliation Component does not initiate remediation directly. | + + +## 8. Related Policies — Full Component Set + +| Policy | Rule | +|--------|------| +| `CTL-001` | The Request Orchestrator is the single event bus for all request lifecycle events. No component communicates directly with another component outside of events published to the Request Orchestrator. | +| `CTL-002` | Policies ARE the orchestration. The Request Orchestrator does not contain hardcoded pipeline logic. | +| `CTL-003` | Dynamic and static flows compose naturally. Static flows are Policy Groups with concern_type: orchestration_flow and ordered: true. | +| `CTL-004` | Cost Analysis is not a billing system. It provides cost signals for placement and attribution. | +| `CTL-005` | Cost Analysis unavailability never blocks placement. | +| `PLC-001` through `PLC-006` | Placement Engine policies (see Section 4.7) | +| `LCE-001` through `LCE-005` | Lifecycle Constraint Enforcer policies (see Section 5.7) | +| `SIX-001` through `SIX-004` | Search Index policies (see Section 6.6) | +| `DRC-001` through `DRC-005` | Drift Reconciliation policies (see Section 7.7) | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/control-plane/internal-component-auth.md b/architecture/control-plane/internal-component-auth.md new file mode 100644 index 0000000..1546ff8 --- /dev/null +++ b/architecture/control-plane/internal-component-auth.md @@ -0,0 +1,367 @@ +--- +Maps to: udlm/governance/accreditation-and-authorization-matrix.md +--- + +# DCM Data Model — Internal Component Authentication + +> **Implements contracts defined in UDLM**: +> [udlm/governance/accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). +> UDLM defines the five-check boundary model and the "network position grants zero trust" +> principle. This document specifies how DCM applies that boundary model to internal +> control-plane component-to-component authentication. + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference — Zero Trust Internal Auth +**Related Documents:** [Accreditation and Zero Trust](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) | [Deployment and Redundancy](../runtime-features/deployment-redundancy.md) | [credential management service Model](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md) | [Auth Providers](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [Session Revocation](session-revocation.md) | [Design Priorities](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md) + +> **This document maps to: DATA + POLICY** +> +> Internal component identities are Data — each component has a UUID, certificate, and service account. Internal auth is Policy — the same five-check boundary model from [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) applies at every internal call boundary, with no exceptions for "trusted internal network." This document specifies how DCM's control plane components authenticate to each other in a distributed deployment. + +--- + +## 1. The Core Principle + +**Network position grants zero trust.** This is stated in [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) for external interactions. It applies equally to internal component communication. A call from the Policy Engine to the Placement Engine receives the same boundary checks as a call from an external consumer. The service mesh enforces this at the infrastructure level; DCM enforces it at the application level. + +**Two-layer enforcement:** +1. **Mesh layer (infrastructure):** mTLS mutual authentication (RFC 8446 TLS 1.3), certificate validation (RFC 5280), traffic policies — enforced by the service mesh (Istio or equivalent) +2. **Application layer (DCM):** component identity verification, operation authorization, scoped interaction credentials — enforced by DCM's Ingress and Auth subsystems + +Neither layer alone is sufficient. The mesh layer prevents impersonation at the transport level; the application layer enforces what each component is permitted to do. + +--- + +## 2. Component Identity Model + +Every DCM control plane component has a **component identity** — a stable, verifiable identity used for both mTLS and application-layer authorization. + +```yaml +component_identity: + component_uuid: # stable; assigned at deployment time + component_type: api_gateway | policy_engine | placement_engine | request_orchestrator | + scoring_engine | drift_reconciler | lifecycle_enforcer | notification_router | + audit_store | session_store | message_bus | service_provider_proxy + component_name: # human-readable; e.g. "policy-engine-eu-west-1" + deployment_uuid: # identifies the DCM deployment instance + + # Certificate identity + mtls_certificate: + subject: "CN=-,O=dcm-internal" + san: [, , ] + issuer_ca: + issued_at: + expires_at: + + # Service account (application layer) + service_account_uuid: # DCM actor of type "component_service_account" + allowed_operations: [] # what this component may call + allowed_targets: [] # which components it may call +``` + +### 2.1 Component Types and Communication Graph + +Not every component may call every other. The allowed communication graph is declared and enforced: + +``` +Consumer/Admin/Provider → API Gateway +API Gateway → Request Orchestrator +API Gateway → Policy Engine (for direct policy evaluation) +API Gateway → Session Store (token validation) + +Request Orchestrator → Policy Engine +Request Orchestrator → Placement Engine +Request Orchestrator → Scoring Engine +Request Orchestrator → Audit Store +Request Orchestrator → Message Bus + +Policy Engine → Audit Store +Policy Engine → Message Bus (policy evaluation events) + +Placement Engine → Audit Store +Placement Engine → Message Bus + +Scoring Engine → Audit Store + +Drift Reconciler → API Gateway (discovery dispatch) +Drift Reconciler → Audit Store +Drift Reconciler → Message Bus + +Lifecycle Enforcer → API Gateway (decommission dispatch) +Lifecycle Enforcer → Audit Store +Lifecycle Enforcer → Message Bus + +Notification Router → Message Bus (subscribe) +Notification Router → credential management service Proxy (notification channel credentials) + +All components → Session Store (revocation check) +All components → credential management service Proxy (interaction credential requests) +``` + +**ICOM-004:** Components may only call components declared in their `allowed_targets` list. A call from an unexpected source component is rejected with `403 Forbidden` and an audit record. + +--- + +## 3. Certificate Issuance and Internal CA + +### 3.1 Certificate Authority for Internal Components + +Each DCM deployment uses a **registered Certificate Authority (CA)** for issuing component mTLS certificates. This may be: + +**Option A — Built-in Internal CA (default):** DCM operates its own CA per deployment. Simple to configure; no external dependencies; suitable for minimal through standard profiles. + +**Option B — External CA via credential management service:** An enterprise CA registered as a credential management service (HashiCorp Vault PKI, Venafi TLS Protect, EJBCA, AWS ACM Private CA, Azure Key Vault). The external CA issues component certificates using the standard credential management service interface — DCM requests certificates via the provider's API (ACME/EST/SCEP/CMP). See [credential management service Model](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md) for registration. Recommended for fsi and sovereign profiles where the enterprise PKI chain must be maintained. + +Both options satisfy ICOM-001 (mTLS required). The distinction is who issues the certificates, not whether mTLS is used. + +**The registered CA's root certificate is installed in all component trust stores at deployment time.** For Option B, the credential management service's CA root (which may itself be a subordinate of an enterprise root) is the trust anchor. + +```yaml +internal_ca: + ca_uuid: + deployment_uuid: + ca_type: built_in | external_service_provider + service_provider_uuid: # if ca_type: external + external_ca_protocol: acme | est | scep | cmp | null # if external + root_cert_fingerprint: + certificate_lifetime: P90D # profile-governed — see table below + renewal_trigger: P14D + algorithm: ECDSA-P-384 # FIPS-compliant; all profiles + crl_endpoint: + ocsp_endpoint: +``` + +### 3.2 Profile-Governed Certificate Configuration + +| Profile | Cert lifetime | Renewal trigger | Bootstrap token TTL | Min key algorithm | +|---------|--------------|-----------------|--------------------|--------------------| +| `homelab` | P180D | P30D | PT4H | RSA-2048 (min) | +| `dev` | P90D | P14D | PT1H | RSA-2048 (min) | +| `standard` | P90D | P14D | PT1H | ECDSA-P-256 (min) | +| `prod` | P90D | P14D | PT1H | ECDSA-P-384 | +| `fsi` | P30D | P7D | PT30M | ECDSA-P-384 | +| `sovereign` | P14D | P3D | PT15M | ECDSA-P-384 (HSM-backed if hardware_attested) | + +> **sovereign profile:** Certificates must be HSM-backed if the deployment posture is `hardware_attested`. The external CA option (Option B) using an HSM-backed Vault PKI backend satisfies this requirement. + +### 3.3 Certificate Lifecycle + +``` +Component starts + │ + ▼ Does component have a valid certificate? + │ YES → Use existing certificate + │ NO (first start or expired) → Request certificate from Internal CA + │ + ▼ Certificate request to Internal CA: + │ component_uuid, component_type, deployment_uuid + │ CSR signed with bootstrap key (see Section 5) + │ + ▼ Internal CA issues certificate + │ Subject: CN=-,O=dcm-internal + │ SAN: component_uuid, component_name, internal DNS name + │ Valid for: P90D (profile-governed) + │ + ▼ Component stores certificate; begins accepting mTLS connections + │ + ▼ 14 days before expiry: auto-renewal + Background thread requests new certificate + Transition: both old and new cert valid for PT1H + Old cert retired after transition +``` + +--- + +## 4. Application-Layer Authorization + +mTLS verifies **who** is calling. Application-layer authorization verifies **what** the caller is permitted to do. + +### 4.1 Interaction Credential for Internal Calls + +Every internal component call follows the same ZTS-002 scoped interaction credential model used for external provider dispatch: + +``` +Component A prepares to call Component B + │ + ▼ Request interaction credential from credential management service Proxy: + │ credential_type: dcm_interaction + │ issued_to.component_uuid: + │ scope.operations: [] + │ scope.target_component: + │ expires_at: (explicit narrowing of the provider-callback §3.3 ladder for internal calls) + │ + ▼ Call Component B with: + │ mTLS certificate (transport identity) + │ Interaction credential in Authorization header (operation authorization) + │ Correlation ID (tracing) + │ + ▼ Component B validates: + │ 1. mTLS cert from Component A's known CA ✓ + │ 2. Interaction credential: not revoked, not expired, scoped to this operation ✓ + │ 3. Component A is in allowed_sources for this endpoint ✓ + │ 4. Operation matches declared scope ✓ + │ → All pass: process request + │ → Any fail: 403 + audit record ICOM_AUTH_FAILURE +``` + +### 4.2 Internal Endpoint Authorization + +Each internal component endpoint declares which source components are permitted to call it: + +```yaml +internal_endpoint: + component: policy_engine + endpoint: POST /internal/evaluate + allowed_sources: + - api_gateway + - request_orchestrator + required_scope: policy.evaluate + audit_every_call: true # all internal calls are audited +``` + +**ICOM-003:** Internal endpoints that receive calls from unauthorized source components return 403 and write an `ICOM_UNAUTHORIZED_SOURCE` audit record. This audit record has urgency: high — unexpected internal call patterns are security signals. + +--- + +## 5. Bootstrap — First Certificate + +The bootstrap problem: a new component needs a certificate, but it has no certificate yet to authenticate its request. DCM solves this with a **bootstrap token** mechanism. + +### 5.1 Bootstrap Token + +At deployment time, the platform admin generates a one-time bootstrap token for each component: + +``` +POST /api/v1/admin/components/bootstrap-tokens + +{ + "component_type": "policy_engine", + "component_uuid": "", + "deployment_uuid": "", + "expires_at": "" // short-lived: PT1H maximum +} + +Response 201: +{ + "bootstrap_token": "", // one-time use; stored as env var or secret + "component_uuid": "", + "expires_at": "" +} +``` + +### 5.2 First Certificate Acquisition + +``` +New component starts with bootstrap_token in environment + │ + ▼ POST /internal/ca/issue-certificate + │ Authorization: Bootstrap + │ Body: { component_uuid, component_type, deployment_uuid, csr_pem } + │ + ▼ Internal CA validates: + │ Bootstrap token not expired + │ Bootstrap token not previously used (one-time) + │ component_uuid matches token's declared component_uuid + │ + ▼ Certificate issued + │ Bootstrap token invalidated immediately after use + │ + ▼ Component uses certificate for all subsequent communication + No further need for bootstrap token +``` + +**ICOM-007:** Bootstrap tokens are one-time-use and must expire within PT1H of creation. A bootstrap token that is not used within PT1H is automatically invalidated. Platform admins must generate new tokens if a component fails to start within the window. + +### 5.3 Kubernetes Deployment Integration + +In Kubernetes deployments, bootstrap tokens are injected as Kubernetes Secrets and mounted as environment variables. The component reads the bootstrap token on startup, acquires its certificate, then deletes the Kubernetes Secret. This ensures the bootstrap credential is not persisted beyond initial use. + +```yaml +# Kubernetes Secret (deleted by component after first cert acquisition) +apiVersion: v1 +kind: Secret +metadata: + name: dcm-policy-engine-bootstrap +type: Opaque +stringData: + DCM_BOOTSTRAP_TOKEN: "" + DCM_COMPONENT_UUID: "" + DCM_INTERNAL_CA_ENDPOINT: "https://dcm-internal-ca.dcm-system.svc.cluster.local" +``` + +--- + +## 6. Certificate Compromise Response + +If a component certificate is compromised, the response follows the same emergency pattern as credential compromise: + +``` +Certificate compromise detected + │ + ▼ Compromised cert added to Internal CA CRL + │ CRL update propagated to all components within SLA: + │ standard/prod: PT1M + │ fsi/sovereign: PT15S + │ + ▼ Component identity suspended in DCM + │ All active interaction credentials for this component → revoked + │ ICOM_CERT_COMPROMISED audit record written + │ + ▼ Platform admin notified (urgency: critical) + │ + ▼ New certificate issued for legitimate component instance + │ Previous certificate remains in CRL permanently + │ + ▼ Component resumes with new certificate +``` + +**ICOM-008:** Compromised internal component certificates are added to the Internal CA CRL immediately. All other components refresh their CRL cache within the profile-governed SLA and reject connections presenting the revoked certificate. + +--- + +## 7. Deployment Architecture Summary + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ DCM Control Plane │ +│ │ +│ ┌──────────┐ mTLS+cred ┌──────────────────┐ │ +│ │API Gateway│───────────→│Request Orchestrator│ │ +│ └──────────┘ └────────┬─────────┘ │ +│ │ mTLS+cred (each call) │ +│ ┌───────────────┼───────────────┐ │ +│ ↓ ↓ ↓ │ +│ ┌─────────────┐ ┌──────────┐ ┌──────────────┐ │ +│ │Policy Engine│ │Placement │ │Scoring Engine│ │ +│ └─────────────┘ │Engine │ └──────────────┘ │ +│ └──────────┘ │ +│ │ +│ ┌──────────────────┐ ┌─────────────┐ ┌──────────────────┐ │ +│ │credential management service│ │Session Store│ │Internal CA │ │ +│ │Proxy │ │ │ │(cert authority) │ │ +│ └──────────────────┘ └─────────────┘ └──────────────────┘ │ +│ │ +│ Service Mesh (Istio): mTLS enforcement at transport layer │ +│ All calls: authenticated + authorized + audited │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 8. System Policies + +| Policy | Rule | +|--------|------| +| `ICOM-001` | All internal component-to-component communication must use mTLS with certificates issued by the deployment's Internal CA. Plaintext internal communication is prohibited in all profiles. | +| `ICOM-002` | Every internal call must present a scoped interaction credential (ZTS-002) in addition to the mTLS certificate. The mTLS certificate proves identity; the interaction credential proves authorization for the specific operation. | +| `ICOM-003` | Internal endpoints reject calls from components not in their `allowed_sources` list with 403 and an `ICOM_UNAUTHORIZED_SOURCE` audit record (urgency: high). | +| `ICOM-004` | Components may only call components declared in their `allowed_targets` list. Attempts to call unauthorized components are rejected at the mesh layer (traffic policy) and, if they reach the application layer, at the application layer. | +| `ICOM-005` | All internal component calls are audited: source component, target component, operation, interaction credential UUID, outcome. Internal audit records are written to the same Audit Store as external interactions. | +| `ICOM-006` | Component certificates are issued by the **registered trust-anchor CA** (the built-in Internal CA by default; a registered external CA per ICOM-009 / ADR-022) with a maximum validity of P90D and renewed automatically P14D before expiry. Certificates from **unregistered** CAs are not accepted. | +| `ICOM-007` | Bootstrap tokens are one-time-use and expire within PT1H. A bootstrap token that has been used is immediately invalidated. Unused tokens are invalidated at expiry. | +| `ICOM-008` | Compromised internal component certificates are added to the Internal CA CRL immediately. All components refresh their CRL cache within the profile-governed SLA. | +| `ICOM-009` | The trust anchor for internal component mTLS is a registered root or intermediate CA whose certificate is installed in all component trust stores at deployment time. The trust anchor may be the built-in Internal CA or an external CA registered as a Certificate Provider (e.g. HashiCorp Vault PKI, Venafi, EJBCA) — see [credential management service Model](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md) Section on External CAs. Components do not accept certificates from unregistered trust anchors. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/control-plane/self-health.md b/architecture/control-plane/self-health.md new file mode 100644 index 0000000..fa9072c --- /dev/null +++ b/architecture/control-plane/self-health.md @@ -0,0 +1,463 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — Operational Health +Maps to: udlm/contracts/provider-contract.md +--- + +# DCM Data Model — DCM Self-Health Endpoints + +> **Implements contracts defined in UDLM**: +> [udlm/contracts/provider-contract.md](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md). +> UDLM defines the provider health contract that Service Providers must expose. +> DCM operationalizes its own liveness, readiness, and component-health +> endpoints — DCM exposes the same health contract it requires of the providers +> it integrates, so it is itself a well-behaved provider to its operator and +> load-balancer. + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference — Operational Health +**Related Documents:** [Deployment and Redundancy](../runtime-features/deployment-redundancy.md) | [Internal Component Authentication](internal-component-auth.md) | [Operator Interface Specification](../../docs/specifications/dcm-operator-interface-spec.md) | [Admin API Specification](../../docs/specifications/dcm-admin-api-spec.md) + +> **Events:** Health state change events fire as `provider.healthy` / `provider.unhealthy` for external systems, and `governance.profile_changed` when health thresholds are adjusted. See [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md). + +> **This document maps to: PROVIDER** +> +> DCM itself must expose the same health contract it requires of Service Providers (doc OIS §4). This document specifies DCM's own liveness, readiness, and component health endpoints — required for Kubernetes operator deployment, load balancer health checking, and operational monitoring. + +--- + +## 1. Three Health Endpoints + +DCM exposes three distinct health endpoints, following Kubernetes conventions: + +| Endpoint | Purpose | Failure action | Authentication | +|----------|---------|---------------|---------------| +| `GET /livez` | Is DCM alive? | Kubernetes restarts the pod | None | +| `GET /readyz` | Is DCM ready to serve traffic? | Kubernetes removes from load balancer | None | +| `GET /api/v1/admin/health` | Detailed component status | Informational — no automatic action | Admin auth required | + +Liveness and readiness are unauthenticated because they must work before authentication infrastructure is operational (e.g. during startup). + +--- + +## 2. Liveness — `/livez` + +Liveness answers: **is this DCM process alive?** + +A liveness failure means the process is deadlocked, in an unrecoverable state, or otherwise unable to continue. Kubernetes responds by restarting the pod. + +```http +GET /livez HTTP/1.1 + +HTTP/1.1 200 OK +Content-Type: application/health+json + +{ + "status": "pass" +} +``` + +**Liveness checks (minimal — fast):** +- Process is responding +- No deadlock detected in core event loop +- Internal CA is reachable (for deployments with component auth) + +**Liveness failure response:** +```http +HTTP/1.1 503 Service Unavailable +Content-Type: application/health+json + +{ + "status": "fail", + "failure_reason": "event_loop_deadlock | internal_ca_unreachable | oom_imminent" +} +``` + +**Liveness SLA:** Must respond within PT5S. No external calls. No database reads. + +--- + +## 3. Readiness — `/readyz` + +Readiness answers: **is this DCM instance ready to serve requests?** + +A readiness failure removes the instance from the load balancer rotation without restarting it. This handles startup, migration, and graceful drain scenarios. + +```http +GET /readyz HTTP/1.1 + +HTTP/1.1 200 OK +Content-Type: application/health+json + +{ + "status": "pass", + "checks": { + "session_store": "pass", + "audit_store": "pass", + "policy_engine": "pass", + "message_bus": "pass", + "auth_provider": "pass" + } +} +``` + +**Readiness checks:** +- Session Store: can write and read a test record +- Audit Store: reachable and writable +- Policy Engine: responding to internal health ping +- Message Bus: connected and subscribed +- Auth Provider: at least one Auth Provider is responding +- Schema version: database schema matches running code version + +**Readiness failure** (any check fails): +```http +HTTP/1.1 503 Service Unavailable +Content-Type: application/health+json + +{ + "status": "fail", + "checks": { + "session_store": "pass", + "audit_store": "fail", + "policy_engine": "pass", + "message_bus": "pass", + "auth_provider": "pass" + }, + "failing_checks": ["audit_store"] +} +``` + +**Readiness SLA:** Must respond within PT10S. Performs lightweight connectivity checks — no heavy queries. + +### 3.1 Startup vs Operational Readiness + +During startup, DCM goes through a startup sequence before becoming ready: + +``` +Process starts + │ + ▼ /livez → pass (process alive) + │ /readyz → fail (not ready yet) + │ + ▼ Internal CA connects → component certs verified + ▼ Session Store connected → revocation registry loaded + ▼ Audit Store connected → schema version validated + ▼ Policy Engine ready → policies loaded and shadow mode initialized + ▼ Auth Providers connected → at least one responding + ▼ Message Bus connected → subscriptions established + │ + ▼ /readyz → pass (ready to serve traffic) +``` + +`startupProbe` in Kubernetes uses `/readyz` with a longer `failureThreshold` to allow startup time before the liveness probe takes over. + +--- + +## 4. Detailed Health — `/api/v1/admin/health` + +The detailed health endpoint provides per-component status for operational monitoring. Requires admin authentication. + +```http +GET /api/v1/admin/health HTTP/1.1 +Authorization: Bearer + +HTTP/1.1 200 OK +Content-Type: application/health+json + +{ + "status": "pass | warn | fail", + "dcm_version": "1.2.0", + "dcm_instance_uuid": "", + "deployment_profile": "prod", + "uptime_seconds": 864023, + "checked_at": "", + + "components": { + "api_gateway": { + "status": "pass", + "latency_p99_ms": 12, + "requests_per_minute": 340 + }, + "request_orchestrator": { + "status": "pass", + "queue_depth": 3, + "in_flight": 7 + }, + "policy_engine": { + "status": "pass", + "active_policies": 42, + "shadow_policies": 3, + "evaluations_per_minute": 280 + }, + "placement_engine": { + "status": "pass" + }, + "scoring_engine": { + "status": "pass", + "evaluations_per_minute": 280 + }, + "request_scheduler": { + "status": "pass", + "scheduled_requests_queued": 5, + "next_dispatch_at": "" + }, + "drift_reconciler": { + "status": "pass", + "last_cycle_completed_at": "", + "open_drift_records": 2 + }, + "lifecycle_enforcer": { + "status": "pass", + "entities_monitored": 1240, + "ttl_warnings_pending": 3 + }, + "discovery_scheduler": { + "status": "pass", + "pending_jobs": 1, + "last_completed_at": "" + }, + "notification_router": { + "status": "pass", + "providers_active": 2, + "delivery_backlog": 0 + }, + "session_store": { + "status": "pass", + "active_sessions": 47, + "revocation_registry_size": 3 + }, + "audit_store": { + "status": "pass", + "records_last_hour": 1840, + "integrity": "verified" # Merkle inclusion/consistency + }, + "message_bus": { + "status": "pass", + "lag_consumer_group_ms": 12 + }, + "internal_ca": { + "status": "pass", + "certificates_active": 12, + "next_expiry_at": "" + } + }, + + "providers": { + "registered": 4, + "healthy": 4, + "degraded": 0, + "unhealthy": 0 + }, + + "auth_providers": { + "registered": 2, + "healthy": 2, + "unhealthy": 0 + } +} +``` + +### 4.1 Status Semantics + +| Status | Meaning | +|--------|---------| +| `pass` | Component fully operational | +| `warn` | Operational but degraded (high latency, reduced capacity, elevated error rate) | +| `fail` | Component not operational; DCM degraded | + +The top-level `status` is the worst status across all components: +- Any `fail` → top-level `fail` +- Any `warn`, no `fail` → top-level `warn` +- All `pass` → top-level `pass` + +--- + +## 5. Kubernetes Manifest + +```yaml +# Standard Kubernetes probe configuration for DCM +livenessProbe: + httpGet: + path: /livez + port: 8443 + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 5 + +readinessProbe: + httpGet: + path: /readyz + port: 8443 + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 5 + failureThreshold: 6 + timeoutSeconds: 10 + +startupProbe: + httpGet: + path: /readyz + port: 8443 + scheme: HTTPS + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 30 # allow up to 300s for startup + timeoutSeconds: 10 +``` + +--- + +## 6. Prometheus Metrics + +DCM exposes Prometheus-compatible metrics alongside health endpoints: + +``` +GET /metrics # Prometheus scrape endpoint (unauthenticated in cluster; + # configurable for external exposure) +``` + +Key metric families: + +``` +# Request pipeline +dcm_requests_total{status, resource_type, profile} +dcm_request_duration_seconds{quantile, resource_type} +dcm_requests_pending_dependency_total +dcm_requests_scheduled_total + +# Policy engine +dcm_policy_evaluations_total{outcome, enforcement_class} +dcm_policy_shadow_divergences_total + +# Sessions +dcm_sessions_active_total +dcm_session_revocations_total{trigger} + +# Drift +dcm_drift_open_records_total{severity} +dcm_drift_detected_total + +# Providers +dcm_providers_registered_total +dcm_providers_healthy_total +dcm_provider_dispatch_duration_seconds{provider_type, quantile} + +# Internal +dcm_internal_ca_certificates_active +dcm_internal_ca_days_until_next_expiry +``` + +--- + +## 7. Per-Provider Metrics Contract + +In addition to DCM control plane metrics, each registered Service Provider must +expose a Prometheus-compatible `/metrics` endpoint meeting the contract defined in +the Registration Specification (GATE-SP-05). + +### 7.1 Required Provider Metric Families + +``` +# Dispatch metrics — how many requests DCM sent to this provider +dcm_provider_dispatches_total{resource_type="Compute.VirtualMachine", outcome="success|failed|timeout"} +dcm_provider_dispatch_duration_seconds{resource_type="Compute.VirtualMachine", quantile="0.5|0.95|0.99"} + +# Realization metrics — outcomes of provisioning +dcm_provider_realizations_total{resource_type="Compute.VirtualMachine", status="OPERATIONAL|FAILED"} + +# Health signal +dcm_provider_health_status # gauge: 1=healthy, 0.5=degraded, 0=unhealthy +``` + +### 7.2 Recommended Provider Metric Families + +``` +# Capacity +dcm_provider_capacity_remaining{resource_type="Compute.VirtualMachine"} # gauge +dcm_provider_capacity_total{resource_type="Compute.VirtualMachine"} # gauge + +# Queue depth (for async-only providers) +dcm_provider_queue_depth{resource_type="Compute.VirtualMachine"} # gauge + +# Tenant usage +dcm_provider_active_resources_total{tenant_uuid="...", resource_type="..."} +``` + +### 7.3 DCM Control Plane Aggregated Provider Metrics + +The DCM control plane exposes aggregated provider metrics at its own `/metrics` +endpoint alongside the control plane metrics from Section 6: + +``` +# Already in Section 6 — shown here for cross-reference +dcm_providers_registered_total # count of registered providers +dcm_providers_healthy_total # count currently healthy +dcm_provider_dispatch_duration_seconds{...} # aggregated across all providers +``` + +### 7.4 Tenant Metadata Endpoint + +Service Providers in `standard` and above profiles (GATE-SP-04) must implement +a tenant metadata endpoint that DCM calls to retrieve per-tenant usage summaries: + +``` +GET /api/v1/tenants/{tenant_uuid}/metadata +Authorization: Bearer + +Response 200: +{ + "tenant_uuid": "", + "active_resources": { + "Compute.VirtualMachine": 12, + "Storage.Block": 8 + }, + "capacity_consumed": { + "Compute.VirtualMachine": { + "cpu_cores": 96, + "ram_gb": 384 + } + }, + "quota_consumed_pct": { + "Compute.VirtualMachine": 48.0 + } +} +``` + +This data is used by DCM's Cost Analysis component and multi-tenant quota +enforcement. It is also exposed to tenant administrators via the consumer API. + +--- + +## 8. Profile-Governed Health Exposure + +| Profile | /livez | /readyz | /api/v1/admin/health | /metrics scraping | +|---------|--------|---------|----------------------|-------------------| +| `homelab` | Unauthenticated | Unauthenticated | Admin auth | Internal network only | +| `dev` | Unauthenticated | Unauthenticated | Admin auth | Internal network only | +| `standard` | Unauthenticated | Unauthenticated | Admin auth | mTLS client cert or auth token | +| `prod` | Unauthenticated | Unauthenticated | Admin auth | mTLS client cert or auth token | +| `fsi` | Unauthenticated | Unauthenticated | Admin auth (MFA required) | mTLS + authorized scraper registration | +| `sovereign` | Unauthenticated within cluster | Unauthenticated within cluster | Admin auth (MFA + step-up) | Disabled externally; internal only | + +**Notes:** +- `/livez` and `/readyz` are always unauthenticated *within the cluster* — Kubernetes probes cannot present auth credentials. However, at the ingress boundary (external load balancer), these paths may be network-restricted. +- For `fsi` and `sovereign` profiles, `/api/v1/admin/health` requires MFA-verified sessions (mfa_verified: true). Step-up MFA is required for sovereign. +- The `sovereign` profile does not expose `/metrics` externally. Prometheus must scrape from within the cluster network only. +- Component-level detail in `/api/v1/admin/health` may be redacted in fsi/sovereign profiles based on the requesting actor's role — SRE sees full detail; read-only admin sees summary only. + +## 7. System Policies + +| Policy | Rule | +|--------|------| +| `HLT-001` | DCM must expose `/livez` and `/readyz` endpoints on the same port as the API, unauthenticated, following RFC 8615 / IANA health+json format. | +| `HLT-002` | `/livez` must respond within PT5S with no external calls or database reads. A non-response within PT5S is treated as liveness failure. | +| `HLT-003` | `/readyz` returns `fail` if the Session Store, Audit Store, Policy Engine, Message Bus, or any Auth Provider is unreachable. It returns `warn` if any optional component is degraded. | +| `HLT-004` | `GET /api/v1/admin/health` requires admin authentication and provides per-component status. It must include the DCM version, instance UUID, and deployment profile. | +| `HLT-005` | DCM must expose Prometheus-compatible metrics at `GET /metrics`. Metrics must include request pipeline, policy engine, session, drift, and provider metrics at minimum. | +| `HLT-006` | The startup sequence must be observable via `/readyz`. DCM must not report `pass` on `/readyz` until the Session Store, Audit Store, Policy Engine, Auth Provider, and Message Bus are all reachable. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/control-plane/session-revocation.md b/architecture/control-plane/session-revocation.md new file mode 100644 index 0000000..63e5e08 --- /dev/null +++ b/architecture/control-plane/session-revocation.md @@ -0,0 +1,361 @@ +--- +Maps to: udlm/governance/auth-providers.md +--- + +# DCM Data Model — Session Token Revocation + +> **Implements contracts defined in UDLM**: +> [udlm/governance/auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md). +> UDLM defines the Auth Provider model and authenticated-session contract. This document +> extends that model with the explicit session-token revocation lifecycle, triggers, and +> the Session Revocation Registry that UDLM leaves unspecified. + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference — Session Lifecycle and Revocation +**Related Documents:** [Auth Providers](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [credential management service Model](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md) | [Accreditation and Zero Trust](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) | [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md) | [Design Priorities](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md) + +> **This document maps to: DATA + POLICY** +> +> A session is a Data artifact with a UUID, lifecycle state, and audit trail. Session revocation is a Policy concern — it fires on triggers defined here and enforced by the Auth Provider and Ingress layer. This document extends the Auth Provider model ([auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md)) with the explicit revocation lifecycle that was previously unspecified. +> +> **Relationship to credential revocation:** CPX-006 ([credentials.md](../credentials-and-auth/credentials.md)) governs credential revocation — when an actor is deprovisioned, all credentials issued to that actor are revoked. This document governs the complementary concern: active *session tokens* must also be invalidated on the same trigger. Credential revocation and session revocation are parallel processes that both fire on actor deprovisioning. + +--- + +## 1. What a Session Is + +A DCM session represents an authenticated actor's active interaction context. It is created when an actor successfully authenticates through an Auth Provider and is destroyed (or expires) when the session ends. + +```yaml +session_record: + session_uuid: + actor_uuid: + auth_provider_uuid: # which provider issued the session + auth_method: oidc | ldap | api_key | mtls | built_in + mfa_verified: # whether per-session MFA was completed + step_up_verified_at: # last step-up MFA completion + + created_at: + last_active_at: + expires_at: # absolute expiry (from token_ttl) + + refresh_token_uuid: # if refresh_enabled: true + refresh_expires_at: + + status: active | refreshing | revoked | expired + revocation_reason: + revoked_at: + revoked_by: + + # Provenance + client_ip: + user_agent: + tenant_uuid: + + # Concurrent session position + session_sequence: # 1 = oldest active session for this actor +``` + +### 1.1 Session Store + +Active sessions are maintained in a **Session Store** — a fast-queryable, low-latency store separate from the Realized State Store. The Session Store is not GitOps-backed; it is operational state that does not need version history. + +```yaml +session_store: + implementation: redis | postgres | in_memory # profile-governed + ttl_enforcement: hard # sessions expire at expires_at regardless + revocation_index: true # fast lookup by session_uuid for revocation + actor_index: true # fast lookup by actor_uuid for bulk revocation +``` + +**Profile-governed defaults:** + +| Profile | Store | Session TTL | Refresh TTL | Max concurrent | +|---------|-------|-------------|-------------|---------------| +| `homelab` | in_memory or sqlite | PT8H | P7D | unlimited | +| `dev` | redis or postgres | PT4H | P3D | 10 | +| `standard` | redis or postgres | PT1H | P1D | 5 | +| `prod` | redis or postgres | PT30M | PT8H | 3 | +| `fsi` | redis or postgres | PT15M | PT1H | 2 | +| `sovereign` | redis or postgres (HSM-backed) | PT15M | PT30M | 1 | + +--- + +## 2. Revocation Triggers + +Session revocation invalidates a session immediately — regardless of its remaining TTL. The following triggers cause revocation: + +| Trigger | Scope | Who initiates | Behavior | +|---------|-------|--------------|---------| +| `actor_logout` | Single session | Actor (self) | Immediate; that session only | +| `actor_logout_all` | All sessions for actor | Actor (self) | Immediate; all active sessions for this actor | +| `actor_deprovisioned` | All sessions for actor | SCIM / Platform admin | Immediate; fires before deprovisioning acknowledged | +| `actor_suspended` | All sessions for actor | Platform admin | Immediate | +| `security_event` | Specified sessions or all | Platform admin / security automation | Immediate; emergency channel notification | +| `concurrent_limit_exceeded` | Oldest session(s) | System | Oldest session revoked when new session created beyond limit | +| `auth_provider_deregistered` | All sessions from that provider | Platform admin | Immediate; actors must re-authenticate via another provider | +| `credential_compromised` | All sessions for actor | Security automation | Immediate; correlates with CPX emergency rotation | +| `admin_forced_logout` | Specified session(s) | Platform admin | Immediate | + +--- + +## 3. Revocation Lifecycle + +### 3.1 Standard Revocation + +``` +Revocation trigger fires + │ + ▼ Session record status → revoked + │ revoked_at, revocation_reason, revoked_by written + │ + ▼ Refresh token invalidated (if exists) + │ Cannot be exchanged; refresh endpoint returns 401 + │ + ▼ Session UUID added to Session Revocation Registry + │ (fast-queryable; all DCM components check this on every request) + │ + ▼ Revocation event published to Message Bus + │ event_type: auth.session_revoked + │ session_uuid, actor_uuid, revocation_trigger, revoked_at + │ + ▼ Audit record written + session_uuid, actor_uuid, revocation_trigger, revoked_by, revoked_at +``` + +### 3.2 Actor Deprovisioning Revocation (parallel with CPX-006) + +Actor deprovisioning fires both credential revocation (CPX-006) and session revocation simultaneously. Neither blocks the other; both must complete before the deprovisioning is acknowledged. + +``` +Actor deprovisioning initiated + │ + ├──→ Credential revocation (CPX-006) + │ All credentials issued to actor_uuid → revoked + │ Credential Revocation Registry updated + │ + └──→ Session revocation (this document) + All active sessions for actor_uuid → revoked + Session Revocation Registry updated + auth.session_revoked events published per session + │ + ▼ Both complete → deprovisioning acknowledged + actor_deprovisioned event published + Audit record for deprovisioning written +``` + +### 3.3 Emergency Revocation (Security Event) + +Security events bypass the standard pipeline. Revocation is immediate with no grace period. + +``` +Security event detected + │ + ▼ Target sessions determined + │ (single session, all sessions for actor, or all sessions from a provider) + │ + ▼ Sessions → revoked immediately + │ Session Revocation Registry updated within SLA: + │ standard/prod: PT30S + │ fsi: PT10S + │ sovereign: PT5S + │ + ▼ auth.security_session_revoked event published (critical urgency) + │ Routed to security team via configured notification service + │ + ▼ Platform admin notified regardless of profile + │ + ▼ All in-flight requests from these sessions → 401 Unauthorized +``` + +--- + +## 4. Session Revocation Registry + +The Session Revocation Registry is the authoritative list of revoked-but-not-yet-expired session UUIDs. Every DCM component that accepts bearer tokens must check this registry on each request. + +```yaml +session_revocation_registry: + # Session UUID → revocation record + # Fast in-memory cache with TTL equal to original session TTL + # After the original session TTL would have expired, the entry is + # removed (the session would have been invalid anyway) + + entry: + session_uuid: + revoked_at: + original_expires_at: # entry removed after this time + revocation_trigger: +``` + +**Cache refresh behavior by profile:** + +| Profile | Max cache age | Behavior on cache miss | +|---------|--------------|----------------------| +| `homelab` | PT5M | Check authoritative store; cache result | +| `standard` | PT1M | Check authoritative store; cache result | +| `prod` | PT30S | Check authoritative store; cache result | +| `fsi` | PT10S | Check authoritative store; cache result | +| `sovereign` | PT5S | No cache — always check authoritative store | + +--- + +## 5. Token Introspection + +DCM's Ingress layer exposes a token introspection endpoint for internal components and external systems that need to validate a token without maintaining their own cache: + +``` +POST /api/v1/auth:introspect + +Authorization: Bearer +Content-Type: application/json + +{ + "token": "" +} + +Response 200 (active session): +{ + "active": true, + "session_uuid": "", + "actor_uuid": "", + "expires_at": "", + "mfa_verified": true, + "tenant_uuid": "", + "roles": ["consumer"], + "scopes": ["read", "write"] +} + +Response 200 (revoked or expired): +{ + "active": false, + "reason": "revoked | expired | not_found" +} +``` + +Session tokens use JWT format (RFC 7519). This introspection endpoint follows [RFC 7662 (OAuth 2.0 Token Introspection)](https://datatracker.ietf.org/doc/html/rfc7662). + +--- + +## 6. Consumer API — Session Management Endpoints + +### 6.1 Logout (Single Session) + +``` +DELETE /api/v1/auth/session + +Response 204 No Content +``` + +Revokes the session corresponding to the bearer token in the `Authorization` header. No body required. + +### 6.2 Logout All Sessions + +``` +DELETE /api/v1/auth/sessions + +Response 204 No Content +``` + +Revokes all active sessions for the authenticated actor. + +### 6.3 List Active Sessions + +``` +GET /api/v1/auth/sessions + +Response 200: +{ + "items": [ + { + "session_uuid": "", + "created_at": "", + "last_active_at": "", + "expires_at": "", + "auth_method": "oidc", + "client_ip": "", + "current": true // true for the session making this request + } + ], + "total": 2 +} +``` + +### 6.4 Revoke Specific Session + +``` +DELETE /api/v1/auth/sessions/{session_uuid} + +Response 204 No Content +Response 404: session not found or does not belong to this actor +``` + +### 6.5 Admin: Force Revoke Session(s) + +``` +POST /api/v1/admin/actors/{actor_uuid}:revoke-sessions + +{ + "scope": "all | session", + "session_uuid": "", // required if scope: session + "reason": "" // required for audit trail +} + +Response 204 No Content +Response 404: actor not found +``` + +--- + +## 7. Concurrent Session Enforcement + +When `concurrent_sessions: N` is declared and a new session would exceed the limit, the oldest active session is revoked automatically: + +``` +New authentication succeeds + │ + ▼ Count active sessions for actor_uuid + │ If count >= concurrent_sessions limit: + │ Revoke oldest session (by created_at) + │ Trigger: concurrent_limit_exceeded + │ + ▼ New session created +``` + +The evicted actor receives an `auth.session_revoked` notification if a notification service is configured with the actor's notification preferences. The event does not block the new session creation. + +--- + +## 8. Relationship to the Credential Revocation Model + +Session revocation (this document) and credential revocation ([credentials.md](../credentials-and-auth/credentials.md), CPX-001–CPX-012) are parallel but distinct: + +| | Session Revocation | Credential Revocation | +|--|---|---| +| **What** | Bearer token / session cookie validity | API key, x509, SSH key, service account token | +| **Store** | Session Revocation Registry | Credential Revocation Registry | +| **Propagation** | Auth layer cache refresh | Message Bus → all components | +| **Actor deprovision** | All sessions revoked | All credentials revoked | +| **TTL** | Session TTL (minutes to hours) | Credential TTL (hours to years) | +| **Emergency SLA** | PT5S–PT30S | PT30S–PT5M | +| **Event** | `auth.session_revoked` | `credential.revoked` | + +**AUTH-016:** On actor deprovisioning, session revocation and credential revocation are parallel operations. The deprovisioning is not acknowledged until both are confirmed complete. + +--- + +## 9. System Policies + +| Policy | Rule | +|--------|------| +| `AUTH-016` | On actor deprovisioning, session revocation and credential revocation (CPX-006) are parallel operations. Deprovisioning is not acknowledged until both complete. | +| `AUTH-017` | **SESSION-REGISTRY SLA (owner):** session revocation must propagate to the Session Revocation Registry within the profile-governed SLA: homelab PT5M, dev PT5M, standard PT1M, prod PT30S, fsi PT10S, sovereign PT5S. (Distinct from the credential CACHE-PROPAGATION TTL and PROVIDER-INVALIDATION SLA — credentials.md §5.) | +| `AUTH-018` | All DCM components that accept bearer tokens must check the Session Revocation Registry on each request. Cache age must not exceed the profile-governed maximum (sovereign: no cache). | +| `AUTH-019` | Emergency session revocation (security_event trigger) fires immediately with no grace period. The `auth.security_session_revoked` event has `urgency: critical` and is non-suppressable. | +| `AUTH-020` | The token introspection endpoint (`POST /api/v1/auth:introspect`) must be authenticated. Access requires an actor or service account with the `introspection` scope. | +| `AUTH-021` | When concurrent session limits are enforced, the oldest session is revoked before the new session is created. The evicted actor is notified via notification service if configured. | +| `AUTH-022` | Refresh tokens are invalidated when their parent session is revoked. A revoked refresh token returns 401 on exchange; it cannot be used to create a new session. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/convergence-engine/dependency-orchestration.md b/architecture/convergence-engine/dependency-orchestration.md new file mode 100644 index 0000000..9503e54 --- /dev/null +++ b/architecture/convergence-engine/dependency-orchestration.md @@ -0,0 +1,348 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Dependency Orchestration +Established: 2026-05-26 +Maps to: udlm/lifecycle/request-dependency-graph.md +--- + +# Convergence Engine — Dependency Orchestration + +> **Implements contracts defined in UDLM**: +> [udlm/lifecycle/request-dependency-graph.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/request-dependency-graph.md). +> UDLM defines the Request Dependency Group structure, `wait_for` values, +> field injection mechanism, `PENDING_DEPENDENCY` state contract, failure +> handling propagation policy, group timeout contract, and the relationship +> to composite service definitions. DCM operationalizes the submission, +> parsing, dispatch orchestration, state lifecycle, failure execution, and +> consumer API. + +--- + +## 1. Request dependency graph submission and parsing + +Consumers submit ad-hoc cross-request ordering via the dependency group +endpoint: + +``` +POST /api/v1/request-groups + +{ + "group_handle": "three-tier-app-deploy", + "on_failure": "cancel_remaining", + "timeout": "PT2H", + "requests": [ + { "ref": "db", + "catalog_item_uuid": "", + "fields": { ... } }, + { "ref": "app", + "catalog_item_uuid": "", + "fields": { ... }, + "depends_on": [ + { "ref": "db", + "wait_for": "realized", + "inject_fields": [ + { "from_field": "realized_fields.primary_ip", + "to_field": "fields.db_host" } + ] + } + ] + }, + { "ref": "lb", + "catalog_item_uuid": "", + "fields": { ... }, + "depends_on": [ + { "ref": "app", "wait_for": "realized", + "inject_fields": [ ... ] } + ] + } + ] +} +``` + +### 1.1 Parsing and validation + +The Request Orchestrator parses the submission and: + +1. **Validates DAG-ness** — runs a topological sort; rejects with 422 if a + cycle is detected (`RDG-001`) +2. **Validates group size** — rejects with 422 if request count exceeds the + profile's max_group_size +3. **Validates nesting depth** — rejects with 422 if dependency chain depth + exceeds the profile's max_nesting_depth +4. **Validates field injection paths** — checks `from_field` paths against + the dependency's resource type spec; per profile, validation may be + advisory (warn), enforced (reject), or policy-gated (also pass through + a compliance-class Validation Policy) +5. **Allocates entity UUIDs** for each request (so the group response can + return entity_uuid immediately) +6. **Resolves local refs** to actual entity UUIDs in the dependency graph + +On valid submission, DCM writes the group record and returns 202 with the +full request list in `PENDING_DEPENDENCY` status. + +### 1.2 Quota enforcement at group submission + +`PENDING_DEPENDENCY` requests count against the consumer's quota immediately +(`RDG-004`). Resources are reserved at group submission, not at dispatch +time. This prevents a consumer from submitting a 50-request group and then +finding only 30 fit in their quota when they start dispatching. + +--- + +## 2. PENDING_DEPENDENCY state mechanics + +`PENDING_DEPENDENCY` is a new status DCM adds to the Intent State lifecycle: + +``` +ACKNOWLEDGED → PENDING_DEPENDENCY → [dependency met] → LAYERS_ASSEMBLED → ... → REALIZED +``` + +PENDING_DEPENDENCY requests: + +- Are visible via `GET /api/v1/requests?status=PENDING_DEPENDENCY` +- Are cancellable via `DELETE /api/v1/requests/{uuid}` +- Receive a `request.pending_dependency` event (urgency: info) +- Do NOT have independent timeouts — the group-level `timeout` governs + +The Request Orchestrator maintains an in-memory dependency graph per group +plus a persistent state in `request_dependency_groups` and +`request_dependencies` tables. On every dependency state change, the +orchestrator re-evaluates dependents: when a dependency reaches its +`wait_for` state, blocked dependents transition out of `PENDING_DEPENDENCY` +and enter the standard assembly pipeline. + +### 2.1 Field injection execution + +When a dependency reaches its `wait_for` state and dependents have +`inject_fields`: + +``` +Dependency realized → Realized State written + │ + ▼ Request Orchestrator looks up inject_fields declarations for dependents + │ For each injection: + │ Extract from_field path from Realized State + │ If extraction fails: per profile, warn (advisory) or fail dispatch (enforced) + │ Inject value at to_field path in dependent's fields + │ + ▼ Dependent request transitions out of PENDING_DEPENDENCY + │ Enters standard nine-step assembly with injected fields + │ + ▼ Injected values pass through Transformation policies normally (RDG-003) +``` + +Injected values are not exempt from policy evaluation. A Transformation +policy that modifies `db_host` will modify the injected value just as it +would a consumer-declared one. + +--- + +## 3. wait_for state evaluation + +DCM tracks four `wait_for` states: + +| Value | Triggered when | +|---|---| +| `acknowledged` | Dependency has entity_uuid (post-Request Orchestrator acknowledgment) | +| `approved` | Dependency has passed all approvals (post-approval tier evaluation) | +| `dispatched` | Dependency has been sent to its provider | +| `realized` | Dependency is fully realized (Realized State written, entity.realized event emitted) | + +The Request Orchestrator subscribes to the events that mark each +transition; when any dependency state matches a dependent's `wait_for`, +the orchestrator unblocks the dependent. + +`realized` is the most common and default. + +--- + +## 4. Failure handling execution + +UDLM defines two propagation policies: `cancel_remaining` and `continue`. +DCM executes: + +### 4.1 cancel_remaining + +``` +Request fails (provider error, recovery policy DISCARD_NO_REQUEUE, etc.) + │ + ▼ Orchestrator inspects the group's on_failure policy + │ on_failure: cancel_remaining + │ + ▼ For all requests in the group with status in + │ {PENDING_DEPENDENCY, ACKNOWLEDGED}: + │ Transition to CANCELLED + │ failure_reason: dependency_failed + │ Emit request.cancelled event + │ + ▼ For already-dispatched requests: follow standard cancellation model + │ (Section 3 in recovery-and-retry.md) + │ + ▼ Emit request.failed for the original failure + │ Emit request.group_failed for the group + │ + ▼ Group status → failed +``` + +### 4.2 continue + +The failed request is marked FAILED. Dependents that depended on it are also +marked FAILED with `failure_reason: dependency_failed`. Independent requests +in the group continue unaffected. The group transitions to `failed` once all +requests reach a terminal state. + +--- + +## 5. Group timeout enforcement + +The Request Orchestrator maintains a per-group timer. When `timeout` elapses +without all requests reaching a terminal state: + +``` +Group timeout reached + │ + ▼ For all non-terminal requests in the group: + │ Transition to FAILED + │ failure_reason: group_timeout + │ Emit request.failed event + │ + ▼ Group status → failed + │ Emit request.group_failed event +``` + +Group timeout is measured from group submission (not from first dispatch). +Individual requests do not have independent timeouts while in +PENDING_DEPENDENCY status — only the group timeout governs that window +(`RDG-005`). + +--- + +## 6. Consumer API endpoints + +### 6.1 Submit dependency group + +``` +POST /api/v1/request-groups +``` + +See Section 1 above for the request body schema. + +Response 202: +```json +{ + "group_uuid": "", + "group_handle": "three-tier-app-deploy", + "requests": [ + { "ref": "db", "request_uuid": "", "entity_uuid": "", "status": "ACKNOWLEDGED" }, + { "ref": "app", "request_uuid": "", "entity_uuid": "", "status": "PENDING_DEPENDENCY" }, + { "ref": "lb", "request_uuid": "", "entity_uuid": "", "status": "PENDING_DEPENDENCY" } + ], + "estimated_completion": "" +} +``` + +### 6.2 Add a request to an existing group + +``` +POST /api/v1/request-groups/{group_uuid}/members +{ + "request_uuid": "", + "depends_on": [ ... ] +} +``` + +A request may belong to at most one group (`RDG-006`); adding to a second +group returns 409 Conflict. + +### 6.3 Query group status + +``` +GET /api/v1/request-groups/{group_uuid} + +→ { + "group_uuid": "", + "group_handle": "three-tier-app-deploy", + "status": "in_progress | completed | failed | cancelled", + "requests": [ + { "request_uuid": "", "ref": "db", "status": "REALIZED" }, + { "request_uuid": "", "ref": "app", "status": "DISPATCHED" }, + { "request_uuid": "", "ref": "lb", "status": "PENDING_DEPENDENCY" } + ], + "created_at": "", + "timeout_at": "" + } +``` + +### 6.4 Cancel a group + +``` +DELETE /api/v1/request-groups/{group_uuid} + +# Cancels all PENDING_DEPENDENCY and ACKNOWLEDGED requests in the group. +# Already-dispatched requests follow the standard cancellation model. +Response 204 +``` + +--- + +## 7. New events + +| Event | Urgency | Trigger | +|---|---|---| +| `request.pending_dependency` | info | Request entered PENDING_DEPENDENCY | +| `request.dependency_met` | info | Dependency reached wait_for state; request proceeding | +| `request.group_completed` | medium | All requests in group reached terminal state | +| `request.group_failed` | high | Group failed or timed out | + +These events are added to the UDLM event catalog ( +[udlm/contracts/event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md)) +under the `request.*` domain. + +--- + +## 8. Profile-governed constraints + +| Profile | Max group size | Max timeout | Field injection validation | Max nesting depth | +|---|---|---|---|---| +| homelab | 100 | P30D | advisory | 3 | +| dev | 100 | P7D | advisory | 3 | +| standard | 50 | P3D | enforced | 3 | +| prod | 25 | P1D | enforced + audited | 3 | +| fsi | 10 | PT8H | enforced + audited + policy gated | 2 | +| sovereign | 5 | PT4H | enforced + audited + policy gated | 2 | + +`RDG-002` sets an absolute upper bound of 100 — profiles may set lower limits +but no profile may set higher. + +--- + +## 9. Relationship to composite service definitions + +Request dependency groups and composite service definitions solve overlapping +but distinct problems: + +| | Request Dependency Group | Composite Service Definition | +|---|---|---| +| Who declares | Consumer at request time | Platform team at catalog time | +| Reusable | No — ad hoc | Yes — catalog item | +| Type constraints | None — any resources | Defined by composite spec | +| Policy governance | Standard consumer request policies | Composite Service policies (CMP-*) | +| Field injection | Consumer-declared inject_fields | Composite handles internally | +| Use case | Ad-hoc deployment ordering | Standard composite service | + +When a standard composite service exists as a composite service definition, +consumers should use it. Request dependency groups are for deployments that +don't fit a predefined composite pattern. + +--- + +## 10. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `RDG-001-DCM` | DCM rejects circular dependency graphs at submission time (422) via topological sort validation | +| `RDG-002-DCM` | DCM enforces a profile-governed maximum group size with absolute upper bound of 100 | +| `RDG-003-DCM` | DCM applies all active Transformation policies to injected field values; injection does not bypass policy | +| `RDG-004-DCM` | DCM counts PENDING_DEPENDENCY requests against consumer quota at group submission time | +| `RDG-005-DCM` | DCM enforces group-level timeout from group submission; individual requests have no independent timeout while PENDING_DEPENDENCY | +| `RDG-006-DCM` | DCM enforces single-group membership; attempts to add a request to a second group return 409 Conflict | diff --git a/architecture/convergence-engine/overview.md b/architecture/convergence-engine/overview.md new file mode 100644 index 0000000..d229246 --- /dev/null +++ b/architecture/convergence-engine/overview.md @@ -0,0 +1,214 @@ +--- +Document Status: 📋 Draft — Initial Specification +Document Type: Architecture Reference — Convergence Engine +Established: 2026-05-26 +Maps to: UDLM four-states contract, capability discovery contract +--- + +# Convergence Engine — Overview + +> **Implements contracts defined in UDLM**: +> [udlm/foundations/four-states.md](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md), +> [udlm/contracts/provider-contract.md](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md), +> [udlm/contracts/event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md), +> [udlm/contracts/capability-discovery.md](https://github.com/croadfeldt/udlm/blob/main/contracts/capability-discovery.md). + +The convergence engine is the heart of DCM. It walks data through the four +UDLM states — **intent → requested → realized → discovered** — and +continuously reconciles realized state against intent. Everything else in +DCM exists to support this loop. + +--- + +## 1. What the engine does + +UDLM defines the four states and the allowed transitions between them. UDLM +does **not** prescribe how a realization drives those transitions. The DCM +convergence engine is one specific answer: + +1. **Accepts intent** (via the API gateway from any ingress: API, GitOps, + CLI, message bus, scheduled trigger). Writes an Intent State record. +2. **Assembles a Requested State** by running the nine-step assembly: + layer resolution → layer merge → policy evaluation → placement → score → + approval routing → Requested State persistence → dispatch preparation → + audit emission. +3. **Dispatches** to the selected provider with a scoped, short-lived + `dcm_interaction` credential. Waits for realization within configured + timeout. +4. **Persists Realized State** on provider callback. Updates the entity + lifecycle state, fires `entity.realized` events. +5. **Reconciles continuously** via the Discovery Service. Polls providers + on schedule, writes Discovered State, compares to Realized State, fires + drift events. The Policy Engine evaluates each drift through Recovery + Policies. + +The loop runs forever — entities continuously move toward their declared +intent until decommissioned. + +--- + +## 2. The engine's responsibilities + +| Responsibility | How DCM fulfills it | +|---|---| +| Walk an entity from intent to realized | Request Orchestrator drives the nine-step pipeline; Request Processor performs assembly | +| Evaluate policy at every transition | Policy Manager evaluates Validation / Transformation / Recovery / Orchestration Flow / Governance Matrix policies via OPA | +| Select a provider for placement | Placement Manager runs the six-step placement algorithm: sovereignty pre-filter → eligibility filter → capability filter → reserve query → scoring → tie-break | +| Dispatch with scoped credentials | API Gateway requests a `dcm_interaction` credential from the Credential Provider, scoped to the specific provider + entity + operation, valid for PT15M–PT1H per profile | +| React to provider events | Provider callbacks land at the Provider Callback API; the Request Orchestrator routes to Realized State persistence and event emission | +| Detect drift | Discovery Service polls per Resource Type Spec's `discovery_schedule`; Drift Detection compares Discovered to Realized field-by-field | +| Evaluate recovery on failure | Recovery Policies fire on declared triggers (timeout, cancellation failure, partial realization, compensation failure); evaluate via the same Policy Manager | +| Audit everything | Audit Service appends a record on every state transition, policy decision, credential issuance, and provider call; records are leaves of an RFC 9162 Merkle tree (signed tree heads) providing tamper evidence | + +--- + +## 3. Pipeline routing — how events flow + +DCM uses PostgreSQL's `LISTEN/NOTIFY` for pipeline routing in standard +deployments (Kafka added as an enhancement for high-throughput deployments). +Every state transition writes a row to `pipeline_events`; a trigger fires +`pg_notify`; subscribed services consume. + +``` +Consumer submits intent + │ POST /api/v1/requests + ▼ +API Gateway — authenticates, injects X-DCM-Tenant + │ + ▼ +Request Orchestrator — writes intent_records row, emits intent.acknowledged + │ LISTEN/NOTIFY + ▼ +Request Processor — assembles, writes requested_records row, emits requested.assembled + │ LISTEN/NOTIFY + ▼ +Policy Manager — evaluates policies, computes score, emits policy.evaluated + score.computed + │ LISTEN/NOTIFY + ▼ +Placement Manager — selects provider, emits placement.decided + │ LISTEN/NOTIFY + ▼ +Request Orchestrator — issues interaction credential, dispatches to provider + │ HTTP POST to provider's dispatch endpoint + ▼ +Provider — realizes the resource, calls back to /api/v1/instances/{id}/status + │ + ▼ +Request Orchestrator — writes Realized State, emits entity.realized + │ LISTEN/NOTIFY + ▼ +Audit Service — appends audit record as a Merkle-tree leaf + │ + ▼ +Discovery Service — runs scheduled discovery for the resource type + │ + ▼ +Drift Detection — compares Discovered to Realized, emits drift events if differ + │ LISTEN/NOTIFY + ▼ +Policy Manager — evaluates Recovery Policies, fires configured action +``` + +Every step also writes provenance, so the full chain is reconstructable from +the audit trail. + +--- + +## 4. Capability discovery and provider matching + +UDLM's +[capability-discovery.md](https://github.com/croadfeldt/udlm/blob/main/contracts/capability-discovery.md) +defines the unified provider model: a provider is an external system that +declares **capabilities** (`realize_resources`, `serve_data`, `authenticate`, +`federate`, `execute_workflows`), not a fixed type. + +DCM's provider registry implements this: + +- Each provider registration includes a `capabilities` block plus declared + `supported_resource_types`. +- The Placement Manager matches a request's resource_type and constraints + against providers whose declared `realize_resources` capability includes + that type. +- A provider declaring multiple capabilities (e.g., InfoBlox declaring both + `serve_data` for IP availability queries and `realize_resources` for + Network.IPAddress allocation) is matched separately for each capability. + +**DCM exposes `GET /api/v1/capabilities`** — the machine-readable advertisement +of what this DCM instance can do (lifecycle management, policy evaluation, +cost analysis, audit trail, placement decisions, drift detection, entity +lifecycle events, subscribe endpoints). External systems (FinOps tools, audit +tools, DAV, federation peers) query this endpoint to discover DCM's +capabilities before integrating. + +**Backward compatibility:** the legacy typed-provider names +(`service_provider`, `information_provider`, etc.) are retained as resolved +labels derived from declared capabilities. Existing registrations continue +to work. + +System policies: `DISC-001` through `DISC-005` (in +[udlm/contracts/capability-discovery.md](https://github.com/croadfeldt/udlm/blob/main/contracts/capability-discovery.md)) +govern capability advertisement authentication, tenant scoping, rate limiting, +and the advisory nature of needs_from_dcm matching. + +--- + +## 5. State semantics — what each state means inside DCM + +| State | What's in it | Where it lives in DCM | Who writes it | +|---|---|---|---| +| **Intent** | Consumer's raw declaration before any processing | `intent_records` table | Request Orchestrator on ingress | +| **Requested** | Assembled, policy-evaluated, placed payload | `requested_records` table | Request Processor after nine-step assembly | +| **Realized** | What the provider built, with provider-side fields | `realized_entities` table (versioned, `is_current` flag) | Request Orchestrator on provider callback | +| **Discovered** | What the provider currently reports | `discovered_records` table (ephemeral snapshots) | Discovery Service on scheduled poll | + +The **Realized State only changes via an authorized request that produces a +corresponding Requested State record** (UDLM invariant RSE-010). Drift detection, +discovery cycles, and lifecycle events do not write to the Realized Store — +they write to other domains and trigger policy evaluation that may produce a +new Requested State. + +--- + +## 6. Reading order for engine internals + +If you're operationalizing or extending the convergence engine, read in this +order: + +1. This overview. +2. [`policy-evaluation.md`](policy-evaluation.md) — how DCM evaluates the unified + governance matrix, hard/soft enforcement, caching, sovereignty zones. +3. [`scoring.md`](scoring.md) — the hybrid scoring model, signals, approval routing. +4. [`recovery-and-retry.md`](recovery-and-retry.md) — timeouts, cancellation, + orphan detection, recovery policy execution, compensation. +5. [`dependency-orchestration.md`](dependency-orchestration.md) — consumer + request dependency graphs, PENDING_DEPENDENCY lifecycle, field injection, + failure handling. + +For governance enforcement specifically: +- [`../governance-enforcement/accreditation-monitor.md`](../governance-enforcement/accreditation-monitor.md) +- [`../governance-enforcement/registry-enforcement.md`](../governance-enforcement/registry-enforcement.md) +- [`../governance-enforcement/contribution-pipeline.md`](../governance-enforcement/contribution-pipeline.md) + +For provider interaction: +- [`../credentials-and-auth/provider-callback.md`](../credentials-and-auth/provider-callback.md) +- [`../credentials-and-auth/credentials.md`](../credentials-and-auth/credentials.md) + +--- + +## 7. Design invariants (DCM-level) + +These hold across all profiles and deployments: + +- The nine-step assembly is the only path from Intent to Requested +- The convergence loop is the only path from Requested to Realized +- The Discovery Service is the only writer of Discovered State +- Every state transition emits an event to `pipeline_events` +- Every state transition produces an audit record +- Every provider call carries a scoped, short-lived interaction credential +- Recovery Policies fire on every closed-vocabulary trigger; the action is + evaluated through the same Policy Manager as any other policy + +These are realization invariants for **DCM specifically**. A peer realization +might choose a different routing mechanism (e.g., Kafka instead of +`LISTEN/NOTIFY`), a different assembly algorithm, or a different audit +storage — and remain UDLM-conformant as long as the wire contracts are honored. diff --git a/architecture/convergence-engine/policy-evaluation.md b/architecture/convergence-engine/policy-evaluation.md new file mode 100644 index 0000000..b989096 --- /dev/null +++ b/architecture/convergence-engine/policy-evaluation.md @@ -0,0 +1,278 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Governance Matrix Evaluator +Established: 2026-05-26 +Maps to: udlm/governance/governance-matrix.md +--- + +# Convergence Engine — Policy Evaluation (Governance Matrix Evaluator) + +> **Implements contracts defined in UDLM**: +> [udlm/governance/governance-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md). +> UDLM defines the unified governance matrix as the single enforcement point +> for cross-boundary decisions, the four matrix axes (subject / data / target / +> context), the decision vocabulary (ALLOW / DENY / ALLOW_WITH_CONDITIONS / +> STRIP_FIELD / REDACT / AUDIT_ONLY), the hard-vs-soft enforcement +> distinction, and field-level controls. DCM operationalizes the evaluation +> algorithm, the caching, the sovereignty zone management, and the integration +> with the convergence pipeline. + +> **Manifest note**: this file is THE matrix evaluator. The DCM split +> manifest had a duplication bug that listed file 14-policy-profiles under +> both `convergence-engine/policy-evaluation.md` and +> `governance-enforcement/policy-profiles.md`. Resolution: file 14 lives at +> `governance-enforcement/policy-profiles.md` (it's pure-dcm and already +> moved). This file holds only the DCM matrix evaluator content from #10 +> (27-governance-matrix). The redundant `governance-enforcement/matrix-evaluator.md` +> was not created — see report. + +--- + +## 1. Evaluation algorithm + +The Policy Manager evaluates the governance matrix at every interaction +boundary: provider dispatch, federation tunnel data transmission, +notification delivery, registration acceptance, and any cross-boundary +capability invocation. The evaluation runs the same algorithm against the +same rule set on every interaction. No parallel enforcement paths exist. + +``` +Interaction attempt arrives at boundary: + subject = { type, identity, accreditation_level, tenant, + capabilities, capability_categories } # provider-as-subject matched by capability/category (ADR-PROV-002) + data = { classification, resource_type, field_paths, capability } # capability here = the INVOKED capability this interaction + target = { identity, sovereignty_zone, accreditation_held, trust_posture, + capabilities, capability_categories } # REPLACES target.type: a provider is matched by its declared + # capabilities (verb × domain) and the NON-EXCLUSIVE categories they + # form (e.g. realize_resources/Storage), never a provider type. A + # resolved provider_type label, if present, is derived + non-authoritative. + context = { profile, zero_trust_posture, federated, cross_jurisdiction, ... } + +Step 1: Collect matching rules + Load all active governance matrix rules across all tiers (system, platform, + tenant, resource_type, entity) + For each rule: evaluate the four axes against the interaction + Result: set of matching rules with decisions and enforcement levels + +Step 2: Evaluate hard constraints first + For each hard DENY rule that matches → DENY immediately; record rule_uuid; + no further evaluation + For each hard ALLOW rule that matches → record as hard allow candidate + If hard DENY exists → terminal decision = DENY + +Step 3: Evaluate soft constraints by domain precedence + Sort matching soft rules: entity > resource_type > tenant > platform > system + At each precedence level, most restrictive wins: + DENY > STRIP_FIELD > REDACT > ALLOW_WITH_CONDITIONS > AUDIT_ONLY > ALLOW + If DENY at any level → terminal decision = DENY + +Step 4: Evaluate conditions for ALLOW_WITH_CONDITIONS + For each ALLOW_WITH_CONDITIONS rule that survived Steps 2-3: + Evaluate all declared conditions + If any condition fails → downgrade to DENY + If all conditions pass → decision remains ALLOW_WITH_CONDITIONS + +Step 5: Apply field permissions + If terminal decision is ALLOW or ALLOW_WITH_CONDITIONS: + Apply field_permissions per governing rule: + allowlist mode: strip all fields not in allowed list + blocklist mode: strip all fields in blocked list + passthrough mode: all fields pass + For each stripped field: + If field is required → escalate to DENY_REQUEST + If field is optional → STRIP_FIELD (proceed without it) + +Step 6: Produce audit record + Record: interaction_uuid, all matching rules, terminal decision, + fields stripped or redacted, governing rule_uuid + Notification: if terminal decision is in rule's notification_on list + +Step 7: Enforce decision + ALLOW / ALLOW_WITH_CONDITIONS → interaction proceeds with permitted fields + DENY → interaction blocked; 403 response with governance_matrix_rule_uuid + STRIP_FIELD → interaction proceeds with stripped payload + REDACT → interaction proceeds with redacted field values + AUDIT_ONLY → interaction proceeds; flagged audit record written +``` + +### 1.1 Capability admission sources matrix rules (ADR-PROV-003) + +A platform admin's disposition of a provider's *declared* capabilities (`capability_admissions` in the DCM-assigned registration verdict) is authored upstream and enforced **here** — the admission record **sources** Governance-Matrix rules; there is no second "admission matrix" (single enforcement surface, §7). Now that `capability` and `capability_category` are target-axis match sources (§1), each disposition maps onto the existing decision vocabulary: + +Two levels, separated by concern: + +- **Platform-level admission (admin, coarse):** `approved` → an `ALLOW` rule matching `target.provider_uuid` + `target.capability_category`; `provisional` → `AUDIT_ONLY` (shadow — exercised but non-binding) until promoted (the same shadow mechanism as PROF-007 elevation); `denied` → a `DENY` rule for that provider + capability category. **Default-deny:** absent an `approved`/`provisional` admission, no rule ALLOWs the capability — a provider is unusable until admitted. +- **Granular approval (policy, conditional):** per-tenant / zone / resource-type / context narrowing is authored as *additional policy rules* (`ALLOW_WITH_CONDITIONS`) — the `matrix-permitted` term. This is the **policy** layer that refines an admitted capability further; it is not an admin-disposition field (ADR-PROV-003). Domain granularity needs no rule — a category *is* verb × domain. + +DCM enforces only the **intersecting ceiling** `effective_capabilities = declared ∩ admitted ∩ registry-enabled ∩ Governance-Matrix-permitted` (mirrors `effective_accepts_roles`) — a rule may only *narrow*, never widen, what was declared and admitted. No new evaluation path is added. + +--- + +## 2. Hard enforcement mechanics + +Hard rules (UDLM contract: `enforcement: hard`) cannot be relaxed by any +downstream rule at any domain level. DCM enforces this by: + +- At evaluation time, **hard DENY short-circuits** the entire rule set — + Steps 3-5 are skipped +- **No tenant-level, entity-level, or operator override** can permit an + interaction blocked by a hard DENY; the override is rejected at policy + contribution time by the Governance Matrix evaluator itself (the meta-rule + that consumer/tenant policies cannot modify system-domain hard rules) +- **Hard ALLOW** is rare and explicitly tracked; auditors can query for all + hard ALLOW rules to confirm none have been added inadvertently + +DCM ships pre-configured hard DENY rules for: + +- `sovereign` and `classified` data classifications crossing any boundary in + any profile (GMX-004) +- PHI without HIPAA BAA at federation boundaries (HIPAA compliance domain) +- Cross-jurisdiction transfer of restricted data in `fsi` profile + +These hard rules are activated automatically by the active profile and +compliance domain. The set is documented in the profile activation manifest. + +--- + +## 3. Soft enforcement execution + +Soft rules establish defaults that downstream (more specific) rules can +tighten — but never relax. DCM enforces this by: + +- Sorting matching rules by domain precedence: entity > resource_type > + tenant > platform > system +- At each level, computing the most restrictive decision across all matching + rules at that level (`DENY > STRIP_FIELD > REDACT > ALLOW_WITH_CONDITIONS + > AUDIT_ONLY > ALLOW`) +- Walking precedence levels from most-specific to least-specific; **a more + restrictive decision at any level wins**; a less restrictive decision at a + more specific level is rejected at policy contribution time (the rule never + becomes active) + +A soft DENY at the system level cannot be relaxed to ALLOW by a +tenant-level rule. The Governance Matrix evaluator detects the attempted +relaxation and rejects the policy contribution at submission time with +`GMX_SOFT_DENY_CANNOT_BE_RELAXED`. + +--- + +## 4. Caching and invalidation + +Policy evaluation is on the hot path for every interaction. DCM caches: + +- **Compiled rule sets** — the active matrix rule set is compiled to an + in-memory evaluation tree per Policy Manager instance; recompiled on rule + change events (`policy.activated`, `policy.deactivated`, + `policy.deprecated`) +- **Per-actor permission cache** — for a session, the actor's role/tenant + scope and matching subject-axis rules are cached for the session TTL + (PT15M–PT8H per profile) +- **Per-provider accreditation cache** — accreditation state for target + evaluations cached for `accreditation_cache_ttl` per profile (PT5M + standard, PT1M fsi/sovereign) + +**Invalidation triggers:** + +- Policy activation/deactivation event → recompile rule set; emit + `policy.cache_invalidated` to all Policy Manager instances +- Actor session revocation (via SES-001 model) → drop actor's permission cache +- Accreditation status change (from Accreditation Monitor) → drop provider's + accreditation cache entry +- Credential revocation event → permission cache entries referencing the + credential are dropped + +Cache invalidation propagates via PostgreSQL `LISTEN/NOTIFY` and completes +within PT5S in standard deployments. In `fsi`/`sovereign` profiles, +cache TTL is shortened to PT1M (or per-call evaluation in `sovereign`) to +limit stale-cache risk. + +--- + +## 5. Sovereignty zone management + +Sovereignty zones are first-class UDLM artifacts ( +[udlm/governance/governance-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md)). +DCM operationalizes them via: + +- **Zone registry** — sovereignty zones stored as DCM artifacts with handle, + jurisdictions, regulatory frameworks, data residency guarantee, inter-zone + agreements +- **Resolution layer** — at policy evaluation, target sovereignty zone is + resolved from the target's registration (provider sovereignty declaration, + peer DCM zone, etc.) +- **Hard rule for sovereign data** — sovereign-classified data carries a + hard DENY for all federation and external-provider interactions in all + profiles including homelab; the rule is shipped pre-activated and cannot + be modified by tenant or platform policies + +### 5.1 Zone evaluation in placement + +When the Placement Manager runs Step 1 (sovereignty pre-filter), it queries +the sovereignty zone of every eligible provider and eliminates any provider +whose zone is not in the request's permitted zones list. This is a +pre-filter, not a tie-breaker — sovereignty failures eliminate providers +from the placement loop entirely. + +### 5.2 Inter-zone agreements + +A sovereignty zone may declare `inter_zone_agreements` — explicit data +transfer agreements with other zones (e.g., EU adequacy decision for +transfers within EU member states). The matrix evaluator consults +`inter_zone_agreements` when evaluating cross-zone interactions; transfers +permitted by an agreement are evaluated at the agreement's permitted +classification cap. + +--- + +## 6. Profile-governed policy configurations + +DCM ships pre-configured rule sets per profile. The rule set is activated +automatically when the profile is set: + +| Profile | Rule set characteristics | +|---|---| +| `homelab` | Hard DENY only for sovereign/classified; soft ALLOW for public/internal; permissive | +| `dev` | Inherits minimal; adds soft ALLOW_WITH_CONDITIONS for confidential to verified targets | +| `standard` | Inherits minimal; adds soft ALLOW_WITH_CONDITIONS for restricted (third_party accreditation required); soft DENY for PHI without HIPAA | +| `prod` | Inherits standard; tightens federation to verified peers only for confidential+; STRIP_FIELD for restricted in notifications | +| `fsi` | Inherits prod; hard DENY for cross-jurisdiction with regulated data; hard ALLOW_WITH_CONDITIONS for PHI requiring HIPAA BAA + verified + ZT full | +| `sovereign` | Inherits fsi; hard DENY for any sensitive data crossing DCM federation; hardware attestation required for any federation | + +When a compliance domain is active (HIPAA, GDPR, PCI-DSS, FedRAMP), its +matrix rules are automatically added to the active rule set. They compose +with profile rules — they do not replace them. + +See [`../governance-enforcement/policy-profiles.md`](../governance-enforcement/policy-profiles.md) +for the complete profile definitions. + +--- + +## 7. Where the evaluator runs + +The Policy Manager service hosts the matrix evaluator. It is invoked from: + +| Call site | When | +|---|---| +| Request Processor (during assembly) | Step 5 of nine-step assembly — evaluates all Validation + Transformation + Governance Matrix rules against the assembled payload | +| Provider Dispatcher | Before provider dispatch — evaluates outbound governance matrix against the dispatch payload + target provider | +| Federation Tunnel | Before every cross-DCM message — evaluates outbound governance matrix against the message + remote DCM peer | +| Notification Router | Before every notification delivery — evaluates outbound governance matrix against the notification payload + destination | +| Webhook Subscription Resolver | When a webhook subscription is established — evaluates the matrix on the subscriber's authority for the subscribed event domain | +| Contribution Submission | When any contributor submits an artifact — evaluates contributor-permission matrix rules (see [`../governance-enforcement/contribution-pipeline.md`](../governance-enforcement/contribution-pipeline.md)) | + +Every invocation produces an audit record (GMX-005), regardless of outcome. + +--- + +## 8. Realization-specific notes + +- **OPA as the evaluation engine.** DCM uses OPA (Open Policy Agent) to + evaluate matrix rules; rules are translated to Rego at compile time. A peer + DCM realization could use a different engine while remaining UDLM-conformant. +- **PostgreSQL as the rule store.** Active rules live in the `policies` + table with status `active` and tier metadata. A peer could use a different + store. +- **`LISTEN/NOTIFY` for invalidation propagation.** A peer could use Kafka + or any other pub/sub. + +These are DCM implementation choices, not UDLM contracts. diff --git a/architecture/convergence-engine/recovery-and-retry.md b/architecture/convergence-engine/recovery-and-retry.md new file mode 100644 index 0000000..abd0e41 --- /dev/null +++ b/architecture/convergence-engine/recovery-and-retry.md @@ -0,0 +1,453 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Recovery and Retry +Established: 2026-05-26 +Maps to: udlm/lifecycle/operational-models.md +--- + +# Convergence Engine — Recovery and Retry + +> **Implements contracts defined in UDLM**: +> [udlm/lifecycle/operational-models.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/operational-models.md). +> UDLM defines the timeout model and state machine, the cancellation +> propagation contract, the orphan-detection contract, the discovery +> scheduling contract, the recovery policy model, and the compensation +> contract. DCM operationalizes the timeout enforcement, cancellation +> execution, orphan detection, discovery job scheduling, recovery policy +> evaluation, and compensation execution. + +--- + +## 1. Timeout enforcement mechanisms + +UDLM defines three independent timeout scopes (assembly, dispatch, +reserve_query). DCM enforces each with per-step deadlines and dedicated +recovery triggers. + +### 1.1 Assembly timeout enforcement + +The Request Processor runs the nine-step assembly inside a budget governed +by `assembly_timeout` (profile-governed; see table below). The total budget +is allocated across steps as proportional fractions: + +| Step | Fraction of assembly_timeout | +|------|----------------------------| +| Layer Resolution | 20% | +| Layer Merge | 10% | +| Policy Evaluation (each) | 15% total; 5% per Mode 1/2; 30s per Internal policy; PT2M per External evaluator | +| Placement Engine Loop | 40% | +| Requested State Persistence | 10% | + +DCM enforces sub-step deadlines via `context.WithDeadline` (Go) or equivalent +in the Request Processor. A step exceeding its sub-deadline immediately fires +`ASSEMBLY_TIMEOUT` recovery trigger regardless of overall budget remaining. + +A external policy evaluation that exceeds PT2M per query causes +`ASSEMBLY_TIMEOUT` even if the overall assembly budget would tolerate it — +this prevents a single slow External Policy Evaluator from consuming the +entire assembly budget. + +| Profile | assembly_timeout default | +|---|---| +| homelab | PT5M | +| dev | PT5M | +| standard | PT3M | +| prod | PT2M | +| fsi | PT2M | +| sovereign | PT2M | + +### 1.2 Dispatch timeout enforcement + +The Request Orchestrator starts a dispatch deadline timer when it sends the +dispatch payload to the provider. If the provider has not posted a final +Realized State callback by the deadline, `DISPATCH_TIMEOUT` fires. + +| Profile | dispatch_timeout default | +|---|---| +| homelab | PT2H | +| dev | PT1H | +| standard | PT1H | +| prod | PT30M | +| fsi | PT30M | +| sovereign | PT30M | + +Resource-type overrides extend the timeout for legitimately long-running +provisioning (Compute.BareMetalServer: PT4H, Storage.LargeVolume: PT2H). +Overrides are declared in the Resource Type Specification. + +### 1.3 Reserve query timeout enforcement + +The Placement Manager queries each eligible provider's reserve endpoint in +parallel during the placement loop, with a short per-call deadline: + +| Profile | reserve_query_timeout default | +|---|---| +| homelab | PT30S | +| dev | PT30S | +| standard | PT10S | +| prod | PT5S | +| fsi | PT5S | +| sovereign | PT10S | + +A reserve query timeout does NOT immediately fire a recovery trigger — the +Placement Manager skips the timed-out provider and continues the placement +loop with remaining candidates. Only when ALL candidates have timed out or +been rejected does `RESERVE_QUERY_ALL_EXHAUSTED` fire. + +### 1.4 Timeout audit records + +Every timeout writes an audit record: + +```yaml +audit_record: + action: ASSEMBLY_TIMEOUT | DISPATCH_TIMEOUT | RESERVE_QUERY_TIMEOUT | + RESERVE_QUERY_ALL_EXHAUSTED + actor: + type: system + system_actor: + component: request_processor | request_orchestrator | placement_manager + trigger: timeout + entity_uuid: + details: + timeout_duration: + actual_elapsed: + step_at_timeout: + recovery_policy_triggered: +``` + +--- + +## 2. Cancellation execution and cleanup + +UDLM defines the three cancellation scenarios (pre-dispatch, post-dispatch +not-yet-started, mid-execution) and the propagation model. DCM enforces: + +### 2.1 Pre-dispatch cancellation (Scenario 1) + +Consumer submits `DELETE /api/v1/requests/{uuid}` while entity is in +pre-DISPATCHED state. DCM: + +1. Marks the Intent State record CANCELLED +2. Halts assembly immediately (cancels the in-flight Request Processor context) +3. Transitions entity to CANCELLED (terminal) +4. Writes `REQUEST_CANCELLED` audit record +5. No recovery policy triggered (clean cancel) +6. Returns `200 OK` with `{ "status": "CANCELLED" }` + +### 2.2 Post-dispatch, provider not yet started (Scenario 2) + +DCM sends cancellation payload to the provider's declared `cancellation_endpoint` +(if provider declares `supports_cancellation: true`). The provider acknowledges; +DCM transitions entity to CANCELLED. Response is `202 Accepted` while DCM +awaits provider confirmation; consumer polls status for the final CANCELLED. + +### 2.3 Mid-execution cancellation (Scenario 3) + +DCM consults `provider.supports_cancellation` and `provider.cancellation_supported_during`: + +- If provider supports cancellation during PROVISIONING: DCM sends the cancellation + payload; provider attempts rollback. Outcomes: + - Rollback clean → entity → CANCELLED (terminal) + - Rollback partial → fires `CANCELLATION_FAILED` recovery trigger + - No response → fires `CANCELLATION_FAILED` recovery trigger +- If provider does NOT support cancellation: entity enters `CANCEL_PENDING`; + DCM waits for provider to complete normally; on completion, fires + `LATE_RESPONSE_RECEIVED` (action typically `DISCARD_AND_REQUEUE` in + cancellation context) + +### 2.4 Cancellation payload + +```json +{ + "cancellation_uuid": "", + "entity_uuid": "", + "requested_state_uuid": "", + "reason": "consumer_requested | timeout | policy_triggered", + "requested_at": "", + "best_effort": true +} +``` + +`best_effort: true` is always set — DCM never guarantees cancellation success. + +--- + +## 3. Orphan detection implementation + +When cleanup cannot be guaranteed, DCM runs an orphan detection pass to find +provider resources with no corresponding DCM Realized State record. + +### 3.1 Triggers + +DCM fires orphan detection on: + +- Dispatch timeout with cancellation sent +- Cancellation failed +- Compensation failed +- `DISCARD_NO_REQUEUE` action taken +- Manual platform admin trigger + +### 3.2 Query implementation + +The Orphan Detection Service queries the provider's discovery endpoint with +narrow criteria: + +```yaml +orphan_detection_query: + provider_uuid: + time_window: + from: + to: + match_criteria: + resource_type: + characteristics: + name_pattern: + size_class: + tags: + exclude: + known_realized_state_uuids: [] +``` + +DCM compares discovery results against known Realized State entities; +unmatched provider-side entities are flagged as orphan candidates. + +### 3.3 Orphan candidate lifecycle + +Orphan candidates enter the platform admin review queue: + +```yaml +orphan_candidate: + orphan_candidate_uuid: + suspected_request_uuid: + provider_entity_id: + provider_uuid: + discovered_at: + characteristics: { ... } + status: under_review | confirmed_orphan | adopted | false_positive + resolution: + action: manual_decommission | adopt_into_dcm | mark_false_positive + resolved_by: + resolved_at: +``` + +Orphan candidates surface in the Platform Admin dashboard and generate a +notification with `urgency: high`. + +--- + +## 4. Discovery job scheduling and execution + +The Discovery Scheduler maintains a priority queue and dispatches discovery +jobs to provider discovery endpoints. UDLM defines the three trigger types +(scheduled / event / on-demand); DCM implements the queue and dispatcher. + +### 4.1 Priority queue + +``` +Priority order: + 1. Critical — COMPENSATION_FAILED orphan detection, sovereignty violation + 2. High — on-demand from platform admin, event-triggered (provider.degraded) + 3. Standard — event-triggered (entity.realized, drift.resolved) + 4. Background — scheduled discovery passes +``` + +Queue depth is bounded per profile. When the queue is full, new Background +items are dropped (with a log entry). Standard and above are never dropped — +they wait. + +### 4.2 Scheduled discovery + +Resource Type Specifications declare `discovery_schedule.default_interval`, +overridable per profile. The Discovery Scheduler runs each schedule via cron +(LISTEN/NOTIFY-based timer + work-stealing across Discovery Service replicas +for HA). + +### 4.3 Event-triggered discovery + +Specific DCM events automatically enqueue an out-of-cycle discovery: + +| Event | Delay | Scope | Reason | +|---|---|---|---| +| `entity.realized` | PT30S | this_entity | Confirm realization matches Requested State | +| `drift.resolved` | PT60S | this_entity | Confirm remediation took effect | +| `provider_update.approved` | PT30S | this_entity | Confirm provider update reflected | +| `provider.degraded` | PT0S | all_entities_on_provider | Assess impact | +| `TIMEOUT_PENDING` | PT5M | this_entity | Orphan detection after timeout | +| `COMPENSATION_FAILED` | PT0S | this_entity_and_dependents | Find orphans | + +### 4.4 On-demand discovery API + +``` +POST /api/v1/admin/discovery:trigger +{ + "scope": "entity | resource_type | provider | tenant", + "entity_uuid": "", + "resource_type": "", + "provider_uuid": "", + "tenant_uuid": "", + "reason": "incident investigation", + "priority": "high" +} +``` + +### 4.5 Discovery audit + +Every discovery cycle writes an audit record: + +```yaml +audit_record: + action: DISCOVERY_CYCLE_COMPLETED | DISCOVERY_CYCLE_FAILED + actor: + type: system + system_actor: + component: discovery_scheduler + trigger: scheduled | event_triggered | on_demand + trigger_event_uuid: + entity_uuid: + details: + entities_discovered: 47 + new_entities_found: 2 + duration: PT8S +``` + +--- + +## 5. Recovery policy evaluation + +UDLM defines Recovery Policies as a formal policy type alongside Validation +and Transformation. DCM evaluates Recovery Policies via the same +Policy Manager. The evaluation precedence is the same as all other policies: + +``` +1. Resource-type-level override (most specific) +2. Tenant-level override +3. Active profile's recovery posture group +4. System default (recovery-automated-reconciliation) + +First matching policy for the trigger condition wins. +Multiple recovery policies for the same trigger at the same domain level +→ policy conflict; CONFLICT_ERROR at ingestion; platform admin notified. +``` + +### 5.1 Built-in recovery profile groups + +DCM ships four built-in recovery profile groups (declared as Policy Groups +with `concern_type: recovery_posture`): + +| Group | Posture | +|---|---| +| `recovery-automated-reconciliation` | Let drift detection converge; default for homelab/dev/standard | +| `recovery-discard-and-requeue` | On ambiguity, clean up and start fresh — prioritize consistency | +| `recovery-notify-and-wait` | Never act automatically — always notify and wait for human; default for prod/fsi/sovereign | +| `recovery-aggressive-retry` | Retry everything before giving up | + +Profile bindings: + +```yaml +profile_recovery_defaults: + minimal: recovery-automated-reconciliation + dev: recovery-automated-reconciliation + standard: recovery-automated-reconciliation + prod: recovery-notify-and-wait + fsi: recovery-notify-and-wait + sovereign: recovery-notify-and-wait +``` + +Tenant and resource-type overrides are permitted; resource-type overrides +the most specific. + +### 5.2 NOTIFY_AND_WAIT consumer interface + +When a recovery policy fires `NOTIFY_AND_WAIT`, DCM sends a notification to +the entity owner with a time-bounded decision interface: + +``` +GET /api/v1/resources/{entity_uuid}/recovery-decisions +→ { + "recovery_decision_uuid": "", + "trigger": "DISPATCH_TIMEOUT", + "entity_uuid": "", + "deadline": "", + "available_actions": [ + { "action": "DRIFT_RECONCILE", "description": "..." }, + { "action": "DISCARD_AND_REQUEUE", "description": "..." }, + { "action": "DISCARD_NO_REQUEUE", "description": "..." } + ] + } + +POST /api/v1/resources/{entity_uuid}/recovery-decisions/{recovery_decision_uuid} +{ + "action": "DISCARD_AND_REQUEUE", + "reason": "Provider was known to be degraded at time of timeout" +} +``` + +If the deadline passes without resolution, the configured +`on_deadline_exceeded` action fires automatically. + +--- + +## 6. Compensation execution + +UDLM defines the composite service compensation contract (reverse-dependency +ordering, declarative per-component compensation behavior). DCM executes: + +### 6.1 Reverse-order execution + +When a composite service partially fails: + +``` +Successful so far: vm ✓, ip ✓ +Failed: dns ✗ (atomic) + +Compensation triggered: + Step 1: decommission vm (compensation_order: 3 → runs first in reverse) + Step 2: release ip allocation (compensation_order: 1 → runs second in reverse) + Compound entity → FAILED (terminal for this request cycle) +``` + +The Composite Service Orchestrator maintains the dependency graph from the +composite service spec and walks it in reverse for compensation. + +### 6.2 Compensation failure + +If a compensation step itself fails: + +1. Entity enters `COMPENSATION_FAILED` state +2. `COMPENSATION_FAILED` recovery trigger fires (default action: `ESCALATE`) +3. Orphan detection triggered immediately, scoped to provider + entity + characteristics +4. ORPHAN_CANDIDATE record created +5. Platform admin notified + +--- + +## 7. New lifecycle states (DCM-internal) + +DCM adds five lifecycle states to operationalize the UDLM recovery contracts: + +| State | Meaning | Recovery trigger | +|---|---|---| +| `TIMEOUT_PENDING` | Dispatch timeout fired; cancellation sent; awaiting outcome | `DISPATCH_TIMEOUT` | +| `LATE_REALIZATION_PENDING` | Provider responded after timeout; NOTIFY_AND_WAIT active | `LATE_RESPONSE_RECEIVED` | +| `INDETERMINATE_REALIZATION` | State ambiguous; drift detection resolving | — | +| `COMPENSATION_IN_PROGRESS` | Composite service rollback underway | — | +| `COMPENSATION_FAILED` | Rollback itself failed; orphaned resources possible | `COMPENSATION_FAILED` | + +These states are DCM-internal — they describe the recovery pipeline mechanics +and are mapped to UDLM lifecycle states (typically as sub-states of +PROVISIONING or FAILED) for external interop. + +--- + +## 8. Policy IDs + +DCM-side policy IDs governing recovery and retry execution: + +| Policy | Rule | +|---|---| +| `OPS-010-DCM` | DCM enforces assembly, dispatch, and reserve-query timeouts independently with profile-governed defaults and resource-type overrides | +| `OPS-011-DCM` | DCM cancellation is always best-effort; outcomes flow through Recovery Policy evaluation | +| `OPS-014-DCM` | DCM evaluates Recovery Policies via the same Policy Manager as all other policy types; same shadow mode, same audit | +| `OPS-017-DCM` | DCM runs composite compensation in reverse dependency order; compensation failure fires immediate orphan detection | +| `OPS-019-DCM` | DCM's NOTIFY_AND_WAIT actions carry a deadline; if exceeded, the configured `on_deadline_exceeded` action fires automatically | diff --git a/architecture/convergence-engine/scoring.md b/architecture/convergence-engine/scoring.md new file mode 100644 index 0000000..77fb721 --- /dev/null +++ b/architecture/convergence-engine/scoring.md @@ -0,0 +1,535 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — Scoring Model Specification +--- + +# DCM Data Model — Hybrid Scoring Model + +> **DCM-native scoring engine; no single UDLM contract counterpart.** +> The hybrid scoring model is a realization-layer extension of the Policy +> abstraction — UDLM defines no scoring contract. A peer DCM realization could +> use a different signal-weighting scheme and still satisfy every UDLM Policy +> and Governance Matrix contract. The Governance Matrix remains a pure boolean +> gate; scoring never applies to cross-boundary data decisions. + + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference — Scoring Model Specification +**Related Documents:** [Foundational Abstractions](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md) | [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) | [Policy Profiles](../governance-enforcement/policy-profiles.md) | [Control Plane Components](../control-plane/components.md) | [Governance Matrix](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md) | [Federated Contribution Model](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md) + +> **This document maps to: DATA + POLICY** +> +> The Scoring Model is an extension of the Policy abstraction. Scored signals are Data artifacts with lifecycle and provenance. Profile thresholds are Policy-governed configuration. The Governance Matrix remains a pure boolean gate — scoring never applies to cross-boundary data decisions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> > **See also:** [Authority Tier Model](https://github.com/croadfeldt/udlm/blob/main/governance/authority-tier-model.md) — the ordered authority tier list, custom tier definition, dynamic threshold format, and ATM system policies. + +> **Design Priority:** The Scoring Model is the primary mechanism for Priority 2 (ease of use) in service of Priority 1 (security). The auto-approval threshold (SMX-008: ≤ 50) and compliance-class Validation Policies are non-negotiable security properties. Profile thresholds and signal weights are the ease-of-use scaling mechanism. See [Design Priorities](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md). + +--- + +## 1. Purpose and Governing Principle + +DCM uses a **hybrid scoring model**: some decisions are boolean gates (facts), others are scored signals (degrees). The governing principle is explicit: + +> **Questions of fact use boolean gates. Questions of degree use scoring.** + +A secondary test for any ambiguous decision: +> **Can a regulator accept "the score was below threshold" as a complete explanation? If not, the decision must be boolean.** + +This document specifies the scoring half of the hybrid. For boolean decisions, see [Governance Matrix](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md) and the compliance enforcement model in [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md). + +### 1.1 What This Model Does + +The scoring model adds three capabilities to the existing architecture: + +1. **Operational Validation policies** contribute a weighted risk score instead of producing a binary deny. The aggregate score drives approval routing. +2. **Advisory Validation policies** produce a completeness score and warning list without blocking the request. +3. **Five scoring signals** aggregate into a request risk score that determines approval routing tier — replacing the current per-policy approval flag with a continuous, profile-governed threshold system. + +### 1.2 What This Model Does Not Do + +The scoring model does **not**: +- Apply to Governance Matrix decisions — these remain boolean always +- Apply to compliance-class Validation policies — PHI→BAA, sovereign data→sovereign provider remain hard gates +- Apply to authentication, authorization, or five-check boundary enforcement +- Apply to lifecycle state transitions +- Replace the Policy Engine — it is a function within it + +--- + +## 2. Validation Policy Enforcement Classes + +Every Validation policy declares an `enforcement_class`. This is a required field in the Policy base contract (added in this document). + +```yaml +enforcement_class: compliance | operational +``` + +### 2.1 Compliance Class + +Behavior: **boolean gate**. A compliance-class Validation Policy that fires produces a `deny` decision. The request is halted immediately. No score is produced. + +**Use for:** +- Data classification boundary rules (PHI requires BAA accreditation) +- Sovereignty violations (classified data leaving declared zone) +- Security hard requirements (unencrypted data, expired certificates) +- Regulatory mandates with no legitimate override path +- Any rule where "score-around" creates legal or compliance liability + +```yaml +# Example compliance-class Validation Policy +policy_type: validation +enforcement_class: compliance +handle: "system/compliance/phi-baa-required" +match: + payload_type: request.layers_assembled + conditions: + - field: payload.data_classification + operator: contains + value: phi + - field: payload.provider.accreditations + operator: not_contains + value: baa_active +output: + decision: deny + reason: "PHI data requires provider with active BAA. Provider has no active BAA." + audit_required: true + notify_on: [DENY] +``` + +### 2.2 Operational Class + +Behavior: **risk score contribution**. An operational-class Validation Policy that fires contributes a weighted score to the request risk score. The request is not immediately halted. Instead the aggregate score determines routing. + +**Use for:** +- Cost ceiling policies (request cost exceeds Tenant recommendation) +- Resource sizing policies (CPU/memory above recommended maximums) +- Unusual timing or context (off-hours request, unusual field combinations) +- Quota pressure (Tenant approaching quota limit) +- Business rule preferences that should escalate review, not block + +```yaml +# Example operational-class Validation Policy +policy_type: validation +enforcement_class: operational +handle: "tenant/payments/gating/cost-ceiling" +scoring_weight: 35 # contribution to request risk score when fired +match: + payload_type: request.layers_assembled + conditions: + - field: payload.cost_estimate.per_month + operator: gt + value: 500 +output: + risk_score_contribution: 35 + reason: "Estimated monthly cost ${{payload.cost_estimate.per_month}} exceeds Tenant ceiling $500" + label: "cost_ceiling_exceeded" + audit_required: true +``` + +### 2.3 Profile-Level Enforcement Class Override + +Profiles can override the enforcement class of individual policies. This is the mechanism for making the scoring system tunable without touching individual policies. + +```yaml +# In a profile definition: +policy_enforcement_overrides: + - policy_handle: "tenant/payments/gating/cost-ceiling" + override_enforcement_class: compliance # escalate to hard gate in this profile + rationale: "FSI profile: all cost violations are hard gates" + + - policy_handle: "system/security/off-hours-request" + override_enforcement_class: operational # demote to soft score in dev profile + rationale: "Dev profile: off-hours requests are expected; score but don't block" +``` + +**Hard constraint:** A profile can **only** override `operational → compliance` or `compliance → operational` for explicitly non-regulatory policies. Policies with `regulatory_mandate: true` in their metadata cannot be demoted to operational by any profile. + +--- + +## 3. Validation Output Classes + +Every Validation policy declares an `output_class`. This is a required field. + +```yaml +output_class: structural | advisory +``` + +### 3.1 Structural Class + +Behavior: **boolean pass/fail**. A structural Validation that fails halts the request. No score is produced. + +**Use for:** +- Required field presence (missing required fields) +- Type correctness (wrong field type) +- Referential integrity (UUID references that don't resolve) +- Format validation (malformed handle, invalid semver) +- Schema conformance + +### 3.2 Advisory Class + +Behavior: **completeness score contribution + warning list**. An advisory Validation that fires contributes to the completeness score and adds a warning to the advisory_warnings list. The request is not halted. + +**Use for:** +- Recommended fields absent (cost_center not provided) +- Unusual values (memory_gb at 1 for a database VM — unusual but not invalid) +- Low-confidence field values (field sourced from a provider with confidence < 0.5) +- Naming convention violations (non-compliant resource name — advisory only) + +```yaml +policy_type: validation +output_class: advisory +handle: "platform/advisory/cost-center-recommended" +scoring_weight: 10 +match: + payload_type: request.layers_assembled +output: + completeness_contribution: 10 + warning_code: "recommended_field_absent" + warning_message: "cost_center not provided — cost attribution will use Tenant default" + field: "fields.cost_center" +``` + +--- + +## 4. The Five Scoring Signals + +The request risk score is assembled from five independent signals. Each signal is normalized to 0–100. The aggregate is a weighted sum, also normalized to 0–100. + +### 4.1 Signal 1 — Operational Validation Policy Score + +**Source:** All operational-class Validation policies that fired during policy evaluation. +**Composition:** Sum of `risk_score_contribution` values from all fired operational Validation Policies. +**Normalization:** Capped at 100 before weighting. Multiple Validation Policies can fire; their contributions accumulate. +**Default weight in aggregate:** 0.45 + +```yaml +operational_gating_score: + fired_policies: + - handle: "tenant/payments/gating/cost-ceiling" + contribution: 35 + reason: "Cost $620/month exceeds ceiling $500" + - handle: "platform/gating/off-hours" + contribution: 15 + reason: "Request submitted outside business hours" + raw_score: 50 # sum of contributions + normalized: 50 # already within 0-100 +``` + +### 4.2 Signal 2 — Policy Completeness Score + +**Source:** All advisory-class Validation policies that fired. +**Composition:** Sum of `completeness_contribution` values from all fired advisory Validations. +**Normalization:** Capped at 100. Score represents "how incomplete is this request" — higher = more warnings. +**Default weight in aggregate:** 0.15 + +### 4.3 Signal 3 — Actor Risk History Score + +**Source:** Decay-weighted history of the actor's previous request outcomes. +**Composition:** Each historical event has a base score contribution and a time-decay multiplier. +**Decay model:** `contribution × e^(-λt)` where `t` is days since event, `λ` = 0.1 (half-life ≈ 7 days). +**Normalization:** 0–100. A clean history = 0. Recent consecutive failures approach 100. +**Default weight in aggregate:** 0.20 + +```yaml +# Events that contribute to actor risk history score +actor_risk_events: + - event: validation_failure # base_contribution: 5 + - event: gating_deny # base_contribution: 10 + - event: compliance_deny # base_contribution: 20 + - event: policy_override_requested # base_contribution: 8 + - event: drift_caused # base_contribution: 15 + - event: decommission_forced # base_contribution: 12 + - event: request_abandoned # base_contribution: 3 +``` + +**Privacy constraint:** Actor risk history scores are never exposed in consumer-facing API responses beyond the actor's own history. They are available in the Admin API for platform admins and in the audit trail. + +### 4.4 Signal 4 — Tenant Quota Pressure Score + +**Source:** Current quota utilization for the resource type being requested. +**Composition:** `max(0, (utilization_pct - free_threshold) / (1 - free_threshold)) × 100` +**Free threshold:** 0.75 (quota pressure score = 0 below 75% utilization). +**At 100% utilization:** quota pressure = 100, but the hard quota gate also fires (blocking the request regardless of score). +**Default weight in aggregate:** 0.10 + +```yaml +quota_pressure_score: + resource_type: "Compute.VirtualMachine" + current_usage: 87 + limit: 100 + utilization_pct: 0.87 + free_threshold: 0.75 + score: 48 # (0.87 - 0.75) / (1 - 0.75) × 100 = 48 +``` + +### 4.5 Signal 5 — Provider Accreditation Richness Score + +**Source:** Accreditation portfolio of the selected/candidate provider. +**Composition:** Weighted sum of accreditation types held, normalized against the maximum possible portfolio. +**Usage:** Used in placement tie-breaking (supplements existing tie-breaking algorithm). Also contributes inversely to request risk score — a richly accredited provider reduces risk. +**Default weight in aggregate:** 0.10 (inverse — higher richness = lower risk contribution) + +```yaml +accreditation_weights: + self_declared: 5 + third_party_audit: 15 + iso_27001: 20 + soc2_type2: 20 + fedramp_moderate: 30 + fedramp_high: 40 + hipaa_baa: 25 + pci_dss: 25 + sovereign_authorization: 50 + +# richness_score = sum(weights for held accreditations) / max_possible × 1 + +# Verification currency multipliers (applied per accreditation, see [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md)) +# Multiplier reduces an accreditation's weight contribution based on how recently +# it was externally verified by the Accreditation Monitor +verification_multipliers: + external_registry_verified_within_P1D: 1.0 # full weight — verified today + external_registry_verified_within_P7D: 0.9 + document_verified_within_P30D: 0.85 + contract_webhook_active: 0.9 + expiry_only_no_external_check: 0.7 # never been externally verified + verification_stale: 0.4 # check overdue + verification_failed_threshold_reached: 0.1 # Monitor cannot reach registry00 +# risk_contribution = (1 - richness_score/100) × 10 [lower richness = higher risk] +``` + +### 4.6 Aggregate Request Risk Score + +``` +request_risk_score = + (operational_gating_score × 0.45) + + (completeness_score × 0.15) + + (actor_risk_history_score × 0.20) + + (quota_pressure_score × 0.10) + + (provider_risk_contribution × 0.10) + +# Normalized: 0–100 +# 0 = clean request, no concerns +# 100 = maximum risk signal across all dimensions +``` + +Signal weights are profile-governed and can be adjusted per deployment. The weights above are the `standard` profile defaults. + +--- + +## 5. Profile-Governed Thresholds + +Every profile declares scoring thresholds that map the continuous risk score to a discrete approval routing decision. + +```yaml +# Approval routing uses named tier thresholds — see Authority Tier Model ([design-principles.md §2 (approval tiers)](../design-principles.md)) +# Tier names are resolved from the ordered authority tier list; numeric weights are derived. +scoring_thresholds: + approval_routing: + - tier: auto + max_score: 24 # score 0–24: auto-approve (SMX-008: never exceed 50) + - tier: reviewed + max_score: 59 # score 25–59: reviewed tier required + - tier: verified + max_score: 79 # score 60–79: verified tier required + - tier: authorized + max_score: 100 # score 80–100: authorized tier required + # Custom tiers (if defined) are inserted into this list; existing names unchanged. + # "authorized" means DCM holds the pipeline and notifies the declared DCMGroup; + # the review process and deliberation are the organization's responsibility. + # DCM records votes via Admin API; external systems (ServiceNow, Jira, Slack) + # may call the API on behalf of authorized group members. See [Design Priorities](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md). + # Note: compliance-class Validation Policy deny always halts regardless of score +``` + +### 5.1 Per-Profile Threshold Defaults + +| Profile | auto_approve | reviewed | verified | authorized | signal_weights | +|---------|-------------|-------------|--------------|-----------|----------------| +| `homelab` | < 45 | 45–74 | 75–100 | — | default | +| `dev` | < 40 | 40–69 | 70–100 | — | default | +| `standard` | < 25 | 25–59 | 60–79 | 80–100 | default | +| `prod` | < 15 | 15–49 | 50–74 | 75–100 | gating_weight: 0.50 | +| `fsi` | < 10 | 10–39 | 40–69 | 70–100 | gating_weight: 0.55, actor_weight: 0.25 | +| `sovereign` | < 5 | 5–29 | 30–59 | 60–100 | gating_weight: 0.60 | + +### 5.2 Resource-Type Threshold Overrides + +Profiles can declare tighter thresholds for specific resource types: + +```yaml +resource_type_threshold_overrides: + - resource_type: "Compute.VirtualMachine" + # tier: auto, max_score: 20 # use named-tier threshold format # tighter than profile default + - resource_type: "Network.VLAN" + # tier: auto, max_score: 10 # use named-tier threshold format # VLANs require more scrutiny + - resource_type: "Storage.Volume" + # tier: verified, max_score: 40 # use named-tier threshold format # storage changes escalate earlier +``` + +### 5.3 Tenant Threshold Overrides + +Platform admins can declare Tenant-level scoring threshold adjustments: + +```yaml +tenant_scoring_config: + tenant_uuid: + threshold_overrides: + # tier: auto, max_score: 15 # use named-tier threshold format # more conservative for this Tenant + signal_weight_overrides: + actor_risk_history_weight: 0.30 # higher actor scrutiny for this Tenant + trusted_actors: + - actor_uuid: + actor_risk_history_score_override: 0 # zero out risk history for trusted automation +``` + +### 5.4 Switching Between Scoring and Boolean Per Policy + +A profile can declare that a specific operational-class policy should behave as boolean (compliance-class) in that profile's context: + +```yaml +# In profile definition: +policy_enforcement_overrides: + - policy_handle: "platform/gating/cpu-size-limit" + override_enforcement_class: compliance + rationale: "Prod profile: CPU limit is a hard constraint, not a risk signal" + applies_to_resource_types: ["Compute.VirtualMachine"] +``` + +And conversely, a compliance-class policy that is **not** a regulatory mandate can be demoted to operational in lower-trust profiles: + +```yaml + - policy_handle: "platform/gating/naming-convention" + override_enforcement_class: operational + scoring_weight_override: 20 + rationale: "Dev profile: naming violations are warnings, not blocks" + requires_regulatory_mandate_false: true # safety check +``` + +--- + +## 6. Score Lifecycle and Audit Trail + +### 6.1 Score Record Structure + +Every scored evaluation produces a Score Record stored in the Audit Store alongside the standard audit record: + +```yaml +score_record: + score_record_uuid: + request_uuid: + entity_uuid: + evaluated_at: + + request_risk_score: 47 + routing_decision: reviewed + routing_threshold_applied: 25 # the threshold that triggered this tier + profile_uuid: + + signal_breakdown: + operational_gating: + score: 50 + weight: 0.45 + weighted_contribution: 22.5 + fired_policies: + - handle: "tenant/payments/gating/cost-ceiling" + contribution: 35 + - handle: "platform/gating/off-hours" + contribution: 15 + completeness: + score: 20 + weight: 0.15 + weighted_contribution: 3.0 + advisory_warnings: 2 + actor_risk_history: + score: 30 + weight: 0.20 + weighted_contribution: 6.0 + recent_events: 2 + quota_pressure: + score: 48 + weight: 0.10 + weighted_contribution: 4.8 + provider_risk: + score: 15 + weight: 0.10 + weighted_contribution: 1.5 + + compliance_gates_evaluated: 3 + compliance_gates_fired: 0 # if any > 0: request halted regardless of risk score +``` + +### 6.2 Score Immutability + +Score Records are immutable once written. Threshold changes do not retroactively alter historical Score Records. If thresholds change, requests evaluated before the change retain their original routing decisions in the audit trail. + +### 6.3 Human Override of Score-Based Routing + +A platform admin or reviewer can override a score-based routing decision with a recorded justification. The override is audited, but the Score Record is never modified — instead an Override Record is written referencing the original Score Record. + +--- + +## 7. Score Exposure in APIs + +### 7.1 Consumer-Facing Score Exposure + +Consumers receive a simplified score view: +- `risk_score` on request status (integer 0–100) +- `routing_decision` (auto_approved | pending_review | pending_verified | pending_authorized) +- `advisory_warnings` list from advisory Validation +- `score_drivers` — human-readable list of the top 3 contributing factors (no raw weights) + +Consumers **do not** receive: +- Actor risk history score breakdown (privacy) +- Signal weights +- Provider accreditation richness detail + +### 7.2 Platform Admin Score Exposure + +Platform admins receive full Score Record detail via the Admin API including all signal breakdowns, weights, and actor risk history detail. + +--- + +## 8. Relationship to Existing Decision Model + +The scoring model slots into the existing pipeline without replacing any component: + +``` +Policy Engine evaluation run: + 1. Evaluate all matching policies (existing behavior) + 2. Compliance-class Validation Policy fires → HALT (existing deny behavior) + 3. Structural Validation fails → HALT (existing fail behavior) + 4. Governance Matrix DENY fires → HALT (existing behavior, unchanged) + 5. NEW: Collect operational Validation Policy contributions → Signal 1 + 6. NEW: Collect advisory Validation contributions → Signal 2 + 7. NEW: Fetch actor risk history score → Signal 3 + 8. NEW: Calculate quota pressure score → Signal 4 + 9. NEW: Calculate provider accreditation richness → Signal 5 + 10. NEW: Aggregate → request_risk_score + 11. NEW: Apply profile thresholds → routing_decision + 12. NEW: Write Score Record to Audit Store + 13. Route request: auto_approve | queue_for_review | queue_dual | queue_authorized +``` + +Steps 2–4 handle standard policy evaluation. Steps 5–13 extend the model with scoring and approval routing. + +--- + +## 9. System Policies + +| Policy | Rule | +|--------|------| +| `SMX-001` | Every Validation policy must declare `enforcement_class: compliance` or `enforcement_class: operational`. Policies without a declared enforcement_class are treated as compliance-class. | +| `SMX-002` | Every Validation policy must declare `output_class: structural` or `output_class: advisory`. Policies without a declared output_class are treated as structural. | +| `SMX-003` | Compliance-class Validation policies with `regulatory_mandate: true` cannot be overridden to operational by any profile. This flag is set by platform admins and is audited. | +| `SMX-004` | The Governance Matrix is always boolean. No Governance Matrix Rule may declare a scoring weight or enforcement_class. | +| `SMX-005` | Signal weights in a profile must sum to 1.00. Profiles with invalid weight sums fail validation at activation time. | +| `SMX-006` | Score Records are immutable. Threshold changes do not retroactively alter historical Score Records. | +| `SMX-007` | Actor risk history scores are not exposed to consumers beyond the actor's own history. Platform admins have full access. | +| `SMX-008` | A profile's `auto_approve_below` threshold may not exceed 50. Auto-approving requests with risk scores above 50 is prohibited in all profiles. | +| `SMX-009` | Operational-class Validation Policy `scoring_weight` values must be declared between 1 and 100. Weights above 100 are validation errors. The aggregate of all fired policies is capped at 100 before weighting. | +| `SMX-010` | Score breakdown must be included in the audit trail for every request that receives a routing decision. A request with no Score Record is an audit integrity violation. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/core-petal-integration.md b/architecture/core-petal-integration.md new file mode 100644 index 0000000..460e2a3 --- /dev/null +++ b/architecture/core-petal-integration.md @@ -0,0 +1,82 @@ +# Core·Petal ↔ DCM — integration design note + +**Status:** 🅿️ **Parked** — captured design, not to be built. Activate only when a specific editor / data-modernization use case demands it (see §11). The federation protocol already carries the hard part, so the cost of waiting is near zero. +**Date:** 2026-07 (working draft) · **For:** DCM/UDLM architecture · **Settles:** *whether* Core·Petal fits DCM (it does, cleanly), *what* it would add, and *what already exists to reuse* — so that when a use case lands, the build is a re-cast, not a new platform. + +Companion to the original working note (June 2026) and to UDLM's schema-sharing, layering, and data-mobility contracts. This version adds the reuse map, the UDLM/DCM boundary, and the value/gating call. + +**Provenance.** The **Core·Petal / mBase (modernizationBase)** model originates with **Kevin** — the canonical-Core + per-editor-Petal framing, the retrieval views, governed staged movement ("touch-it-move-it"), and lineage are his. This note does not claim the model; it assesses how it maps onto DCM/UDLM, marks what is reuse versus net-new, and gates the build on a real use case. Credit for the idea is Kevin's; the integration analysis and boundary discipline here are the contribution. + +--- + +## 1. Thesis — a re-cast, not a bolt-on + +DCM today governs **compute**: it places and operates service providers (containers, VMs, clusters). Core·Petal gives it the same grip on **data** — a shared canonical **Core** model with per-editor **Petal** extensions, consumer-shaped **views**, governed **movement**, and **lineage** — under the *same* control plane. The integrating insight is that **a Petal is a schema extension**, and DCM's schema-sharing protocol already distributes extensions. So Core·Petal re-casts DCM's data plane in Core·Petal terms; it does not add a second platform. + +## 2. The boundary — UDLM contract vs DCM runtime (ADR-008) + +This is the spine the picture (§8) doesn't show: every box in the diagram is DCM runtime, but the **contract** lives in three edges. Hold this line. + +| Component | UDLM (data / contract) | DCM (runtime) | +|---|---|---| +| Schema bundle Core/Petal categories | **`core_types[]` / `petal_types[]` + `owning_editor`** (schema-sharing §3/§9) | Bundle exchange + §6 unknown-type discovery | +| Editor | **`editor-contract`** (petal schema ref + transformation spec + consumed views) | **Editor Manager** — registers editors like the SPM registers providers | +| Views | view *shape* (Core / Editor projection) — reuse ADR-002 served overlay | **View & Transformation Service** (`?view=core \| editor:{id}`) | +| Movement | permission=policy, reqs=data (ADR-003); reserve→commit barrier (ADR-011) | **Mediation Gateway** — legacy access + "touch-it-move-it" | +| Lineage | field-level provenance + audit + ADR-012 references/lineage/impact | **Lineage/Provenance** — *only* the currency/value + ML-prioritization hooks are new | +| Store | store-binding contract ([D1], `key_bindings`, sovereignty) | **mBase** — Core + per-editor Petal + lineage stores (federate-don't-copy) | + +**Rule:** UDLM gets the `editor-contract`, the manifest categories, and *reused* view/movement/lineage contracts. DCM gets the four managers + mBase. An Editor is a UDLM contract shape realized by a DCM manager — the same split as type-vs-provider. + +## 3. What it provides, why it matters, and the condition + +**Capability — a governed data plane.** Four things DCM cannot do today: (1) one canonical model many editors extend, no point-to-point integration; (2) consumer-shaped views without copies; (3) governed, lineage-tracked "touch-it-move-it" movement behind Do-No-Harm gates; (4) one provable record spanning meaning (Core) + need (Petal) + movement (lineage). + +**Value.** Collapses N² data integration to N Petals over one Core; modernization without big-bang; provable lineage for regulated data (e.g. Medicare claims); unified compute + data governance under one policy engine, eventing backbone, and audit trail. And it rides existing DCM machinery, so the marginal cost is low. + +**The condition.** This is the **master-data / canonical-data-model capability class** — valuable *when there is demand for it*. The novelty is doing it on DCM's federation protocol, not the pattern. So value is **use-case-gated**: with a live editor-driven program the fit is unusually clean; without one it is scope expansion that turns DCM into a data platform. **Hence: parked.** + +## 4. The integrating insight — a Petal is a schema extension + +Schema-sharing already states a peer "may extend udlm with custom resource types… and any peer can discover, fetch, validate, and version-negotiate those schemas." That *is* the Core·Petal contract in protocol terms: Core = the baseline/core types every peer assumes; a Petal = a peer-owned custom-type extension in that peer's bundle; "same Core + own Petal per editor" = bundle version-negotiation + §6 unknown-type discovery. The only genuinely new manifest semantic is **ownership + the core/petal distinction** — a Petal is a peer-owned extension *of a shared Core*. + +## 5. Reuse map — what already exists (do not rebuild) + +The largest risk is building parallel subsystems for things UDLM/DCM already have. Four of the note's "new" concerns substantially exist: + +- **Lineage/Provenance** → field-level provenance (layering) + audit Merkle chain + **ADR-012 references + immutable lineage + transitive change-impact**. Reuse; add only currency/value + ML-prioritization. +- **Views (Core/Editor)** → ADR-002 served observational overlay + information-providers. A `view=core|editor:{id}` is a projection over that. +- **Staged "touch-it-move-it" movement** → ADR-003 (data mobility: reqs=data / mechanism=provider / permission=policy) + ADR-011 (reserve→commit). Map onto these, don't invent a migration engine. +- **Petal distribution** → schema-sharing bundle + semver + `schema.unknown_type` / `schema.version_incompatible`. Add only ownership/categorization. + +## 6. The two UDLM deltas (the whole net-new data surface) + +1. **Schema-bundle manifest** gains `core_types[]` and `petal_types[]` alongside `entity_types`; each Petal entry carries `owning_editor` + version. (schema-sharing §3/§9.) +2. **`editor-contract`** — the data-plane analogue of `provider-contract.md`: `petal_schema_ref` + `transformation_spec` + `consumed_views`. The Editor Manager registers against it exactly as the SPM registers against the provider-contract. + +Everything else in the design is DCM runtime or reuse. + +## 7. The DCM runtime (a future ADR-025, when un-parked) + +Extended: Catalog → **Core Model Registry** (`*EXT`); Policy → governance/Do-No-Harm gates (`*EXT`); NATS → governance + staged-movement CloudEvents (`*EXT`); persistence → **mBase** (`*EXT`). New: **Editor Manager**, **View & Transformation Service**, **Mediation Gateway**, **Lineage/Provenance** (scoped per §5). These land as a DCM ADR with the required Data·Policy·Provider lens when a UC activates the work. + +## 8. Integrated interface architecture + +![Integrated DCM × Core·Petal interface architecture](images/core-petal-integration.png) + +*Gold = extended existing interface (`*EXT`); teal = new (`+NEW`); the Editors plane (green) is the data-plane peer of the Service Providers plane (blue); mBase is the shared Core·Petal store. The three edges labelled `editor-contract / petal schema`, `schema-bundle (core+petal)`, and `view API core|editor` are the **UDLM contract surface** (§2); everything in the red box is DCM runtime. Note the Editors→SP edge: an Editor is a data-plane contract that MAY also be realized as a compute workload — the strongest single argument for one control plane.* + +## 9. Worked example — Medicare claims (FISS / MCS) + +The canonical **claim** is the Core. **FISS** (Part A) and **MCS** (Part B) register as editors: each sees the identical claim Core, contributes its own Petal of program-specific attributes, and consumes an Editor view transformed to its expected shape. Claim-intake stages (accept → consistency edits → external-file validation → batch edits) run as editor operations against the Core; results, denials, and adjustments emit as governed events with full lineage. A legacy FISS store is reached through the Mediation Gateway during transition, its records mapped to Core+Petal and moved into mBase on first touch. + +## 10. Open questions (resolve when un-parked) + +- Is a Petal a **type-extension** or a **layer**? (It reads as a type-extension; but per-editor reformatting resembles the layer/overlay model — pick one, or state the relationship.) +- How does central **Core-type governance** reconcile with the authority-tier model + **Petal ownership** by editors? +- Does an Editor get its own **`tenant_boundary`**, or is it a sub-scope of a tenant? +- Where does **transformation** run, and is `transformation_spec` declarative (a mapping) or a UDF reference? + +## 11. Trigger to un-park + +Activate when there is a **concrete editor-driven use case** — a real multi-editor canonical-data-model or legacy-modernization program (the FISS/MCS shape). At that point: mint the two UDLM deltas (§6), open DCM ADR-025 (§7) with the reuse map (§5) enforced, and anchor it on the UC's specific Core + Petals. Until then, this note is the captured design; nothing is built. diff --git a/architecture/credentials-and-auth/auth-implementation.md b/architecture/credentials-and-auth/auth-implementation.md new file mode 100644 index 0000000..ce359b0 --- /dev/null +++ b/architecture/credentials-and-auth/auth-implementation.md @@ -0,0 +1,333 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Authentication Implementation +Established: 2026-05-26 +Maps to: udlm/governance/auth-providers.md +--- + +# Authentication Implementation + +> **Implements contracts defined in UDLM**: +> [udlm/governance/auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md). +> UDLM defines the Auth Provider taxonomy (built-in, static API key, local +> users, GitHub/GitLab OAuth, LDAP, FreeIPA, AD, OIDC, SAML, mTLS, SCIM 2.0), +> the multi-provider authentication contract, and the credential +> types/issuance taxonomy. DCM operationalizes the implementation: library +> choices, integration mechanics, routing logic, session management, and +> token lifecycle. + +--- + +## 1. Authentication implementation within DCM + +The DCM API Gateway is the single ingress for all authenticated requests. It +performs the following on every request: + +1. Extract the authentication signal (mTLS cert, Bearer token, Basic auth, + HMAC signature) +2. Route to the appropriate registered Auth Provider per the resolution order +3. Validate the credential through the provider +4. Resolve the actor's roles, groups, and tenant scope +5. Inject `X-DCM-Tenant`, `X-DCM-Actor-Uuid`, `X-Request-ID` headers for + downstream services +6. Apply rate limiting per the authenticated actor + +The Built-in Auth Provider ships with DCM and is always registered. It +provides static API key, local user/password, and optional GitHub/GitLab +OAuth (opt-in via configuration). + +### 1.1 Library and protocol choices + +| Mechanism | DCM library / protocol | +|---|---| +| OIDC / OAuth 2.0 | Standard OIDC discovery + JWKS endpoint validation; ID token format per RFC 7519 | +| SAML 2.0 | OASIS SAML 2.0 assertion parsing; optional Auth Provider type | +| LDAP / FreeIPA / AD | RFC 4511 LDAP v3; bind operation for authentication; group membership via filter (LDAP_MATCHING_RULE_IN_CHAIN for AD nested groups) | +| Kerberos (FreeIPA SSO) | GSSAPI; keytab-based service principal | +| mTLS | RFC 5280 X.509 chain validation; CN → actor mapping | +| SCIM 2.0 | RFC 7643/7644 endpoints at `/scim/v2/Users` and `/scim/v2/Groups` | +| Local users | argon2id password hashing; SQLite (homelab/dev) or PostgreSQL (standard+) backend | +| Sessions | RFC 7519 JWT for stateless session tokens; refresh tokens stored in DB | + +### 1.2 Internal vs External mode + +DCM follows the same Internal/External pattern as policy evaluation and +secrets management: + +- **Internal mode (default):** local user accounts in the `actors` table; + passwords as argon2id hashes; DCM-issued JWT session tokens with + configurable expiry. Zero external dependencies — bootstrap with a local + admin account and start. +- **External mode (optional):** register one or more `auth_provider` + instances; DCM validates their tokens, extracts claims, maps groups to + DCM roles. Multiple providers enable tenant-routed authentication + (Tenant A through AD, Tenant B through Okta). + +External Auth Providers register through the standard provider registration +contract with `provider_type: auth_provider` and capability declaration. + +--- + +## 2. Credential Management Service integration + +DCM never stores credentials directly. The Built-in Auth Provider's secret +storage uses the same envelope encryption mechanism as DCM internal secrets: + +| KEK source | Profile | +|---|---| +| Environment variable | homelab, dev (homelab) | +| Kubernetes Secret | standard, prod | +| HSM via PKCS#11 | fsi, sovereign | + +For external Auth Providers, DCM resolves bind passwords, OAuth client +secrets, SAML signing certificates, and all other secrets via the registered +Credential Management Service (see +[`credentials.md`](credentials.md)). + +### 2.1 Secret references + +Every Auth Provider configuration references secrets, never embeds them: + +```yaml +auth_provider: + provider_type: freeipa + config: + bind_password_ref: + service_provider_uuid: + secret_path: "dcm/auth/freeipa/bind-password" +``` + +DCM resolves the reference at runtime via the Credential Management Service. +Plaintext credentials in registration payloads are rejected (`AUTH-007`). + +--- + +## 3. Provider authentication routing logic + +The API Gateway routes incoming requests to the appropriate Auth Provider +based on the authentication signal: + +```yaml +auth_provider_resolution: + resolution_order: + - signal: mtls_client_cert + provider_uuid: + - signal: bearer_token_oidc + provider_uuid: + - signal: bearer_token_apikey + provider_uuid: + - signal: basic_auth + provider_uuid: + - signal: hmac_signature + provider_uuid: + - signal: none + action: reject # always — no anonymous access +``` + +The resolution order is declared in DCM configuration; the API Gateway walks +it on every request. The first match wins. + +### 3.1 Auth Provider chain (enrichment + augmentation) + +A request can authenticate with one provider and enrich claims via another: + +```yaml +auth_provider_chain: + authentication: + provider_uuid: # fast LDAP bind + enrichment: + provider_uuid: # LDAP group membership + augmentation: + provider_uuid: # OIDC userinfo for rich claims (dept, cost_center, project codes) +``` + +The API Gateway invokes each stage; failure at enrichment or augmentation is +logged but does not block the request unless the active profile requires +all stages to succeed (`fsi`/`sovereign`). + +### 3.2 Failover behavior (AUTH-013) + +When an Auth Provider becomes unhealthy: + +- **In-flight requests** authenticated before the failure continue using + cached session tokens +- **New requests** follow the declared failover chain +- **Session expiry during outage** requires re-authentication via available + failover provider; if all providers unavailable → reject with clear error + +```yaml +auth_failover_config: + primary_provider_uuid: + failover_chain: + - provider_uuid: + promotion_delay: PT30S + - provider_uuid: + session_cache: + enabled: true + ttl: PT8H +``` + +--- + +## 4. Session management and token lifecycle + +DCM issues its own session tokens (JWT) for actors authenticated through any +Auth Provider. Session tokens carry: `actor_uuid`, `roles`, `tenant_scope`, +`auth_provider_uuid`, `exp`, `iat`. + +### 4.1 Session configuration + +```yaml +session: + token_ttl: PT8H # per-profile default + refresh_enabled: true + refresh_ttl: P7D + concurrent_sessions: 3 # max per actor; enforced via session_store +``` + +The `sessions` table stores active session metadata; revocation is a status +update. Token introspection per RFC 7662 is exposed at +`POST /api/v1/auth:introspect`. + +### 4.2 MFA enforcement (AUTH-014) + +DCM implements two-tier MFA: + +- **Per-session MFA:** validated at login; captured in the JWT `mfa_verified` claim +- **Step-up MFA:** additional challenge at sensitive operations within an + already-authenticated session; results in a short-lived (PT10M) step-up + token + +```yaml +step_up_mfa_config: + step_up_required_for: + - platform_policy_activate + - provider_decommission + - tenant_decommission + - sovereignty_zone_change + - auth_provider_update + - manual_rehydration + step_up_method: totp | push_notification | hardware_token | sms + step_up_token_ttl: PT10M + step_up_challenge_max_age: PT5M +``` + +Profile defaults govern which operations require step-up: + +| Profile | Per-Session MFA | Step-Up Required | +|---|---|---| +| homelab | No | No | +| dev | No | No | +| standard | Recommended | Optional | +| prod | Required | Destructive operations | +| fsi | Required | All policy changes | +| sovereign | Required (hardware token) | All administrative operations | + +### 4.3 Session revocation + +Session revocation follows the SES-001 model (in +[`../control-plane/session-revocation.md`](../control-plane/session-revocation.md)). +Triggers include actor deprovisioning, manual admin revocation, password +change, and security event. + +When a session is revoked, the Auth Implementation: + +1. Marks the session row status: `revoked` +2. Publishes `session.revoked` event to `pipeline_events` +3. All Policy Manager and API Gateway instances invalidate their permission + cache entries for the actor + +### 4.4 SCIM 2.0 deprovisioning + +When SCIM signals an actor deprovision: + +1. Actor's session(s) revoked immediately +2. All credentials issued to actor revoked (per `CPX-006`) +3. In-flight requests complete on cached tokens; new requests rejected +4. Audit record written with `source: scim_deprovision` + +The SCIM endpoint at `/scim/v2/Users/{id}` (DELETE) triggers +`AUTH-016`/`SES-001`/`CPX-006` in parallel. + +--- + +## 5. Git PR actor identity resolution + +When DCM processes Git PR ingress, the Git server's authenticated user must +resolve to the same DCM actor as if they had logged into the web UI: + +``` +Git server authenticates user → PR merge webhook → DCM Auth Provider resolution → DCM actor +``` + +DCM trusts the Git server's verified identity assertion — not user-declared +Git config. Resolution methods: + +| Method | When | +|---|---| +| `oidc_subject_lookup` | Git server uses same OIDC/OAuth IdP as DCM | +| `ldap_username_lookup` | Git server authenticates via LDAP/AD | +| `ssh_key_fingerprint` | SSH key-authenticated Git workflows | +| `webhook_service_account` | Automated CI/CD Git workflows | + +The resolved actor carries identical role, group, and tenant scope to the +same user authenticating via web UI (`AUTH-011`). Git PR ingress does not +grant different permissions than any other ingress surface. + +--- + +## 6. Authentication ladder (DCM realization) + +Every rung is authenticated. The ladder is about setup effort — not whether +authentication exists. + +| Profile | Modes available | Setup effort | +|---|---|---| +| `homelab` | Static API key, Local user/password | 30 seconds – 2 minutes | +| `dev` | + GitHub/GitLab OAuth, FreeIPA/AD direct bind | 5–15 minutes | +| `standard` | + OIDC via broker (Dex/Keycloak), AD/FreeIPA direct | 30–60 minutes | +| `prod` | + OIDC direct, MFA | 1–2 hours | +| `fsi` | + mTLS required, MFA required | 4–8 hours | +| `sovereign` | + Air-gapped OIDC/mTLS | 1–2 days | + +### 6.1 Built-in Auth Provider storage backend (AUTH-015) + +```yaml +builtin_auth_provider_config: + user_store: + profile_defaults: + minimal: sqlite # zero infrastructure; single-file + dev: sqlite + standard: postgresql + prod: postgresql + fsi: postgresql # encrypted (TDE required) + sovereign: postgresql # HSM-backed encryption required + encryption_at_rest: + required_profiles: [fsi, sovereign] + key_ref: + service_provider_uuid: + path: "dcm/auth/builtin/encryption-key" +``` + +The local user store should only contain: bootstrap users, service accounts, +and API key holders. Enterprise users belong in external Auth Providers +(LDAP, OIDC, SCIM). + +--- + +## 7. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `AUTH-001-DCM` | All DCM authentication is handled through a registered Auth Provider; the built-in is always available | +| `AUTH-002-DCM` | DCM routes to the appropriate Auth Provider based on the authentication signal in the request | +| `AUTH-005-DCM` | When an Auth Provider becomes unhealthy, existing sessions remain valid until TTL expiry; new auth follows failover chain or is rejected | +| `AUTH-006-DCM` | DCM records the Auth Provider used in the ingress block and carries it into the audit record | +| `AUTH-007-DCM` | DCM rejects Auth Provider configurations containing plaintext credentials; secret references required | +| `AUTH-008-DCM` | DCM permits no anonymous access in any profile; homelab/dev support lightweight authenticated modes | +| `AUTH-009-DCM` | DCM always requires authentication on webhook and message bus inbound surfaces regardless of profile | +| `AUTH-010-DCM` | DCM enforces rate limiting per authenticated actor; limits declared on Auth Provider or webhook registration | +| `AUTH-011-DCM` | DCM resolves Git PR actor identity through the registered Auth Provider; resolved actor carries same role/group/tenant scope as web UI authentication | +| `AUTH-013-DCM` | In-flight requests continue on cached tokens during Auth Provider outage; new auth follows failover chain | +| `AUTH-014-DCM` | DCM enforces two-tier MFA: per-session (mfa_verified claim) and step-up (short-lived token) for sensitive operations per policy | +| `AUTH-015-DCM` | DCM's built-in Auth Provider uses SQLite for homelab/dev, PostgreSQL for standard+; encryption-at-rest required in fsi/sovereign | diff --git a/architecture/credentials-and-auth/authority-enforcement.md b/architecture/credentials-and-auth/authority-enforcement.md new file mode 100644 index 0000000..3c2ac1b --- /dev/null +++ b/architecture/credentials-and-auth/authority-enforcement.md @@ -0,0 +1,376 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Authority Tier Enforcement +Established: 2026-05-26 +Maps to: udlm/governance/authority-tier-model.md +--- + +# Authority Enforcement + +> **Implements contracts defined in UDLM**: +> [udlm/governance/authority-tier-model.md](https://github.com/croadfeldt/udlm/blob/main/governance/authority-tier-model.md). +> UDLM defines the core authority tier model (auto / reviewed / verified / +> authorized), the decision_gravity vocabulary, the custom tier definition +> contract, the tier registry change impact detection contract, and the +> degradation review gate contract. DCM operationalizes the tier evaluation +> algorithm, approval authority mapping, profile threshold configuration, +> DCMGroup assignment, tier enforcement at decision points, and the +> degradation review orchestration. + +--- + +## 1. Tier evaluation algorithm + +DCM evaluates required tier at every approval-gated decision point. The +algorithm: + +``` +At decision evaluation time: + ▼ 1. Compute the request risk score (0–100) via the Scoring Model + ▼ 2. Load the active profile's threshold list + ▼ 3. Walk the list in order; the first tier whose max_score ≥ risk_score + is the required tier + ▼ 4. Resolve the numeric weight of the required tier from the ordered + tier registry + ▼ 5. Create an approval record with the required tier name and weight +``` + +The tier **name** (not the weight) is what's stored in the approval record +and shown to reviewers. The weight is used for comparison operations +(e.g., "is this action at least as significant as `verified`?"). + +### 1.1 Approval record + +```yaml +approval_record: + approval_uuid: + subject_uuid: + subject_type: request | policy_contribution | provider_registration | federation_contribution + required_tier: verified # tier name — stable reference + required_tier_weight: 3 # resolved at creation; point-in-time audit + required_tier_gravity: elevated + dcmgroup_uuid: # non-null only for dcmgroup_required: true tiers + quorum_threshold: + status: pending_reviewed | pending_verified | pending_authorized | pending_ + created_at: + window_expires_at: + decisions: [] +``` + +The `required_tier_weight` is **stored at creation** (`ATM-008`). If the +tier registry changes later, the stored weight reflects the state at +creation — point-in-time audit. + +--- + +## 2. Approval authority mapping + +DCM enforces each tier per the contract: + +| Tier | DCM gate | +|---|---| +| `auto` | All structural and governance validation checks pass; automatic activation | +| `reviewed` | One actor with reviewer role records a decision via `POST /api/v1/admin/approvals/{uuid}:vote` | +| `verified` | Two distinct actors with reviewer role each record a decision (DCM enforces actor distinctness — same actor cannot satisfy both) | +| `authorized` | N members of a declared DCMGroup record decisions; DCM tracks quorum (N of M); pipeline advances when N reached | + +### 2.1 Admin API as integration point + +DCM's vote-recording endpoint is designed for external system integration, +not only humans-in-UI: + +``` +POST /api/v1/admin/approvals/{approval_uuid}:vote +Authorization: Bearer # any actor who is a member of the required DCMGroup +{ + "decision": "approve | reject", + "reason": "", + "recorded_via": "dcm_admin_ui | servicenow | jira | slack_bot | api_direct | other", + "external_reference": "" +} +→ { + "approval_uuid": "", + "voter_uuid": "", + "decision": "approve", + "votes_recorded": 2, + "quorum_required": 3, + "quorum_reached": false, + "pipeline_status": "pending_authorized" + } +``` + +The `recorded_via` field provides audit provenance — informational, not +enforced. DCM does not care whether the vote came from a Slack bot, ServiceNow +integration, Jira plugin, or direct API call — only that an authorized actor +recorded it. + +### 2.2 Deadline and escalation + +```yaml +approval_window: + reviewed: PT72H + verified: PT72H + authorized: P7D + on_expiry: + reviewed: escalate + verified: escalate + authorized: reject +``` + +When the window expires without a decision, DCM fires an escalation +notification. For `reviewed` and `verified`, escalates to platform admin or +the next tier. For `authorized`, rejects (cannot lower the authority gate +silently). + +--- + +## 3. Profile threshold configuration + +DCM ships per-profile threshold defaults: + +```yaml +profile_approval_thresholds: + minimal: + - { tier: auto, max_score: 44 } + - { tier: reviewed, max_score: 100 } + + dev: + - { tier: auto, max_score: 39 } + - { tier: reviewed, max_score: 69 } + - { tier: verified, max_score: 100 } + + standard: + - { tier: auto, max_score: 24 } + - { tier: reviewed, max_score: 59 } + - { tier: verified, max_score: 79 } + - { tier: authorized, max_score: 100 } + + prod: + - { tier: auto, max_score: 14 } + - { tier: reviewed, max_score: 49 } + - { tier: verified, max_score: 74 } + - { tier: authorized, max_score: 100 } + + fsi: + - { tier: auto, max_score: 9 } + - { tier: reviewed, max_score: 39 } + - { tier: verified, max_score: 69 } + - { tier: authorized, max_score: 100 } + + sovereign: + - { tier: auto, max_score: 4 } + - { tier: reviewed, max_score: 29 } + - { tier: verified, max_score: 59 } + - { tier: authorized, max_score: 100 } +``` + +**SMX-008 in the dynamic model:** `auto.max_score` may never exceed 50 in any +profile, regardless of custom tier additions. DCM enforces this at profile +contribution time (`ATM-002`). + +### 3.1 Custom tier insertion + +When an organization adds a custom tier (e.g., `compliance_reviewed` between +`verified` and `authorized`), DCM: + +1. Validates `decision_gravity` is consistent with position (`ATM-003`) +2. Requires `verified` tier approval to add the tier (`ATM-004`) +3. Re-resolves numeric weights from list position (`ATM-001`) +4. Triggers the Tier Registry Change Impact Detection pipeline (Section 5) + +Existing references to `authorized` continue to work — the name is stable; +the weight is updated. + +--- + +## 4. DCMGroup assignment + +When a decision requires the `authorized` tier (or any custom tier with +`dcmgroup_required: true`), DCM resolves the required DCMGroup and quorum +threshold from the profile or per-action-type config: + +```yaml +authorized_tier_configuration: + default_dcmgroup_handle: platform/security-council + quorum_threshold: "2 of 5" + + # Per-action-type overrides + action_type_overrides: + - subject_type: provider_registration + provider_type: service_provider + dcmgroup_handle: platform/credential-governance + quorum_threshold: "3 of 5" + - subject_type: federation_contribution + dcmgroup_handle: platform/federation-council + quorum_threshold: "2 of 3" + - subject_type: policy_contribution + policy_domain: system + dcmgroup_handle: platform/policy-governance + quorum_threshold: "3 of 5" +``` + +DCM enforces: +- Required DCMGroup must be declared before the tier can be used as a + routing target (`ATM-006`) +- Each decision is attributed to the specific DCMGroup member who recorded it +- The audit record links to the DCMGroup at decision time (point-in-time + membership) + +--- + +## 5. Tier enforcement at decision points + +DCM applies the tier evaluation algorithm at every decision point: + +| Decision point | Trigger | +|---|---| +| Resource request | Risk score computed; threshold resolved; approval record created (if non-auto) | +| Policy contribution | Per `contribution_policy` in active profile; tier resolved based on contributor + artifact type + profile | +| Provider registration | Per `provider_type_registry.default_approval_method`; tier resolved at registration submission | +| Federation contribution | Per peer trust posture × profile contribution_policy | +| Sovereignty zone change | Tier resolved at change submission; typically `authorized` | +| Auth Provider update | Tier resolved at update; typically `verified` in standard+ | +| Federation policy update | Tier resolved per policy domain | + +In every case, the approval record drives the pipeline. The pipeline holds +in `pending_` state until quorum is reached or the deadline expires. + +--- + +## 6. Degradation review orchestration + +UDLM defines the tier registry change impact detection contract. DCM +operationalizes the detection pipeline. + +### 6.1 Tier impact diff computation + +When a tier registry change is proposed, DCM computes the diff before +activation: + +```yaml +tier_impact_diff: + registry_change_uuid: + proposed_at: + proposed_by: + + tier_changes: + - tier_name: verified + change_type: POSITION_CHANGED # NEW | REMOVED | POSITION_CHANGED | GRAVITY_CHANGED | UNCHANGED + old_position: 3 + new_position: 4 + old_gravity: elevated + new_gravity: elevated + net_effect: UPGRADED # UPGRADED | DEGRADED | NEW | REMOVED | UNCHANGED + + security_degradations: [] + profile_gaps: [] + broken_references: [] +``` + +### 6.2 Affected item query + +For each changed tier, DCM queries for affected items: + +| Category | Query | +|---|---| +| Pending approval records | WHERE required_tier IN (changed_tier_names) AND status LIKE 'pending_%' | +| Profile threshold configs | WHERE tier_registry_version < new_registry_version | +| Provider registration requirements | WHERE default_approval_method IN (changed_tier_names) | +| FCM contribution policy requirements | WHERE any tier reference IN (changed_tier_names) | +| Active policy sets | WHERE policy_content CONTAINS tier_name_reference | + +### 6.3 Impact classification + +Each affected item receives one or more classifications: + +| Classification | Condition | Required action | +|---|---|---| +| `SECURITY_DEGRADATION` | Item references a tier whose gravity decreased OR position decreased | **Blocks activation** — must be reviewed and accepted | +| `SECURITY_UPGRADE` | Item references a tier whose gravity or position increased | Informational | +| `BROKEN_REFERENCE` | Item references a tier name that no longer exists | **Blocks activation** — must be resolved | +| `PROFILE_GAP` | Profile threshold list incomplete after new tier insertion | **Warning** — does not block | +| `STALE_WEIGHT` | Pending approval's stored_tier_weight differs from current | Informational | + +### 6.4 Degradation review gate + +Security degradations block activation. The gate requires: + +1. Each `SECURITY_DEGRADATION` item presented to a reviewer at `verified` or above +2. The reviewer records an explicit acceptance via Admin API +3. The acceptance includes a reason; written to audit trail +4. Only after ALL degradations accepted does the tier registry change activate + +``` +POST /api/v1/admin/tier-registry/{change_uuid}:accept-degradation +{ + "affected_item_uuid": "", + "affected_item_type": "provider_registration_requirement", + "degradation_classification": "SECURITY_DEGRADATION", + "acceptance_reason": "", + "accepted_by": "" +} +``` + +Broken references **cannot be accepted** — they must be resolved (tier +restored, item updated, or item cancelled). DCM will not activate a +registry change that leaves unresolvable references (`ATM-010`). + +### 6.5 Impact report + +DCM generates a tier registry impact report at proposed time and again at +activation time: + +```yaml +tier_registry_impact_report: + registry_change_uuid: + report_generated_at: + stage: proposed | accepted | activated + + summary: + degradations: 0 + upgrades: 3 + new_tiers: 1 + broken_references: 0 + profile_gaps: 2 + stale_weight_records: 4 + + degradations: [] + upgrades: [...] + profile_gaps: [...] + + notification_targets: + - platform_admin + - provider_owners + - affected_actor_groups +``` + +The report is stored in the Audit Store and linked to the tier registry +version (`ATM-011`). + +### 6.6 Audit trail + +Every tier registry change produces: + +- Registry change proposal record +- Tier impact diff record (all changes, all affected items, all classifications) +- Per-degradation acceptance records (if any) +- Registry activation record (actual effective timestamp) +- Per-affected-item notification records + +--- + +## 7. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `ATM-001-DCM` | DCM identifies tiers by name; numeric weight resolved from list position at evaluation time | +| `ATM-002-DCM` | DCM enforces auto.max_score ≤ 50 in any profile regardless of custom tier additions | +| `ATM-003-DCM` | DCM validates custom tier decision_gravity is consistent with position | +| `ATM-004-DCM` | DCM requires verified tier approval for custom tier contributions | +| `ATM-005-DCM` | DCM rejects custom tier definitions that alter dcm_gate semantics of system tiers | +| `ATM-006-DCM` | For dcmgroup_required tiers, DCMGroup and quorum threshold must be declared before tier becomes a routing target | +| `ATM-008-DCM` | DCM stores tier name and resolved weight in approval records at creation — point-in-time audit | +| `ATM-009-DCM` | DCM blocks tier registry activation on SECURITY_DEGRADATION until each is explicitly accepted by verified-tier reviewer | +| `ATM-010-DCM` | DCM blocks tier registry activation on BROKEN_REFERENCE; must be resolved | +| `ATM-011-DCM` | DCM produces tier impact report stored in Audit Store linked to registry version | +| `ATM-012-DCM` | DCM generates warning notification for PROFILE_GAP; change may activate; admins update or acknowledge within approval window | diff --git a/architecture/credentials-and-auth/credentials.md b/architecture/credentials-and-auth/credentials.md new file mode 100644 index 0000000..b86f6ed --- /dev/null +++ b/architecture/credentials-and-auth/credentials.md @@ -0,0 +1,464 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Credential Management +Established: 2026-05-26 +Maps to: udlm/governance/credentials.md +--- + +# Credentials + +> **Implements contracts defined in UDLM**: +> [udlm/governance/credentials.md](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md). +> UDLM defines the credential scope (internal vs consumer-facing), the +> credential type taxonomy (api_key, JWT, mTLS cert, SSH key, secret, +> signing key, HSM-backed, dcm_interaction), the credential lifecycle +> (issuance / active / rotation / revocation / expired), the rotation +> protocol with parallel validity windows, the revocation propagation +> contract, consumer credential delivery, provider API contract, and +> cryptographic requirements (deferred to standards catalog). DCM +> operationalizes the storage, generation, issuance flow, rotation +> execution, revocation enforcement, delivery mechanics, validation, and +> profile-governed configuration. + +--- + +## 1. Credential storage and access control + +DCM operates at two levels of credential management: + +### 1.1 Internal — DCM operational secrets + +DCM's own operational secrets use envelope encryption in the PostgreSQL +`secrets` table. Each value is encrypted with AES-256-GCM using a per-secret +data encryption key (DEK); DEKs are encrypted with a master key (KEK) +sourced from the deployment environment. + +| KEK source | Profile | Security level | +|---|---|---| +| Environment variable | homelab, dev | Basic — protects against database theft | +| Kubernetes Secret | standard, prod | Good — K8s RBAC + etcd encryption | +| HSM via PKCS#11 | fsi, sovereign | Strong — KEK never leaves the HSM | + +The `secrets` table has the same RLS, append-only audit, and tenant isolation +as every other DCM table. Used for: provider authentication credentials +(values referenced from PCA records), encryption keys for sensitive JSONB +fields (PHI, PCI), audit signing keys, internal service credentials. + +### 1.2 Consumer-facing — Credential Provider + +Consumer-facing credentials (kubeconfigs, database passwords, API keys, SSH +keys, service account tokens) flow through a registered Credential Provider +(a service_provider with `Credential.*` in supported_resource_types). + +**Credential values are never stored in DCM** (`CPX-001` — non-negotiable in +all profiles). DCM stores only credential metadata: UUID, type, scope, +expiry, status. The actual value is held exclusively by the Credential +Provider, retrieved by the authorized consumer via the provider's +`value_retrieval_endpoint`. + +External Credential Providers are registered via the standard provider +registration contract: + +```yaml +provider: + provider_type: service_provider + supported_resource_types: + - "Credential.Secret" + - "Credential.Certificate" + - "Credential.SSHKey" + - "Credential.APIKey" + capability_extension: + hsm_support: true + rotation_protocol: automatic + max_secret_size_bytes: 65536 + supported_algorithms: [rsa-2048, rsa-4096, ecdsa-p256, ecdsa-p384, ed25519] +``` + +--- + +## 2. Credential generation implementation + +DCM does not generate credential values itself (except bootstrap tokens during +initial registration). Generation is delegated to the registered Credential +Provider per the contract in +[udlm/governance/credentials.md](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md). + +DCM's role at generation time: + +1. Validate the issuance request matches an authorized DCM operation (e.g., + resource realization triggered the request) +2. Compute the `expires_at` based on the active profile's max_lifetime +3. Build the scope: `issued_to`, `operations`, `resource_types`, `tenant_uuid` +4. Apply profile-governed bindings: `bound_to_ip` if required, hardware + attestation flag if sovereign +5. Submit to the Credential Provider's `issue_endpoint` +6. Persist the returned credential_record metadata in `credentials` table +7. Return credential metadata to the consumer (never the value) + +--- + +## 3. Issuance flow orchestration + +### 3.1 Resource credential issuance (consumer-facing) + +Credentials issued as part of resource realization flow through the standard +provider dispatch pipeline: + +``` +Consumer requests resource (e.g., Compute.VirtualMachine) + ▼ Layer assembly + policy evaluation + │ Transformation policy may inject credential_requirements: + │ - credential_type: ssh_key + │ issued_to: requesting_actor + │ scope: [ssh_access] + ▼ Placement selects Service Provider for the VM + ▼ After VM realization: Credential Provider dispatched + │ Sub-request issued to Credential Provider with: + │ entity_uuid, credential_type, issued_to.actor_uuid, + │ scope.operations, scope.resource_types, expires_at + ▼ Credential Provider issues credential; returns credential_record + ▼ DCM writes credential_record to Realized State; links credential_uuid to entity_uuid + ▼ Consumer receives realized entity + credential metadata + │ Consumer calls value_retrieval_endpoint to get actual credential + │ (step-up MFA may be required per profile) +``` + +### 3.2 DCM interaction credential issuance + +DCM interaction credentials are issued automatically before each provider +interaction. They implement the Zero Trust scoped credential model: + +``` +DCM prepares to dispatch to a provider + ▼ API Gateway requests interaction credential from Credential Provider: + │ credential_type: dcm_interaction + │ issued_to.component_uuid: + │ issued_to.provider_uuid: + │ scope.operations: [dispatch] + │ scope.resource_types: [Compute.VirtualMachine] + │ entity_uuid: + │ expires_at: (external-dispatch ceiling; the per-profile ladder is provider-callback §3.3 — the one table) + ▼ DCM includes credential in provider dispatch + ▼ Provider validates credential scope before executing + ▼ Credential expires after PT15M regardless of use + │ (no renewal; new credential issued for next interaction) +``` + +### 3.3 Bootstrap credential issuance + +During bootstrap (before Credential Provider is registered), DCM uses a +bootstrap credential mechanism. After bootstrap, all credentials are issued +through a registered Credential Provider. + +--- + +## 4. Rotation job scheduling and execution + +DCM operationalizes the UDLM rotation contract through scheduled rotation +jobs (no transition window for emergency rotations). + +### 4.1 Rotation triggers + +| Trigger | DCM mechanism | +|---|---| +| `scheduled` | Cron-based rotation per credential type interval | +| `pre_expiry` | Time-based: rotation initiated `pre_expiry_window` before expires_at; PT5M for dcm_interaction, P14D for x509, P7D for ssh_key | +| `provider_initiated` | Provider's update notification (PCA model) triggers rotation | +| `security_event` | Emergency revocation triggers — no transition window | +| `actor_request` | Consumer requests via API; rate-limited per policy | + +### 4.2 Rotation protocol execution + +``` +Rotation initiated (any trigger): + ▼ DCM requests new credential from Credential Provider + │ rotation_of: + │ same scope; new expires_at + ▼ Credential Provider issues new credential + │ Returns new credential_record; old NOT yet revoked + ▼ New credential delivered to authorized consumer/component + ▼ Transition window: both credentials valid + │ Duration: P1D for consumer credentials (default) + │ PT5M for dcm_interaction + │ P7D for x509_certificate + ▼ Old credential revoked at end of transition window + │ Revocation propagated to all registered consumers + ▼ Rotation record written to audit trail +``` + +### 4.3 Pre-expiry rotation scheduler + +A background worker per Credential Provider scans for credentials approaching +expiry: + +```sql +SELECT credential_uuid, expires_at +FROM credentials +WHERE status = 'active' + AND expires_at - pre_expiry_window <= now() + AND rotation_in_progress = false +``` + +For each match, the rotation pipeline kicks off automatically. The worker +runs on the cadence of the shortest pre_expiry_window across active +credential types (typically PT1M for dcm_interaction credentials). + +### 4.4 Emergency rotation (security event) + +``` +Triggers: security.credential_compromised, security.anomalous_usage_detected, + actor.deprovisioned, provider.deregistered, accreditation.revoked + ▼ No transition window + ▼ Old credential revoked immediately + ▼ New credential issued and delivered via fastest available channel + ▼ Security event record written with full context + ▼ Compliance-class Validation Policy firing audited against the event + ▼ Platform admin notified regardless of profile +``` + +--- + +## 5. Revocation enforcement across providers + +### 5.1 Revocation triggers + +| Trigger | DCM Behavior | +|---|---| +| `actor_deprovisioned` | All credentials for actor revoked; propagated via SCIM (per `CPX-006`) | +| `entity_decommissioned` | All credentials scoped to entity revoked before decommission confirmed (per `CPX-007`) | +| `security_event` | Immediate; no transition window | +| `provider_deregistered` | All interaction credentials for provider revoked | +| `actor_request` | Consumer may revoke their own credentials | +| `ttl_expired` | Lifecycle Constraint Enforcer triggers revocation | + +### 5.2 Revocation propagation + +DCM maintains a Credential Revocation Registry — fast-queryable store of +revoked credential UUIDs. All components that receive interaction credentials +must check this registry on each use, not just at issuance. + +``` +Credential revoked + ▼ credential record status: active → revoked + ▼ revoked_at, revocation_reason persisted + ▼ credential.revoked event published to pipeline_events + ▼ All subscribed components update local revocation cache + │ CACHE-PROPAGATION TTL (owner: this table; CPX-003-DCM cites it): + │ PT1M standard, PT30S fsi/sovereign + ▼ Credential Provider notified to invalidate stored value + │ PROVIDER-INVALIDATION SLA (owner: this table): + │ standard/prod: PT5M + │ fsi/sovereign: PT1M + │ (Distinct SLAs elsewhere: SESSION-REGISTRY SLA = session-revocation.md AUTH-017; + │ internal-cert CRL refresh = internal-component-auth.md ICOM-008. Four named SLAs, + │ one owner each — cite the name, never restate the numbers.) + ▼ Audit record: credential_uuid, revocation_trigger, revoked_by_actor +``` + +### 5.3 Revocation check at use + +Providers receiving DCM interaction credentials must validate at use time, +not only at receipt: + +1. Verify credential signature (if signed) +2. Check credential UUID against local revocation cache +3. Verify credential has not expired (`expires_at`) +4. Verify operation is within credential scope +5. Verify IP binding if `bound_to_ip` is set + +Failure → return `403 Forbidden` with `credential_revoked` or +`credential_expired` error code. + +--- + +## 6. Consumer delivery mechanics + +After resource realization with associated credential, the consumer receives +`credential_record` metadata in the realized entity response. The actual +value is retrieved separately: + +``` +GET /api/v1/resources/{entity_uuid}/credentials +→ { + "credentials": [ + { + "credential_uuid": "", + "credential_type": "ssh_key", + "status": "active", + "issued_at": "", + "expires_at": "", + "scope": {...}, + "retrieval": { + "endpoint": "/api/v1/credentials//value", + "auth_required": "step_up_mfa", + "retrieval_count": 1, + "last_retrieved_at": "" + }, + "rotation_schedule": {...} + } + ] + } +``` + +### 6.1 Value retrieval + +``` +GET /api/v1/credentials/{credential_uuid}/value +Authorization: Bearer +X-DCM-StepUp-Token: # if auth_required: step_up_mfa +→ { + "credential_uuid": "", + "credential_type": "ssh_key", + "value": { "private_key": "...", "public_key": "...", "username": "..." }, + "expires_at": "", + "retrieval_uuid": "" # idempotency key; audited + } +``` + +Every retrieval is audited: credential_uuid, actor_uuid, retrieved_at, +retrieval_uuid (`CPX-005`). + +--- + +## 7. Provider authentication validation + +The Credential Provider must validate at use time (see Section 5.3 above): +- Signature verification +- Revocation cache check +- Expiry check +- Scope check +- IP binding (if bound) +- `key_usage` enforcement — a credential issued for `authentication` cannot + be used for `signing` (`CPX-009`) + +--- + +## 8. Profile-governed constraints (enforcement) + +DCM enforces the credential profile configuration at issuance and use. + +| Setting | homelab/dev | standard/prod | fsi/sovereign | +|---|---|---|---| +| Default TTL | P365D | P90D | P30D | +| Max TTL | unlimited | P365D | P90D | +| Rotation grace period | P7D | P3D | P1D | +| HSM required | No | No | Yes (signing keys) | +| Idle credential detection | Disabled | P90D warning | P30D auto-revoke | +| IP binding | Optional | Optional | Required | +| FIPS level | None | Level 1 | Level 2 (fsi) / Level 3 (sovereign) | + +Full per-profile configuration matrix is in +[udlm/governance/credentials.md Section 12.1](https://github.com/croadfeldt/udlm/blob/main/governance/credentials.md). +DCM applies these constants at issuance — Credential Provider may issue +shorter than max_lifetime; never longer. + +### 8.1 Algorithm enforcement + +DCM rejects credentials with forbidden algorithms (MD5, SHA-1, DES, 3DES, +RC4, RSA < 2048, ECDSA < P-256) at issuance regardless of profile. Approved +algorithms vary per profile: + +- `homelab/dev`: negative list (forbidden_algorithms enforced; everything + else permitted) +- `standard+`: positive list per credential type +- `fsi`: FIPS-approved subset (Ed25519 excluded from FIPS 140-2 in `fsi`; + permitted in standard) +- `sovereign`: hsm_backed_only across all types + +See [`../reference/implementation-standards.md`](../../reference/implementation-standards.md) +for the algorithm and FIPS-level decisions DCM makes. + +--- + +## 9. Integration with external services + +| External system | Integration | +|---|---| +| HashiCorp Vault PKI | Register as Credential Provider with `secret_engine: vault`; supports x509_certificate, secrets, dynamic secrets | +| AWS Secrets Manager | Register as Credential Provider with `secret_engine: aws_secrets_manager` | +| Azure Key Vault | Register as Credential Provider with `secret_engine: azure_key_vault` | +| GCP Secret Manager | Register as Credential Provider with `secret_engine: gcp_secret_manager` | +| Local HSM (sovereign) | Register as Credential Provider with `secret_engine: local_hsm`; FIPS 140-2 Level 3 | +| Enterprise CA (cert-manager / Venafi / EJBCA) | Register as Credential Provider with `external_ca_config` supporting ACME / EST / SCEP / CMP | + +### 9.1 External CA integration + +DCM's Credential Provider model natively supports external CAs as backends +for x509_certificate. When configured as the trust anchor for internal +component auth, DCM's component certificate requests flow through the +Credential Provider interface instead of the built-in Internal CA. This +makes DCM's internal mTLS fully auditable through existing enterprise PKI +infrastructure — a key requirement for fsi/sovereign profiles. + +```yaml +external_ca_config: + ca_protocol: acme | est | scep | cmp | vault_pki | aws_acm_pca | azure_key_vault + ca_endpoint: + issued_cert_lifetime: P90D + subject_template: "CN={{component_type}}-{{component_uuid}},O=dcm-internal" +``` + +--- + +## 10. Idle credential detection + +A credential issued but never retrieved within the declared threshold +triggers an idle alert: + +```yaml +idle_credential_record: + credential_uuid: + issued_at: + threshold_hours: 48 + last_checked_at: + retrieval_count: 0 + status: idle_alert_pending +``` + +Idle threshold by profile: P30D (homelab) → PT12H (sovereign). The credential +is NOT automatically revoked at the threshold — alert only. Auto-revocation +after 2× threshold is profile-configurable (`CPX-010`). + +--- + +## 11. Lifecycle state machine (DCM realization) + +``` + ┌──────────────┐ + issuance │ │ expiry / explicit + ─────────────────►│ ACTIVE │────revocation──────────► REVOKED / EXPIRED + │ │ + └──────┬───────┘ + │ rotation initiated + ▼ + ┌──────────────┐ + │ ROTATING │ both old and new valid + │ │ during transition window + └──────┬───────┘ + │ transition window ends + │ or emergency revocation + ▼ + REVOKED +``` + +Every transition writes an audit record with `credential_uuid`, transition +type, and trigger metadata. + +--- + +## 12. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `CPX-001-DCM` | DCM stores only credential metadata; credential values never in DCM data model, GitOps stores, Realized State Store, or Audit Store | +| `CPX-002-DCM` | Every DCM provider interaction presents a scoped, short-lived `dcm_interaction` credential; providers reject calls without one (403) | +| `CPX-003-DCM` | DCM propagates revocation within profile-governed cache TTL (PT1M standard; PT30S fsi/sovereign) | +| `CPX-004-DCM` | DCM emergency rotation has no transition window; old revoked immediately; new delivered via fastest channel | +| `CPX-005-DCM` | DCM audits first credential value retrieval in all profiles; subsequent retrievals in standard+ | +| `CPX-006-DCM` | Actor deprovisioning triggers immediate revocation of all credentials issued to the actor (parallel with session revocation) | +| `CPX-007-DCM` | Entity decommissioning triggers revocation of all credentials scoped to entity before decommission confirmed | +| `CPX-008-DCM` | DCM rejects unbound credentials in fsi/sovereign; IP-bound or HSM-backed required | +| `CPX-009-DCM` | DCM declares algorithm + key_usage on every credential at issuance (standard+); enforces key_usage at validation | +| `CPX-010-DCM` | DCM fires idle detection at profile threshold; alert-only; auto-revocation after 2× threshold profile-configurable | +| `CPX-011-DCM` | DCM compliance overlays always tighten (never relax) base profile credential requirements | +| `CPX-012-DCM` | CPX-001-DCM applies in ALL profiles including homelab; no profile permits credential values in DCM stores | diff --git a/architecture/credentials-and-auth/provider-callback.md b/architecture/credentials-and-auth/provider-callback.md new file mode 100644 index 0000000..f340293 --- /dev/null +++ b/architecture/credentials-and-auth/provider-callback.md @@ -0,0 +1,420 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Provider Callback Authentication +Established: 2026-05-26 +Maps to: udlm/contracts/provider-callback-auth.md +--- + +# Provider Callback Authentication — mTLS + Interaction Credential + +> **Implements contracts defined in UDLM**: +> [udlm/contracts/provider-callback-auth.md](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-callback-auth.md). +> UDLM defines the mechanism-neutral two-layer authentication contract: any +> callback MUST be validated via two independent identity factors. DCM +> picks **mTLS as Layer 1** and **interaction credential as Layer 2** as +> its specific realization. A peer DCM realization could pick different +> layers (JWT + signed assertion, hardware-backed tokens, etc.) and remain +> UDLM-conformant — provided it declares its chosen mechanism via the +> schema-sharing protocol +> ([udlm/contracts/schema-sharing.md](https://github.com/croadfeldt/udlm/blob/main/contracts/schema-sharing.md)). + +--- + +## 1. The mechanism + +DCM realizes UDLM's two-layer auth contract with: + +| Layer | UDLM contract | DCM mechanism | +|---|---|---| +| 1 (transport identity) | "any peer MUST attest provider identity at registration via a verifiable mechanism" | **mTLS** — the provider presents its registered X.509 certificate; DCM validates the chain against the registered CA | +| 2 (operation authorization) | "every callback MUST present an independently-verifiable credential scoped to the operation" | **Interaction credential** — a `dcm_interaction` typed credential issued by the Credential Provider, presented as `Authorization: Bearer `, scoped to provider_uuid + allowed_operations | + +Both layers are required on every callback. mTLS alone proves identity but +not authorization; credential alone proves authorization but not identity. + +This mechanism is **DCM-specific**. A federation peer's DCM realization must +declare its chosen mechanism via the schema-sharing bundle so federated peers +can verify each other. + +--- + +## 2. Provider certificate storage and validation + +### 2.1 Certificate registration + +At provider registration: + +```yaml +provider_registration: + certificate: + pem: + ca_chain: + rotation_interval: P90D +``` + +DCM validates at registration: +- Certificate chain valid and trusted +- Certificate not in DCM's Credential Revocation Registry +- Certificate `CN` or `SAN` matches the declared `handle` +- Certificate `expires_at` not in the past + +DCM stores the certificate fingerprint. On every subsequent inbound +connection, DCM validates the presented certificate against the stored +fingerprint for this provider. + +### 2.2 mTLS enforcement at TLS handshake + +``` +Provider → DCM: + TLS ClientHello → ServerHello + DCM certificate + Provider verifies DCM certificate (DCM identity) + Provider sends its certificate + DCM validates: + 1. Certificate chain → registered CA for this provider + 2. Certificate fingerprint → matches stored fingerprint for provider_uuid + 3. Certificate not in Credential Revocation Registry + 4. Certificate expires_at not expired + Any failure → TLS handshake rejected; connection refused +``` + +### 2.3 Certificate rotation + +Providers rotate certificates on the declared `rotation_interval`. DCM fires +a `P14D` warning event when approaching expiry. During the rotation transition +window (P7D), DCM accepts both the current and new certificate +simultaneously. After the window, only the new certificate is accepted. + +--- + +## 3. Interaction credential issuance and management + +### 3.1 Provider callback credential + +At provider activation, DCM issues a `dcm_interaction` credential through +the Credential Provider: + +```yaml +provider_callback_credential: + credential_uuid: + credential_type: dcm_interaction + issued_to: + provider_uuid: + provider_handle: + issued_at: + expires_at: # profile-governed + operation_scope: + allowed_operations: + - realized_state_push + - capacity_report + - interim_status + - update_notification + - lifecycle_event + - notification_poll + non_transferable: true + bound_to_ip: # fsi/sovereign: required + revocation_check_url: +``` + +Presented as `Authorization: Bearer ` on all callback API calls. + +**Key property:** the credential is scoped to the `provider_uuid` — not to +specific entities or operations within that provider. Entity-level scope is +enforced separately (Section 6). + +### 3.2 Credential issuance lifecycle + +``` +Registration approved (provider status → ACTIVE) + ▼ API Gateway requests credential from Credential Provider: + │ credential_type: dcm_interaction + │ issued_to.provider_uuid: + │ allowed_operations: [] + │ expires_at: + ▼ Credential Provider issues credential + │ Stores credential_record in Credential Store + │ Returns credential_value (the bearer token) + ▼ DCM delivers credential to provider via activation response + │ POST /api/v1/admin/providers/{uuid}:approve + │ Response includes: credential_ref (UUID for retrieval) + ▼ Provider retrieves credential value via Credential Provider endpoint + │ GET {service_provider_endpoint}/credentials/{credential_ref}/value + │ (Requires the registration token used at initial registration — one-time bootstrap) + ▼ Provider stores credential securely and uses for all callback calls +``` + +### 3.3 Credential lifetime by profile — the one `dcm_interaction` ladder + +The single per-profile lifetime table for `dcm_interaction` credentials (cited by credentials.md §3.2; +internal component calls **narrow** to PT5M — internal-component-auth.md §4.1 — never a second table): + +| Profile | Lifetime | Rotation trigger | IP binding | +|---|---|---|---| +| homelab | PT8H | Pre-expiry P1H | No | +| dev | PT4H | Pre-expiry P30M | No | +| standard | PT1H | Pre-expiry PT10M | No | +| prod | PT30M | Pre-expiry PT5M | Optional | +| fsi | PT15M | Pre-expiry PT3M | Required | +| sovereign | PT15M + hardware attestation | Pre-expiry PT3M | Required; HSM-bound | + +### 3.4 Rotation protocol + +``` +PT{rotation_trigger} before credential expiry: + ▼ DCM initiates rotation + │ Requests new credential from Credential Provider + │ rotation_of: + │ same allowed_operations scope; new expires_at + ▼ Credential Provider issues new; old NOT yet revoked + ▼ DCM pushes rotation notification to provider + │ POST {provider_health_endpoint}/credential-rotation (if supported) + │ OR: credential.rotating event published to Message Bus + ▼ Transition window: both credentials valid + │ Duration: 50% of credential lifetime + ▼ Transition window closes; old credential revoked + │ Revocation event → all components update revocation cache +``` + +If the provider fails to pick up the new credential before the window closes, +the old credential is revoked and subsequent callbacks return `403 Forbidden` +with code `CREDENTIAL_EXPIRED`. The provider must re-register to recover. + +--- + +## 4. mTLS enforcement at callback endpoint + +The Provider Callback API endpoints all require Layer 1 (mTLS) at the TLS +termination point. If mTLS fails, the TLS handshake is rejected before +Layer 2 evaluation: + +| Endpoint | mTLS required | Credential required | +|---|---|---| +| `POST /api/v1/providers` (registration) | Yes | Bootstrap registration token | +| `POST /api/v1/providers/{provider_uuid}/capacity` | Yes | dcm_interaction credential | +| `PUT /api/v1/instances/{resource_id}/status` | Yes | dcm_interaction credential | +| `POST /api/v1/provider/entities/{entity_uuid}/status` | Yes | dcm_interaction credential | +| `POST /api/v1/provider/entities/{entity_uuid}/update-notification` | Yes | dcm_interaction credential | +| `GET /api/v1/provider/notifications/{notification_uuid}` | Yes | dcm_interaction credential | +| `POST /api/v1/instances/{resource_id}/events` | Yes | dcm_interaction credential | + +--- + +## 5. Validation logic at callback time + +Layer 2 validation runs after the TLS handshake completes: + +``` +1. Extract credential_value from Authorization: Bearer header + → Missing or malformed: 401 Unauthorized; MISSING_CREDENTIAL audit record + +2. Look up credential_record by credential_value hash + → Not found: 401 Unauthorized; CREDENTIAL_NOT_FOUND audit record + +3. Check credential_record.status is 'active' + → Revoked: 403 Forbidden; code: CREDENTIAL_REVOKED + → Expired: 403 Forbidden; code: CREDENTIAL_EXPIRED + +4. Check credential_record.expires_at > now + → Expired: 403 Forbidden; code: CREDENTIAL_EXPIRED + +5. Check credential_record.issued_to.provider_uuid matches: + a. The provider_uuid in the URL path (where applicable) + b. The mTLS certificate's registered provider (Layer 1 binding) + → Mismatch: 403 Forbidden; code: CREDENTIAL_SCOPE_VIOLATION + +6. Check operation_type for this endpoint is in allowed_operations + → Not in scope: 403 Forbidden; code: OPERATION_NOT_IN_SCOPE + +7. If bound_to_ip is set: verify client IP matches + → Mismatch: 403 Forbidden; code: IP_BINDING_VIOLATION +``` + +All failures write an audit record with credential_uuid, provider_uuid, +endpoint, and failure reason. + +After 5 consecutive `CREDENTIAL_SCOPE_VIOLATION` or `IP_BINDING_VIOLATION` +failures from the same provider within PT1H, DCM fires +`security.unsanctioned_provider_write` and notifies the platform admin +(urgency: critical). + +--- + +## 6. Entity authorization checks + +A valid credential proves the caller is the registered provider. It does NOT +prove the provider is authorized to act on a specific entity. Entity-level +authorization runs on each call. + +### 6.1 Resource ownership binding (realized_state_push, interim_status) + +``` +PUT /api/v1/instances/{resource_id}/status + +DCM checks: + 1. Look up Requested State record for resource_id + 2. Verify credential's provider_uuid matches provider_uuid in Requested State + 3. Verify entity is in a lifecycle state that permits this push + (PROVISIONING, UPDATING, or DECOMMISSIONING — not OPERATIONAL, not DECOMMISSIONED) + + → Mismatch on provider_uuid: 403; code: ENTITY_NOT_OWNED_BY_PROVIDER + → Wrong lifecycle state: 409; code: INVALID_LIFECYCLE_STATE_FOR_PUSH +``` + +A provider receiving a resource_id (e.g., by observing traffic) cannot push +realized state for an entity it was not dispatched to. + +### 6.2 Update notification binding + +``` +POST /api/v1/provider/entities/{entity_uuid}/update-notification + +DCM checks: + 1. Look up Realized State record for entity_uuid + 2. Verify credential's provider_uuid matches the provider_uuid in the most + recent Realized State + 3. Verify the provider's registration includes the update_capability + declared in the notification_type field + + → Provider not current owner: 403; code: ENTITY_NOT_OWNED_BY_PROVIDER + → Update type not declared: 403; code: UPDATE_TYPE_NOT_DECLARED +``` + +### 6.3 Lifecycle event binding + +``` +POST /api/v1/instances/{resource_id}/events + +DCM checks: + 1. Verify credential's provider_uuid matches provider on record for resource_id + 2. Verify resource is in an operational state (not DECOMMISSIONED) + 3. Verify event_type is in the standard event catalog + + → Provider not current owner: 403; code: ENTITY_NOT_OWNED_BY_PROVIDER + → Entity decommissioned: 409; code: ENTITY_DECOMMISSIONED + → Unknown event_type: 400; code: UNKNOWN_EVENT_TYPE +``` + +--- + +## 7. Registration token generation and validation + +The initial `POST /api/v1/providers` registration call cannot use a callback +credential (none exists yet). DCM uses a single-use registration token: + +```yaml +registration_token: + token_uuid: + token_value: + issued_at: + expires_at: # typically PT72H + scope: + provider_type_id: service_provider + provider_handle_pattern: "eu-west-*" + grants_auto_approval: true | false + used: false # single-use; set true after first successful use +``` + +Passed as `Authorization: Bearer ` on the initial registration +call. After first successful registration, marked `used: true`. Re-registration +requires a new token (per `PCA-006`). + +**mTLS still required for registration** — the provider must present the +certificate declared in the payload, proving private-key possession. + +### 7.1 Re-registration + +For re-registration (same `name`, updating version or capabilities), the +provider uses its active callback credential. Re-registration that changes +sovereignty declaration requires a new registration token (treated as a new +registration requiring new approval; `PCA-007`). + +--- + +## 8. Revocation enforcement + +### 8.1 Triggers + +| Trigger | What happens | +|---|---| +| Provider deregistered | All callback credentials for provider revoked immediately | +| 5+ scope violations in PT1H | Provider suspended; credential revoked; platform admin notified | +| Provider certificate expiry without rotation | Credential revoked at certificate expiry | +| Platform admin explicit revocation | Immediate; provider must re-register | +| Provider compromise suspected | Emergency revocation; Recovery Policy evaluates affected entities | + +### 8.2 Revocation cache + +DCM components maintain a local Credential Revocation Cache populated from +the Message Bus `credential.revoked` event stream: + +- Cache TTL matches the maximum credential lifetime for the active profile +- On cache miss: remote check against Credential Store (prevents stale cache + from accepting revoked credentials) +- Cache invalidation is immediate on `credential.revoked` event receipt + (not TTL-based) + +The revocation cache ensures revocation propagates within PT30S even without +a cache miss triggering a remote lookup. + +--- + +## 9. Emergency revocation + +``` +Platform admin triggers emergency revocation: + POST /api/v1/admin/providers/{provider_uuid}/revoke-credential + { reason: , suspend_provider: true | false } + + ▼ DCM revokes credential immediately + │ credential_record.status → revoked + │ Revocation event → Message Bus + │ All DCM components update revocation cache (within PT30S) + ▼ If suspend_provider: true + │ Provider status → SUSPENDED + │ New requests not routed to this provider + │ Active realizations enter PENDING_REVIEW state + ▼ Recovery Policy evaluates affected entities: + Entities currently hosted at provider: notify Tenant owners + In-progress operations: depends on Recovery Policy profile +``` + +--- + +## 10. Schema sharing declaration + +Per the UDLM compatibility model, DCM declares its chosen mechanism in its +schema bundle so federated peers can verify and interoperate: + +```yaml +# Excerpt from DCM's schema bundle published per udlm/contracts/schema-sharing.md +provider_callback_auth: + contract_version: 1.0 + layer_1_mechanism: mtls + layer_2_mechanism: interaction_credential + layer_1_protocol_refs: + - rfc_5280 # X.509 PKI + - rfc_8446 # TLS 1.3 + layer_2_credential_type: dcm_interaction + layer_2_format: bearer_token + layer_2_transport: Authorization HTTP header +``` + +A federated peer that picks a different mechanism (e.g., JWT + signed +assertion) declares its mechanism similarly; cross-peer federation +negotiation includes mechanism compatibility checks. + +--- + +## 11. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `PCA-001-DCM` | All DCM provider calls present both valid mTLS (Layer 1) and valid interaction credential (Layer 2); neither alone is sufficient | +| `PCA-002-DCM` | Interaction credentials are scoped to provider_uuid; cannot act on entities at other providers | +| `PCA-003-DCM` | Entity-level authorization checked on every realized_state_push, update_notification, and lifecycle_event call independent of credential validity | +| `PCA-004-DCM` | Five consecutive scope or IP binding violations within PT1H triggers automatic provider suspension and admin notification | +| `PCA-005-DCM` | Interaction credentials issued by the Credential Management Service — the authoritative source for all issuance, rotation, and revocation | +| `PCA-006-DCM` | Registration tokens are single-use; a token used once is permanently invalidated regardless of expires_at | +| `PCA-007-DCM` | Re-registration changing sovereignty declaration requires a new registration token and new approval pipeline; version/capability updates do not | +| `PCA-008-DCM` | Interaction credentials must be rotated before expiry; expired without rotation → CREDENTIAL_EXPIRED; provider must obtain new via platform admin | +| `PCA-009-DCM` | For fsi/sovereign, interaction credentials are IP-bound; mismatched IP → rejected regardless of validity | +| `PCA-010-DCM` | All inbound provider calls — including rejected — produce an audit record; no silent failures | diff --git a/architecture/data-policy-boundary.md b/architecture/data-policy-boundary.md new file mode 100644 index 0000000..93a00e2 --- /dev/null +++ b/architecture/data-policy-boundary.md @@ -0,0 +1,58 @@ +# DCM ↔ UDLM — the Data / Policy responsibility boundary + +DCM and UDLM are two domains separated by one **responsibility boundary** — a service / contract seam. +Getting it right is what keeps the data model portable, auditable, and sovereign, and keeps DCM's logic +where it belongs. This is the DCM-side statement of the boundary defined in UDLM's +`design-principles/core-tenets.md`. + +| Domain | Owner | Responsibility | +|---|---|---| +| **Data** | UDLM | Custody of data through its lifecycle: identity, the four states, versioning, relationships, provenance, audit records, sovereignty fields. *Hold, move, reference, version, audit.* | +| **Policy** | **DCM** | **Application of policy** — transformation, enrichment, derivation, decision, governance. *Compute, derive, evaluate, decide, enforce.* | + +**UDLM defines the contracts (Data, Provider, and Policy); DCM is where Policy is *applied*.** UDLM +carries the data policy acts on and records the decisions policy makes; it never executes logic. DCM +never becomes the system of record for lifecycle state; it applies logic over UDLM data. + +## What DCM owns (the verbs) +- **Assembly** — Intent → Requested: merge Layers (data) under Policy (logic), recording per-field + provenance back into the UDLM record. +- **Policy evaluation** — Validation Policy, Transformation; the **Governance Matrix** and + **sovereignty/accreditation/trust** decisions; placement. +- **Dependency-graph application** — validate the DAG (`RDG-001`), order forward execution, run + compensation in reverse, schedule rehydration in dependency order. +- **Realization** — dispatch to Providers, collect Realized state, run Discovery → drift, resolve + conflicts (field ownership / server-side apply). +- **Audit production** — write the synchronous, append-only, Merkle-chained log (`AUD-001/002`). +- **Adopted-standard runtime** — for externally-adopted standards (FOCUS, OpenCost, OSCAL, SCIM), the + data carries identity + version pins; **DCM negotiates, translates, and enforces standard versions** + and records the effective version as provenance. Full requirements: `adopted-standards-dcm.md` + (`ADS-001…010`); the Data-side contract is UDLM `design-principles/adopted-standards.md` (tenet T5). +- **Enforcement** — reject changes to `immutable`/createOnly fields; reject sovereignty-boundary + violations; reject non-conformant data at the seam. + +## What DCM must NOT do (boundary violations) +- Become the durable system of record for lifecycle state — that is UDLM data. +- **Absorb an adopted standard's schema** into DCM persistence, or become the system of record for + adopted data (cost, compliance, identity). That data conforms to its standard and is referenced via an + Information Provider, lookup-only (`adopted-standards-dcm.md`, `ADS-007`). +- Push executable logic *into* the portable data model. UDLM carries **no embedded expression + language**; all transformation/enrichment is DCM policy. This is what makes the contract layer + **deterministic and reproducible** (the precondition for tamper-evident audit and sovereignty): + determinism is structural in the data because the evaluator lives only in DCM. +- Let a runtime decision that legitimately depends on live state (e.g. placement by current capacity) + silently alter the contract. Such decisions are **recorded as decisions in the audit log**, never + written back as if they were the reproducible definition. + +## Why the boundary holds the line on the four pillars +- **Audit:** data is immutable + version-pinned (`$id`); DCM produces the proofs. Reproducible forever. +- **Observability:** UDLM declares relationships + typed outputs; DCM reconciles observed vs declared. +- **Dependency graph:** UDLM carries typed edges; DCM constructs/validates/orders the DAG. +- **Sovereignty:** UDLM marks sovereignty fields `immutable` and bundles offline closures; DCM enforces + the Governance Matrix. Because no expression rides in the data, nothing can route around the boundary. + +> Test: a **noun** (record, contract, edge, marker, pin) is UDLM's. A **verb** (assemble, evaluate, +> decide, enforce, transform, resolve) is DCM's. + +See UDLM `design-principles/core-tenets.md` (T1–T4) and `cross-cutting-requirements.md` for the +substrate side; the Resource Type Registry (`registry/`) is the concrete Data-domain contract DCM applies. diff --git a/architecture/dcm-pattern-catalog-overlay.md b/architecture/dcm-pattern-catalog-overlay.md new file mode 100644 index 0000000..2e5cd02 --- /dev/null +++ b/architecture/dcm-pattern-catalog-overlay.md @@ -0,0 +1,408 @@ +# Deployment Pattern Catalog — How It Overlays on DCM + +**Date:** April 2026 +**Status:** Architecture Overlay — For Team Discussion + +--- + +## What a Deployment Pattern Is + +A deployment pattern is a **reusable, provider-agnostic blueprint** that defines a collection of resources, their dependencies, their runtime wiring, and their operational policies — together delivering a service that no single provider offers. + +Examples: + +| Pattern | Constituents | What it delivers | +|---------|-------------|-----------------| +| **Standard Web Application** | 2 app server VMs, 1 PostgreSQL DB, 1 load balancer, 1 network segment, 3 DNS records | A production web app with HA, monitoring, and DNS | +| **Secure Data Pipeline** | 1 Kafka cluster, 2 worker VMs, 1 S3-compatible store, 1 network policy, 1 encryption key | An encrypted ingest pipeline with data residency controls | +| **Developer Sandbox** | 1 VM, 1 namespace, 1 ephemeral DB, 1 port forward | A disposable dev environment with TTL auto-cleanup | +| **Regulated Database Service** | 1 PostgreSQL cluster (HA), 1 backup schedule, 1 encryption key, 1 audit log sink, 2 DNS records | A database meeting FSI data handling requirements | +| **Edge Compute Node** | 1 bare metal host, 1 MicroShift cluster, 1 VPN tunnel, 1 monitoring agent, 1 cert | A self-contained edge node with central management | + +The key property: **no single provider owns the pattern.** The PostgreSQL DB might come from one provider, the VMs from another, the load balancer from a third, and the DNS records from a fourth. The pattern defines *what* is needed and *how the pieces connect* — DCM figures out *who* provides each piece. + +--- + +## The Layering Model + +``` +┌─────────────────────────────────────────────────────────┐ +│ PATTERN CATALOG │ +│ Curated library of reusable deployment blueprints │ +│ "Standard Web App" · "Data Pipeline" · "Dev Sandbox" │ +│ │ +│ Authored by: Platform Engineers │ +│ Consumed by: Consumer Developers │ +│ Stored in: Resource Type Registry (composite types) │ +├─────────────────────────────────────────────────────────┤ +│ SERVICE CATALOG │ +│ Provider-specific offerings of patterns + atomic items │ +│ "EU-WEST Web App — Standard" · "APAC VM — Large" │ +│ │ +│ Populated by: Providers (atomic) + Patterns (composite) │ +│ Consumed by: Consumer Developers │ +├─────────────────────────────────────────────────────────┤ +│ DCM CONTROL PLANE │ +│ Decompose → Policy → Placement → Dispatch → Audit │ +│ │ +│ Processes: Both atomic requests and pattern requests │ +│ Each constituent → full pipeline independently │ +├─────────────────────────────────────────────────────────┤ +│ SERVICE PROVIDERS │ +│ VM · Network · Database · DNS · Storage · Container │ +│ │ +│ Fulfill: Individual constituents of a pattern │ +│ Report back: Realized state per constituent │ +└─────────────────────────────────────────────────────────┘ +``` + +**The Pattern Catalog is not a new architectural component.** It is a curated view of the Resource Type Registry filtered to composite resource types. DCM already has all the machinery to execute patterns — the composite service model, dependency graphs, binding fields, and constituent dispatch. What the Pattern Catalog adds is the **curation and consumer experience layer** on top of that machinery. + +--- + +## How a Pattern Maps to DCM Constructs + +A single deployment pattern maps to these existing DCM concepts: + +| Pattern concept | DCM construct | Where it lives | +|----------------|--------------|----------------| +| The pattern itself | Composite Resource Type Specification | Resource Type Registry | +| The constituents | Resource Type references with dependency declarations | `constituents[]` in the composite spec | +| How pieces connect | Binding fields — runtime values from one constituent injected into another | `binding_fields[]` on dependent constituents | +| What the consumer fills in | Parameterized fields exposed at the pattern level | `fields_from_parent[]` mapping pattern params → constituent fields | +| Who provides each piece | `provided_by: external` (DCM places) or `provided_by: self` (composite service handles) | Per-constituent declaration | +| What happens on failure | Lifecycle policy on the composite spec | `on_constituent_failure: rollback_all | continue_degraded | notify` | +| Operational policies | Standard DCM policies scoped to the pattern's resource type | Policy match on `resource_type = ApplicationStack.WebApp` | + +--- + +## Concrete Example: "Standard Web Application" Pattern + +### Pattern Definition (authored by Platform Engineer) + +```yaml +# Registered in Resource Type Registry as a composite resource type +resource_type: ApplicationStack.WebApp +version: "1.0.0" +entity_type: composite_resource +description: "Production web application with database, app servers, load balancer, and DNS" + +# What the consumer fills in when requesting this pattern +parameters: + app_name: + type: string + required: true + description: "Application name — used for hostnames, DNS, and resource tagging" + environment: + type: string + required: true + constraint: { layer_reference: "environment" } # values governed by environment layers + db_engine: + type: string + required: true + default: postgresql + constraint: { enum: [postgresql, mysql, mariadb] } + db_storage_gb: + type: integer + required: true + default: 50 + constraint: { min: 10, max: 1000 } + app_replicas: + type: integer + required: true + default: 2 + constraint: { min: 1, max: 10 } + expose_public: + type: boolean + default: false + description: "Whether to create a public DNS record and public LB listener" + +# The constituents and how they connect +constituents: + - name: network_segment + resource_type: Network.Segment + provided_by: external + depends_on: [] + required_for_delivery: required + fields_from_parent: + - source: "environment" + target: "environment" + - source: "app_name" + target: "segment_name_prefix" + + - name: database + resource_type: Database.Managed + provided_by: external + depends_on: [network_segment] + required_for_delivery: required + binding_fields: + - source: "network_segment.subnet_cidr" + target: "database.network_cidr" + - source: "network_segment.security_group_id" + target: "database.security_group_id" + fields_from_parent: + - source: "db_engine" + target: "engine" + - source: "db_storage_gb" + target: "storage_gb" + - source: "environment" + target: "environment" + + - name: app_server + resource_type: Compute.VirtualMachine + provided_by: external + depends_on: [database, network_segment] + required_for_delivery: required + binding_fields: + - source: "database.ip_address" + target: "app_server.config.db_host" + - source: "database.port" + target: "app_server.config.db_port" + - source: "database.credentials_ref" + target: "app_server.config.db_credentials_ref" + - source: "network_segment.subnet_cidr" + target: "app_server.network_cidr" + fields_from_parent: + - source: "app_name" + target: "hostname_prefix" + - source: "app_replicas" + target: "replicas" + - source: "environment" + target: "environment" + + - name: load_balancer + resource_type: Network.LoadBalancer + provided_by: external + depends_on: [app_server] + required_for_delivery: required + binding_fields: + - source: "app_server.ip_addresses" + target: "load_balancer.backend_pool" + - source: "app_server.port" + target: "load_balancer.backend_port" + fields_from_parent: + - source: "app_name" + target: "lb_name" + - source: "expose_public" + target: "public_listener" + + - name: dns_internal + resource_type: DNS.Record + provided_by: external + depends_on: [load_balancer] + required_for_delivery: partial + binding_fields: + - source: "load_balancer.vip_address" + target: "dns_internal.target_address" + fields_from_parent: + - source: "app_name" + target: "hostname" + + - name: dns_public + resource_type: DNS.Record + provided_by: external + depends_on: [load_balancer] + required_for_delivery: optional + condition: "parent.expose_public == true" # only created if consumer wants public access + binding_fields: + - source: "load_balancer.public_vip_address" + target: "dns_public.target_address" + fields_from_parent: + - source: "app_name" + target: "hostname" + +lifecycle_policy: + on_constituent_failure: rollback_all + decommission_order: reverse_dependency # DNS first, then LB, then app, then DB, then network +``` + +### What the Consumer Sees + +The consumer browses the service catalog, finds "Standard Web Application," and submits: + +```json +POST /api/v1/requests +{ + "catalog_item_uuid": "webapp-standard-uuid", + "fields": { + "app_name": "pet-clinic", + "environment": "production", + "db_engine": "postgresql", + "db_storage_gb": 100, + "app_replicas": 3, + "expose_public": true + } +} +``` + +Six fields. The consumer has no idea that this will produce 6 resources across potentially 4 different providers. + +### What DCM Does + +``` +1. Intent captured — consumer's 6 fields stored + +2. Pattern decomposed — DCM reads the composite resource type spec + → 6 constituents identified + → Dependency graph resolved: + Network Segment (no deps) + ├── Database (needs network) + │ └── App Server (needs DB + network) + │ └── Load Balancer (needs app server) + │ ├── DNS Internal (needs LB) + │ └── DNS Public (needs LB, conditional on expose_public=true) + +3. Each constituent gets its own full pipeline: + → Layer assembly (datacenter, environment, tenant, compliance layers merge in) + → Policy evaluation (sovereignty, sizing, naming, monitoring — per constituent) + → Placement (each constituent placed independently, all honoring sovereignty) + +4. Execution in dependency order: + + Round 1: Network Segment + → Placed with EU-WEST network provider + → Realized: subnet_cidr=10.5.0.0/24, security_group_id=sg-abc123 + + Round 2: Database (binding fields inject network values) + → config.network_cidr = 10.5.0.0/24 (from network_segment) + → config.security_group_id = sg-abc123 (from network_segment) + → Placed with EU-WEST database provider + → Realized: ip_address=10.5.0.50, port=5432, credentials_ref=vault:secret/pet-clinic-db + + Round 3: App Server (binding fields inject DB + network values) + → config.db_host = 10.5.0.50 (from database) + → config.db_port = 5432 (from database) + → config.db_credentials_ref = vault:secret/pet-clinic-db (from database) + → config.network_cidr = 10.5.0.0/24 (from network_segment) + → Placed with EU-WEST compute provider, 3 replicas + → Realized: ip_addresses=[10.5.0.10, 10.5.0.11, 10.5.0.12], port=8080 + + Round 4: Load Balancer (binding fields inject app server IPs) + → config.backend_pool = [10.5.0.10, 10.5.0.11, 10.5.0.12] (from app_server) + → config.backend_port = 8080 (from app_server) + → Placed with EU-WEST network provider + → Realized: vip_address=10.5.0.100, public_vip_address=203.0.113.50 + + Round 5: DNS records (binding fields inject LB addresses) + → DNS Internal: pet-clinic.internal → 10.5.0.100 + → DNS Public: pet-clinic.example.com → 203.0.113.50 (created because expose_public=true) + +5. All constituents realized → composite entity status: OPERATIONAL + → Consumer receives: entity_uuid, status, and connection details + → 6 entities, each with independent audit trail, drift detection, lifecycle +``` + +### What Policies See + +Policies don't need special awareness of patterns. Each constituent is a standard DCM request with a standard resource type. Existing policies apply naturally: + +| Policy | Fires on which constituent | What it does | +|--------|---------------------------|-------------| +| Sovereignty Validation Policy (compliance) | All 6 | Ensures all constituents land in EU-WEST | +| VM sizing limits | App Server only | Validates replica count and VM size within tenant tier | +| DB storage limits | Database only | Validates db_storage_gb within allowed range | +| Network naming | Network Segment, DNS | Enforces naming conventions | +| Monitoring injection | App Server, Database | Injects monitoring agent config | +| Backup policy | Database | Injects backup schedule based on environment | + +**No new policy types are needed.** The pattern decomposes into standard resource types, and standard policies match on those types. + +--- + +## How the Pattern Catalog Surfaces in DCM + +### In the Resource Type Registry + +Patterns are compound Resource Type Specifications with `entity_type: composite_resource`. They live alongside atomic resource types in the same registry: + +``` +Resource Type Registry +├── Compute.VirtualMachine (atomic) +├── Network.Segment (atomic) +├── Database.Managed (atomic) +├── DNS.Record (atomic) +├── Network.LoadBalancer (atomic) +├── ApplicationStack.WebApp (composite ← this is a pattern) +├── ApplicationStack.DataPipeline (composite ← this is a pattern) +├── Environment.DevSandbox (composite ← this is a pattern) +└── Platform.EdgeNode (composite ← this is a pattern) +``` + +### In the Service Catalog + +Provider catalog items can reference either atomic or composite resource types. For patterns, the catalog item represents the pattern itself — the consumer requests the pattern, not the individual constituents: + +``` +Service Catalog +├── "EU-WEST VM — Standard" → Compute.VirtualMachine (atomic, provider-specific) +├── "EU-WEST VM — Large" → Compute.VirtualMachine (atomic, provider-specific) +├── "Standard Web Application" → ApplicationStack.WebApp (composite, multi-provider) +├── "Secure Data Pipeline" → ApplicationStack.DataPipeline (composite, multi-provider) +└── "Developer Sandbox" → Environment.DevSandbox (composite, multi-provider) +``` + +### In the Consumer API + +No API changes. The consumer requests a catalog item. Whether it's atomic or compound is transparent — the same `POST /api/v1/requests` endpoint handles both. The response includes constituent status for composite requests. + +### In RHDH + +The RHDH catalog page shows patterns alongside atomic offerings. Patterns have a "Components" view showing the constituent resources, their dependency graph, and (after realization) the binding field values. Platform engineers use RHDH scaffolding templates to create new patterns. + +--- + +## Who Authors Patterns vs Who Consumes Them + +| Role | What they do with patterns | +|------|--------------------------| +| **Platform Engineer** | Authors pattern definitions (composite resource type specs). Defines constituents, dependencies, binding fields, parameters, lifecycle policies. Registers patterns in the Resource Type Registry. Creates service catalog items for patterns. | +| **Policy/Compliance Owner** | Writes policies that apply to pattern constituents. Does not need to know about patterns specifically — policies match on resource types, which patterns decompose into. May also write pattern-level policies (e.g., "all ApplicationStack.* types require monitoring on every constituent"). | +| **Consumer Developer** | Browses the catalog, selects a pattern, fills in parameters, submits. Sees aggregate status. Can drill into constituent detail. Does not need to understand the decomposition. | +| **Infrastructure Operator** | Provides the atomic services that patterns compose. Registers providers for Compute, Network, Database, DNS — not for the pattern itself. | + +--- + +## Pattern Lifecycle + +Patterns follow the standard DCM artifact lifecycle: + +``` +developing → proposed → active → deprecated → retired +``` + +**Versioning:** Patterns are versioned (`ApplicationStack.WebApp v1.0.0`). A new version can add optional constituents, change defaults, or add new binding fields without breaking existing deployments. Removing a required constituent is a major version bump. + +**Deprecation:** When a pattern version is deprecated, existing realized instances continue operating. New requests are redirected to the successor version. Consumers are notified of the deprecation timeline. + +**Pattern evolution:** Adding an optional constituent (e.g., adding a cache layer to the web app pattern) is a minor version bump. Existing deployments don't gain the new constituent automatically — but new requests do. Consumers with existing deployments can opt in via an update request. + +--- + +## Interaction with Other DCM Features + +| Feature | How it interacts with patterns | +|---------|-------------------------------| +| **Drift detection** | Each constituent is independently discoverable. Drift on any constituent is detected and attributed to that constituent — not to the pattern as a whole. | +| **Decommission** | Pattern decommission triggers reverse-dependency-order teardown of all constituents. Consumer can also decommission individual constituents (e.g., remove the public DNS record) without tearing down the pattern. | +| **Rehydration** | Pattern rehydration rebuilds all constituents in dependency order with current policies. Binding fields resolve against newly realized values. | +| **Sovereignty** | Every constituent is independently sovereignty-checked. A pattern cannot span sovereignty zones unless every constituent passes its own sovereignty policy. | +| **Cost estimation** | Pattern cost is the sum of constituent costs. Each constituent's cost comes from its provider catalog item. | +| **Audit** | Each constituent has its own Merkle audit trail. The pattern entity has a composite audit record linking all constituent entity_uuids. | +| **Override** | A policy block on any constituent blocks the entire pattern. The consumer resolves the block for that specific constituent — modify, override, cancel, or escalate to the responsible policy domain owner. | +| **Federation** | Pattern constituents can be placed across DCM instances. The network segment might be local while the database is federated to a remote DCM with a specialized database provider. | + +--- + +## New Use Case for Requirements Document + +### UC-100: Deploy a Resource Pattern from the Pattern Catalog + +A consumer browses the Pattern Catalog section of the service catalog and selects "Standard Web Application." The catalog shows the pattern's components (network, database, app servers, load balancer, DNS), the parameters the consumer needs to provide, the dependency graph, and estimated cost. + +The consumer fills in 6 parameters (app_name, environment, db_engine, db_storage_gb, app_replicas, expose_public) and submits. DCM decomposes the pattern into 6 constituent resources, resolves the dependency graph, and processes each constituent through the full pipeline — layer assembly, policy evaluation, placement, dispatch — independently. Binding fields inject runtime values (IP addresses, connection strings, credentials references) from realized constituents into dependent ones. + +The consumer monitors aggregate progress ("3 of 6 constituents realized") and can drill into individual constituent status. On completion, the consumer has a fully wired application environment: database with data, app servers connected to the database, load balancer distributing traffic, and DNS records resolving. + +If any required constituent fails (e.g., the database provider reports an error), the pattern's lifecycle policy determines the response — rollback all realized constituents, continue in degraded mode, or notify and hold for manual intervention. + +**Success criteria:** Single request produces a complete, wired application environment. Runtime values flow correctly between constituents via binding fields. Each constituent is independently managed (own entity_uuid, audit trail, drift detection, lifecycle). Decommission reverses dependency order. Sovereignty enforced per-constituent. No new control plane services, policy types, or API endpoints required — patterns use existing DCM machinery. diff --git a/architecture/dcm-platform-requirements.md b/architecture/dcm-platform-requirements.md new file mode 100644 index 0000000..8e3c730 --- /dev/null +++ b/architecture/dcm-platform-requirements.md @@ -0,0 +1,485 @@ +# DCM PLATFORM REQUIREMENTS DOCUMENT + +**Data Center Management — Sovereign Cloud Framework** + +*Requirements for the DCM control plane, data model, and provider ecosystem enabling enterprise organizations to manage infrastructure lifecycle with policy-as-code governance, tamper-evident audit, and multi-provider abstraction.* + +**Date:** April 2026 +**Author:** Chris, Principal Architect +**Project:** Red Hat FlightPath — github.com/dcm-project + +--- + +# Context + +DCM addresses a fundamental gap in enterprise infrastructure management: on-premises data centers lack the unified control plane that public clouds provide as table stakes. Organizations operating large-scale, multi-platform infrastructure spend disproportionate engineering effort stitching together disparate automation tools, enforcing governance manually, and reconciling inventory that diverges silently between intended and actual state. + +**Target Organizations** + +- Enterprise data center operators running heterogeneous infrastructure (VM, container, network, storage) across multiple platforms +- Organizations with regulatory requirements (financial services, government, healthcare) requiring provable audit trails and data residency enforcement +- Platform engineering teams seeking to offer self-service infrastructure to development teams with built-in governance + +**DCM's Value Proposition** + +DCM is the management plane that sits above provisioning tools (Ansible, Terraform, Kubernetes operators) and governs what gets requested, approved, built, owned, and decommissioned. It provides: a unified data model and API across all infrastructure platforms; policy-as-code enforcement on every request before provisioning; full lifecycle management from request through decommission with tamper-evident audit; and a provider abstraction with capability discovery that makes any infrastructure platform consumable through the same interface. + +**Delivery Context** + +DCM is an open-source project under the Red Hat FlightPath initiative, licensed under Apache 2.0. The engineering team spans Red Hat and community contributors. The initial milestone is a summit demonstration showing end-to-end provisioning with policy enforcement and audit verification. + +--- + +# Executive Summary + +DCM requires a control plane that enables: declarative lifecycle management of infrastructure resources across heterogeneous providers; policy-as-code governance enforced on every request with configurable override mechanisms; a provider abstraction that makes any infrastructure platform — VM, container, network, storage — consumable through the same interface and data model; tamper-evident audit with mathematical provability (Merkle tree) at configurable granularity; sovereignty and data residency enforcement as first-class concepts, not afterthoughts; and a consumer experience that abstracts infrastructure complexity behind a service catalog. + +The platform is designed as a management plane — it orchestrates lifecycle, enforces governance, and maintains state. It does not provision infrastructure directly. Provisioning is delegated to service providers (OpenStack, KubeVirt, ACM, Ansible, Terraform) that implement DCM's provider contract. + +--- + +# Personas + +## Consumer Developer + +A developer or application owner who requests and manages infrastructure resources through the service catalog. Interacts with DCM via API, Web UI (RHDH), or Git PR. Does not need to understand which provider, datacenter, or network configuration fulfills their request — DCM handles that. + +- **Key activities:** Browse service catalog, submit resource requests, track request status, handle policy blocks, manage running resources, view cost estimates, trigger decommission + +## Platform Engineer + +Defines the organizational infrastructure standards that DCM enforces. Authors resource type specifications, data layers (datacenter configs, environment defaults, tenant overrides), policies, and composite resource type specifications (three-tier applications, data pipelines). + +- **Key activities:** Define resource types and catalog items, author data layers, write and test policies (shadow mode), create composite service definitions, manage the resource type registry + +## Infrastructure Operator + +Manages the physical and virtual infrastructure that DCM's providers abstract. Implements and operates service providers, manages provider health and capacity, handles accreditation and sovereignty declarations, and responds to drift remediation alerts. + +- **Key activities:** Implement and register service providers, manage provider health and capacity reporting, handle naturalization/denaturalization, respond to discovery and drift events + +## Policy and Compliance Owner + +Defines and manages the governance rules that DCM enforces. Authors validation policies (allow/deny), sovereignty constraints, and override approval rules. Reviews audit trails and compliance reports. + +- **Key activities:** Author and activate policies, configure policy profiles (homelab through sovereign), review override requests (dual-approval), verify audit integrity, manage compliance rescans + +## Platform Administrator + +Deploys and operates the DCM control plane itself. Manages the 9 control plane services, database, tenant configuration, auth provider setup, and platform health monitoring. + +- **Key activities:** Deploy and upgrade DCM, onboard tenants, configure auth providers, manage profiles and deployment configuration, monitor platform health, rotate credentials + +--- + +# Use Cases + +The following use cases capture DCM's core requirements, organized by lifecycle phase and prioritized for the summit demonstration and subsequent delivery phases. + +| **ID** | **Use Case** | **Priority** | **Personas** | +|--------|-------------|-------------|-------------| +| **UC-001** | Deploy DCM Control Plane | **P0** | Administrator | +| **UC-002** | Register Service Providers | **P0** | Operator, Administrator | +| **UC-003** | Populate Service Catalog | **P0** | Platform Engineer | +| **UC-004** | Configure Organizational Policies | **P0** | Policy Owner, Platform Engineer | +| **UC-005** | Configure Deployment Profiles | **P1** | Administrator | +| **UC-010** | Provision a Virtual Machine | **P0** | Consumer | +| **UC-011** | Provision a Three-Tier Application | **P0** | Consumer | +| **UC-012** | Handle a Policy-Blocked Request | **P0** | Consumer | +| **UC-013** | View Cost Before Committing | **P1** | Consumer | +| **UC-014** | Track Request Progress | **P0** | Consumer | +| **UC-020** | Update a Running Resource | **P0** | Consumer | +| **UC-021** | Scale Resource Capacity | **P1** | Consumer | +| **UC-022** | Detect and Remediate Drift | **P0** | Operator, Consumer | +| **UC-023** | Transfer Resource Ownership | **P2** | Consumer, Administrator | +| **UC-024** | Manage Subscription Lifecycle | **P2** | Consumer, Administrator | +| **UC-030** | Rebuild from Stored State (DR) | **P1** | Operator, Administrator | +| **UC-031** | Migrate Resources to New Provider | **P2** | Operator, Administrator | +| **UC-040** | Decommission a Resource | **P0** | Consumer | +| **UC-050** | Enforce Sovereignty and Data Residency | **P0** | Policy Owner | +| **UC-051** | Rescan Existing Resources Against New Policy | **P1** | Policy Owner | +| **UC-052** | Prove Pipeline Integrity to Auditor | **P0** | Policy Owner | +| **UC-053** | Prove Override Authorization | **P1** | Policy Owner | +| **UC-060** | Cross-Instance Placement (Federation) | **P2** | Administrator | +| **UC-070** | Monitor DCM Platform Health | **P1** | Administrator | +| **UC-071** | Onboard a New Tenant | **P0** | Administrator | +| **UC-072** | Validate Provider Accreditation | **P2** | Administrator, Policy Owner | +| **UC-080** | Scoped Interaction Credentials (Zero Trust) | **P1** | Administrator | +| **UC-081** | Session Management and Auth Failover | **P0** | Consumer, Administrator | +| **UC-090** | ITSM Integration (ServiceNow) | **P2** | Administrator, Operator | +| **UC-091** | Git-Based Request Ingress | **P1** | Consumer, Platform Engineer | +| **UC-100** | Deploy a Resource Pattern from Pattern Catalog | **P0** | Consumer, Platform Engineer | + +--- + +## Day 0 — Platform Setup + +### UC-001: Deploy DCM Control Plane + +An administrator deploys the DCM control plane to an OpenShift cluster. PostgreSQL is the only required infrastructure dependency. The deployment includes 9 control plane services (API Gateway, Catalog Manager, Request Processor, Policy Engine, Placement Engine, Request Orchestrator, Audit Service, Discovery Service, Provider Manager), the PostgreSQL database with 18 tables and RLS tenant isolation, and initial configuration (system admin account, default profile, built-in auth provider). After deployment, the administrator verifies all services are healthy and the API gateway is reachable. + +**Success criteria:** All 9 services healthy. API gateway responds to requests. Admin can authenticate and access the admin API. No external dependencies required beyond PostgreSQL. + +### UC-002: Register Service Providers + +An infrastructure operator registers a service provider (e.g., OpenStack Nova compute) with DCM. The registration includes: provider type (service, information, meta, auth, peer_dcm, or process), capability declaration (resource types supported, lifecycle operations), health endpoint URL, sovereignty zone declarations (which jurisdictions the provider operates in), accreditation declarations (compliance certifications), and callback authentication configuration. DCM validates the registration, probes the health endpoint, and activates the provider. The provider is now available for placement consideration. + +**Success criteria:** Provider registered and active. Health check passes. Resource types appear in the registry. Provider is eligible for placement queries. + +### UC-003: Populate Service Catalog + +A platform engineer defines resource types and catalog items. A resource type specification (e.g., `Compute.VirtualMachine v1.0.0`) declares the vendor-neutral field schema (cpu_count, memory_gb, os_family), constraints (min/max values, allowed enums), type-level dependencies (every VM requires a Network.IPAddress), and lifecycle rules. A provider catalog item (e.g., "EU-WEST OpenStack VM — Standard") ties a resource type spec to a specific provider with pricing, SLAs, and availability. The platform engineer also creates data layers: datacenter layers (location, network ranges), environment layers (production vs dev defaults), tenant layers (team-specific overrides), and compliance layers (EU data residency requirements). + +**Success criteria:** Consumers can browse the catalog filtered by RBAC. Resource type schemas are queryable. Dependency graphs are visible. Data layers resolve correctly during assembly. + +### UC-004: Configure Organizational Policies + +A policy and compliance owner authors the baseline policy set: a sovereignty validation policy (enforcement_class: compliance) that blocks resources from deploying outside designated zones (hard enforcement); a sizing validation policy that enforces CPU and memory limits per tenant tier; a naming transformation policy that auto-generates standardized hostnames; a monitoring transformation policy that injects the organization's monitoring agent into every production resource; and a cost validation policy (enforcement_class: compliance) that blocks requests exceeding budget thresholds. Each policy is first deployed in **shadow mode** — it evaluates against real traffic and logs results without blocking requests. After validation, the policy is promoted to active. Policies use the Gatekeeper ConstraintTemplate pattern: reusable Rego logic with parameterized instances. + +**Success criteria:** Policies evaluate correctly in shadow mode. No false positives on legitimate requests. Activation enforces the policy on all matching requests. Audit records produced for every evaluation. + +### UC-005: Configure Deployment Profiles + +An administrator configures deployment profiles that govern operational behavior per environment: + +| Profile | Audit Granularity | Override Timeout | Policy Minimums | Use | +|---------|-------------------|-----------------|----------------|-----| +| homelab | stage | 24h | none | Homelab, evaluation | +| dev | stage | 4h | naming, tagging | Development | +| standard | mutation | 8h | all core policies | Production | +| fsi | field | 48h | sovereignty + all core | Financial services | +| sovereign | field (synchronous) | 72h | all policies on all ops | Government, classified | + +**Success criteria:** Profile assignment per tenant governs audit depth, override windows, and policy enforcement minimums. Profile changes take effect on next request. + +--- + +## Day 1 — Consumer Operations + +### UC-010: Provision a Virtual Machine + +A consumer developer browses the service catalog, selects "Virtual Machine — Standard," and submits a request with 6 fields: cpu_count, memory_gb, storage_gb, os_family, environment, and name. The consumer does not specify a provider, datacenter, or network configuration. + +**DCM processes the request through the full pipeline:** +1. **Intent captured** — Consumer's raw declaration stored with entity_uuid assigned +2. **Layer assembly** — 5 data layers merge organizational context (datacenter, environment, compliance, tenant, provider defaults) into the consumer's 6 fields, producing 10+ fields with full provenance +3. **Dependency resolution** — Resource type spec declares VM requires Network.IPAddress; DCM creates an IP sub-request automatically +4. **IP policy evaluation** — Sovereignty, subnet isolation, and pool selection policies evaluate against the IP sub-request; IPAM provider selected and IP allocated +5. **VM policy evaluation** — Validation (sizing, sovereignty, approved OS images, field constraints) and Transformation (monitoring injection) policies evaluate +6. **Placement** — Sovereignty pre-filter eliminates non-compliant providers; remaining providers scored by capacity and confidence; best provider selected +7. **Dispatch** — Request Orchestrator sends the assembled payload (including the dependency-injected IP address) to the selected provider; the provider naturalizes DCM's unified payload into its native API +8. **Realization** — Provider provisions the VM, denaturalizes the result back to DCM's format, and callbacks with realized state +9. **Audit** — 17 Merkle tree leaves recorded across the VM entity and IP dependency + +**Success criteria:** Consumer receives a running VM with the pre-allocated IP address. Full provenance chain traceable from every field value back to its origin. Audit trail is tamper-evident and verifiable. Consumer tracked progress through pipeline stages via status API. + +### UC-011: Provision a Three-Tier Application + +A consumer requests a "Web Application — Standard" catalog item. This is a composite service backed by a composite resource type specification that decomposes into four constituent resources: network port, database VM, application server VM, and load balancer. + +The composite resource type spec declares the dependency graph and binding fields: +- Network port has no dependencies — provisioned first +- Database depends on network port — IP address injected from port +- Application server depends on database — connection string and credentials injected +- Load balancer depends on application server — backend pool configured from app server IPs + +DCM processes each constituent through the full pipeline (policy evaluation, placement, dispatch) independently, respecting dependency order. Realized outputs from each constituent flow into dependent constituents via binding fields. If any required constituent fails, a compensation policy triggers reverse-order teardown of already-realized constituents. + +**Success criteria:** Single catalog request produces 4 running, interconnected resources. Runtime values (IPs, connection strings) flow correctly between constituents. Each constituent is independently managed in DCM (its own entity_uuid, audit trail, drift detection). Decommission reverses dependency order. + +### UC-012: Handle a Policy-Blocked Request + +A consumer requests a VM with `environment: production` in a zone that violates their tenant's EU data residency policy. The request enters `POLICY_BLOCKED` state. + +The consumer receives a structured response containing: the blocking policy name, type, and enforcement level; the specific field that violated (sovereignty_zone); compliant value suggestions (e.g., "zone must be one of: eu-west-1, eu-west-2"); and four resolution paths. + +| Resolution | What happens | +|-----------|-------------| +| **Modify request** | Consumer changes the zone to eu-west-1 and resubmits. Request proceeds. | +| **Request override** | Consumer provides justification. For hard policies, dual-approval required (two approvers, different roles). | +| **Cancel** | Consumer abandons the request. Audit trail preserved. | +| **Escalate** | Request routed to the role responsible for the blocking policy domain (e.g., sovereignty admin for data residency violations, security admin for compliance blocks, cost admin for budget overruns). The responsible role reviews and may register an Exception Grant for future similar requests. Routing is configurable per policy domain and profile. | + +**Success criteria:** Consumer receives actionable guidance, not just "denied." Override requests are auditable. Block timeout auto-cancels abandoned requests. Override frequency is tracked for policy review. + +### UC-013: View Cost Before Committing + +A consumer sees an estimated cost for their resource request before submitting. Cost data comes from provider catalog item metadata (declared at registration). The placement engine considers cost alongside sovereignty, tier, and capacity constraints — not as the sole factor. + +**Success criteria:** Cost estimate displayed before submission. Cost attribution tracks to tenant and business unit. + +### UC-014: Track Request Progress + +After submitting a request, the consumer monitors progress through pipeline stages: SUBMITTED → ASSEMBLING → POLICY_EVALUATION → PLACEMENT → DISPATCHED → REALIZING → OPERATIONAL. For composite services, each constituent's status is tracked independently. + +**Success criteria:** Real-time status updates via Server-Sent Events (SSE) or polling. Constituent-level tracking for composite service requests. Failed stages show clear error with remediation guidance. + +--- + +## Day 2 — Ongoing Management + +### UC-020: Update a Running Resource + +A consumer modifies a running VM's memory from 8 GB to 16 GB. DCM creates a new request with operation_type `update`. Only lifecycle-relevant policies fire: the sizing validation policy re-evaluates (lifecycle_scope includes `update`, changed_field_filter includes `memory_gb`). The sovereignty policy does not re-evaluate (no zone change). Placement does not re-run (no provider change). The provider receives a delta payload and applies the change. + +**Success criteria:** Only relevant policies fire. Unchanged fields are not re-evaluated. Audit records the specific mutation. Realized state updated to reflect new memory. + +### UC-021: Scale Resource Capacity + +A consumer scales application server replicas from 2 to 4. If the existing provider/zone has capacity, the scale occurs in place. If capacity is insufficient, placement re-evaluates to find a zone with capacity — subject to the same sovereignty and policy constraints as initial provisioning. + +**Success criteria:** Scale-in-place when capacity exists. Automatic placement re-evaluation when it doesn't. Sovereignty constraints honored throughout. + +### UC-022: Detect and Remediate Drift + +The Discovery service polls providers on a configurable interval (default: 5 minutes). It compares discovered state to realized state. If a VM was manually modified outside DCM (e.g., memory changed from 8 GB to 16 GB via the hypervisor console), drift is detected. The configured drift policy determines the response: notify (alert the consumer and operator), auto-remediate (revert to realized state), or log (record the drift for manual review). + +**Success criteria:** Drift detected within one polling interval. Notification sent to appropriate parties. Auto-remediation restores realized state when configured. Both VM entity and its IP dependency are independently discoverable. + +### UC-030: Rebuild from Stored State (Disaster Recovery) + +A datacenter failure renders an entire availability zone unavailable. The infrastructure operator initiates rehydration for all affected resources. DCM reads the original **Intent State** for each affected entity — the consumer's raw declaration, preserved immutably since submission. + +Each entity re-enters the full pipeline as a new request with operation_type `rehydration`: + +1. **Layer assembly re-runs** — layers may have changed since original provisioning (new compliance requirements, updated monitoring agents). The resource gets current organizational context, not stale data. +2. **All policies re-evaluate** — current sovereignty policies, sizing limits, and security requirements apply. A resource that was compliant when originally provisioned may now violate a newer policy. If so, it enters POLICY_BLOCKED and the operator must resolve before rehydration proceeds. +3. **Placement re-evaluates** — the original provider/zone is unavailable. The placement engine scores surviving providers, subject to the same sovereignty pre-filter. A resource originally in EU-WEST-Prod-1 may rehydrate to EU-WEST-Prod-2. +4. **Dependencies rehydrate in order** — for composite services (three-tier apps), DCM reads the dependency graph and rehydrates constituents in dependency order: database first, then backend (with new DB IP injected), then frontend (with new backend IP injected). Binding fields resolve against newly realized values, not cached originals. +5. **Entity UUID is preserved** — the resource keeps its original entity_uuid across rehydration. Audit trail links the original lifecycle to the rehydrated one. + +**What does NOT happen:** The realized state from the lost zone is not replayed. The intent is re-processed from scratch with current layers, current policies, and current provider availability. This is a design choice — rehydration produces resources that comply with today's rules, not yesterday's. + +**Success criteria:** All affected resources rebuilt on surviving infrastructure. Current policies enforced (not original-time policies). Dependencies resolve correctly with new runtime values. Entity UUIDs preserved. Complete audit trail links original and rehydrated lifecycles. Sovereignty constraints honored — resources cannot rehydrate into non-compliant zones. + +### UC-040: Decommission a Resource + +A consumer or TTL trigger initiates decommission. DCM checks for dependencies: if other resources depend on this one (e.g., a VM using an IP address), decommission is blocked with guidance. If no blockers, the request enters the pipeline with operation_type `decommission`. The provider tears down the resource. Credentials are revoked. The IP address is released back to its pool. Audit trail is preserved permanently. + +**Success criteria:** Dependency checks prevent premature teardown. Provider confirms teardown. Dependent resources (IPs, credentials) are cleaned up. Audit trail survives decommission. + +--- + +## Governance + +### UC-050: Enforce Sovereignty and Data Residency + +All resources handling restricted, PHI, or PCI data are placed exclusively in designated sovereignty zones. The sovereignty validation policy (enforcement_class: compliance) fires on **every lifecycle operation** (initial provisioning, update, scale, rehydration, ownership transfer) — not just initial provisioning. A resource in EU-WEST stays in EU-WEST for its entire lifecycle. + +Override requires dual-approval: two approvers from different roles, with written justification and compensating controls. Every override produces a Merkle audit leaf at field granularity. + +**Success criteria:** No resource is ever realized in a non-compliant zone without an audited, dual-approved override. Sovereignty is enforced end-to-end, not just at creation. + +### UC-052: Prove Pipeline Integrity to Auditor + +An external auditor requests proof that a specific provisioning request was processed correctly. DCM returns: +- **Inclusion proof:** Mathematical proof that the specific audit record exists in the Merkle tree +- **Consistency proof:** Mathematical proof that the tree has only grown since the last signed tree head (no deletions) +- **Request-chain proof:** Chain-of-custody across the request's audit leaves from intent through realization — each Ed25519-signed, with `output_payload_hash[N] == input_payload_hash[N+1]` +- **Signed tree head:** The current root hash signed by DCM's identity + +The auditor can independently verify these proofs without trusting DCM. + +**Success criteria:** Auditor verifies integrity using only DCM's public key and the proof data. No trust relationship with DCM required. Verification completes in seconds regardless of tree size. + +--- + +## Platform Operations + +### UC-071: Onboard a New Tenant + +An administrator creates a new tenant with profile assignment (e.g., "standard" for a production team, "dev" for a sandbox). RLS isolation is enforced immediately — the new tenant cannot see other tenants' data. RBAC is configured: which roles exist, which actors belong to which roles. The catalog is filtered: the tenant sees only catalog items they're authorized to request. The administrator submits a test request to verify full pipeline isolation. + +**Success criteria:** Tenant data is isolated by RLS from first query. RBAC filters the catalog correctly. Test request flows through the complete pipeline with no cross-tenant data leakage. + +### UC-081: Session Management and Auth Failover + +An actor authenticates via the configured auth provider (built-in, Keycloak, LDAP, OIDC). Session tokens are issued with configurable TTL. Concurrent session limits are enforced. If the primary auth provider fails, existing sessions remain valid (cached) and new authentication routes to the failover chain. Session revocation propagates immediately. + +**Success criteria:** Authentication works through all configured providers. Failover is transparent. Session revocation takes effect immediately. Concurrent session limits enforced. + +### UC-091: Git-Based Request Ingress + +A developer submits an infrastructure request via Git Pull Request. The PR identity is resolved to the same DCM actor as API/UI login. The request enters the standard pipeline. Policy dry-run feedback is posted as PR comments. Approval via PR review maps to DCM approval. On merge, the request is dispatched. + +**Success criteria:** Git PR identity resolves to DCM actor. Policy feedback appears as PR comments. Merge triggers dispatch. Full audit trail links PR to DCM request. + +--- + +## Federation + +### UC-060: Cross-Instance Placement (Federation) + +An organization runs two DCM instances: DCM-EMEA (EU datacenters) and DCM-APAC (Asia-Pacific datacenters). A consumer on DCM-EMEA requests a resource that, due to latency requirements for an APAC-facing application, should be placed in an APAC zone. The consumer does not need to know which DCM instance will fulfill the request. + +**How federation works:** + +1. **Peer DCM registration** — DCM-APAC is registered as a `peer_dcm` provider on DCM-EMEA. The registration includes sovereignty declarations, federation eligibility scope (which resource types and operations are permitted), and an mTLS certificate for the federation tunnel. + +2. **Request enters normal pipeline on DCM-EMEA** — Layer assembly, policy evaluation, and placement all run locally. The consumer's sovereignty policy allows APAC placement for this resource type. + +3. **Placement considers remote providers** — The placement engine treats DCM-APAC's providers as candidates alongside local providers. Cross-DCM confidence scoring applies: `cross_dcm_confidence = source_confidence × (tunnel_trust_score / 100)`. Remote providers are scored lower by the trust factor, but may win if local providers lack capacity or sovereignty eligibility. + +4. **DCM-EMEA dispatches to DCM-APAC via federation tunnel** — The request payload is sent over the mTLS tunnel. DCM-APAC receives it, runs its own local policy evaluation (local policies govern — remote policies cannot override), and dispatches to its selected provider. + +5. **Dual audit** — Audit records are written in both DCM instances with a shared `correlation_id`. DCM-EMEA records the outbound federation dispatch. DCM-APAC records the local pipeline execution and realization. Either instance can produce a complete audit trail for its portion. + +6. **Realized state flows back** — DCM-APAC's provider callback flows back through the federation tunnel to DCM-EMEA. The consumer on DCM-EMEA sees the resource as OPERATIONAL with the APAC provider's realized fields. + +**Federation constraints:** +- Sovereignty is verified before tunnel establishment — a classified-zone provider cannot participate in federation +- Storage providers default to `federation_eligibility: none` — data sovereignty prohibits storage federation unless explicitly authorized +- Remote DCMs cannot decommission local resources through a tunnel — decommission is always local +- Certificate rotation uses a 30-day overlap period so peers can update trust stores without coordinated downtime + +**Success criteria:** Consumer requests a resource on DCM-EMEA and receives it from DCM-APAC without knowing the routing. Audit trail in both instances with shared correlation_id. Sovereignty enforced at both ends. Federation tunnel is mTLS-only. + +--- + +## Integration + +### UC-090: ITSM Integration (ServiceNow Change Management) + +An organization requires that all infrastructure provisioning creates change records in ServiceNow, updates are tracked through CMDB configuration items, and decommission retires the CI. DCM integrates with ServiceNow via a **process_provider** — a bidirectional integration that enriches DCM entities with ITSM metadata without making ServiceNow a required dependency. + +**Design principle:** DCM replaces the infrastructure ticket as the provisioning mechanism. ITSM integration is additive — it enriches, it does not gate (unless explicitly configured to do so). + +**Outbound flow (DCM → ServiceNow):** + +1. **ITSM Policy evaluates** — An ITSM Action policy fires on DCM lifecycle events. It is a side-effect policy: it triggers ITSM actions but does not block the pipeline by default. + +2. **On `request.dispatched`** — The ITSM integration creates a ServiceNow change request (CHG record) containing the request details, requesting actor, tenant, resource type, and placement decision. The CHG number is stored on the DCM entity as `business_data.itsm_references[].external_id`. + +3. **On `entity.realized`** — The ITSM integration updates the change request to "Implemented" and creates or updates a CMDB Configuration Item (CI) with the realized resource's details (IP address, provider, datacenter, ownership). + +4. **On `entity.updated`** — The ITSM integration updates the CMDB CI and creates a new change task linked to the parent CHG. + +5. **On `entity.decommissioned`** — The ITSM integration closes the change request, retires the CMDB CI, and updates the CI's lifecycle status. + +**Inbound flow (ServiceNow → DCM):** + +6. **Change Advisory Board (CAB) approval** — For organizations that require CAB approval before provisioning, the ITSM Policy can be configured as `block_until_approved`. The DCM pipeline pauses after the change request is created in ServiceNow. When the CAB approves, ServiceNow calls DCM's approval API (`POST /api/v1/admin/approvals/{uuid}:vote`) with the decision. The pipeline resumes. + +7. **ITSM-initiated requests** — A ServiceNow workflow can create a DCM request via the Admin API, enabling "request infrastructure from ServiceNow" patterns for organizations transitioning from ticket-based provisioning. + +**Safety guardrail:** `block_until_created` (wait for CHG creation before proceeding) requires a `block_timeout` — the pipeline never permanently stalls waiting for an ITSM system. If the timeout fires, the pipeline proceeds with a warning and the ITSM record is created asynchronously when the system recovers. + +**Supported ITSM systems:** ServiceNow, Jira Service Management, BMC Remedy/Helix, Freshservice, PagerDuty, Opsgenie, ManageEngine, Cherwell, TOPdesk, and a generic REST adapter for others. + +**Success criteria:** Every provisioning request has a corresponding ServiceNow change record. CMDB CIs are created on realization and retired on decommission. CAB approval gates work when configured. ITSM system unavailability does not block DCM operations (non-blocking default with configurable blocking mode). Bidirectional links between DCM entity_uuid and ServiceNow CHG/CI numbers. + +--- + +## Pattern Catalog + +### UC-100: Deploy a Resource Pattern from the Pattern Catalog + +A consumer browses the Pattern Catalog section of the service catalog and selects "Standard Web Application." The catalog shows the pattern's components (network segment, database, app servers, load balancer, DNS), the parameters the consumer needs to provide, the dependency graph, and estimated cost. + +The consumer fills in 6 parameters (app_name, environment, db_engine, db_storage_gb, app_replicas, expose_public) and submits. DCM decomposes the pattern into 6 constituent resources, resolves the dependency graph, and processes each constituent through the full pipeline — layer assembly, policy evaluation, placement, dispatch — independently. Each constituent may be placed with a different provider: the database with a managed database provider, the VMs with a compute provider, the load balancer with a network provider. + +**Dependency resolution and binding fields in action:** + +1. Network Segment provisioned first (no dependencies) → produces subnet_cidr, security_group_id +2. Database provisioned next → receives network values via binding fields → produces ip_address, port, credentials_ref +3. App Servers provisioned next → receive DB connection string and network config via binding fields → produce ip_addresses, port +4. Load Balancer provisioned next → receives app server IPs as backend pool via binding fields → produces vip_address +5. DNS records provisioned last → receive LB address via binding fields → public DNS conditional on consumer parameter + +The consumer monitors aggregate progress ("4 of 6 constituents realized") and can drill into individual constituent status. On completion, the consumer has a fully wired application environment. + +**Policy interaction:** No new policy types are needed. Each constituent is a standard resource type, and existing policies match naturally — sovereignty policies check every constituent, sizing policies check VMs, storage policies check the database, naming policies check DNS records. A policy block on any constituent blocks the entire pattern until the consumer resolves it. + +**Failure handling:** If any required constituent fails, the pattern's lifecycle policy determines the response: rollback all realized constituents (default), continue in degraded mode, or notify and hold for manual intervention. + +**Pattern authoring:** Platform engineers create patterns as compound Resource Type Specifications in the Resource Type Registry. Patterns define constituents, dependencies, binding fields, exposed parameters, and lifecycle policies. Consumers see them as catalog items indistinguishable from atomic offerings — the decomposition is invisible. + +**Success criteria:** Single request produces a complete, wired application environment. Runtime values flow correctly between constituents. Each constituent is independently managed (own entity_uuid, audit trail, drift detection, lifecycle). Decommission reverses dependency order. No new control plane services, policy types, or API endpoints required — patterns use existing DCM machinery. + +### Architectural overlay — canonical doc + +The pattern architecture lives in +[`dcm-pattern-catalog-overlay.md`](dcm-pattern-catalog-overlay.md). The gist: patterns are +compound Resource Type Specifications in the existing Resource Type Registry — a catalog +overlay, not a new subsystem. No new policy types; constituents are standard resource types +policed by existing policies. Platform engineers author patterns; consumers select them as +catalog items with a small parameter surface; runtime values flow between constituents via +binding fields. Patterns follow the standard artifact lifecycle +(`developing → proposed → active → deprecated → retired`). + +--- + +# Architecture Principles (summary) + +> Non-normative summary — each principle is owned by the ADR/doc it cites; this list carries +> the gist for requirements readers and changes only when the owning decision does. + +- **Management plane, not provisioning tool.** DCM orchestrates lifecycle and enforces governance. Provisioning is delegated to service providers that implement the provider contract. *(owner: [ADR-001](adr/001-why-dcm-exists.md))* +- **Three abstractions.** Everything in DCM is Data, Provider, or Policy. No exceptions. If a new concept doesn't map to one of these three, the abstraction model needs revision. *(owner: [ADR-002](adr/002-three-abstractions.md))* +- **Provider-agnostic.** Any infrastructure platform is consumable through the same interface via naturalization/denaturalization. Providers declare capabilities (realize_resources, serve_data, authenticate, federate, execute_workflows) rather than being assigned rigid types. Multi-capability providers register once. No lock-in to any platform. *(owner: [ADR-005](adr/005-provider-abstraction.md) + [ADR-023](adr/023-provider-naturalization-boundary.md))* +- **Discoverable.** DCM advertises its capabilities via a machine-readable endpoint. External systems query what DCM offers and subscribe to data streams without reading documentation. Providers declare what they need from DCM at registration; DCM matches needs to capabilities automatically. *(owner: [ADR-005](adr/005-provider-abstraction.md); the surface: [DCM-Capabilities-Matrix.md](DCM-Capabilities-Matrix.md))* +- **Policy-mandatory.** Every request is policy-evaluated. This is not optional. Governance is the value proposition, not a feature toggle. *(owner: [ADR-006](adr/006-policy-engine.md))* +- **Tamper-evident audit.** Every mutation is recorded in a Merkle tree with Ed25519 signatures. Auditors can verify integrity without trusting DCM. *(owner: [ADR-010](adr/010-audit-tamper-evidence.md))* +- **Sovereignty first-class.** Data residency is enforced at request time on every lifecycle operation, not discovered after deployment. *(owner: [ADR-011](adr/011-sovereignty-data-residency.md))* +- **Minimal infrastructure.** PostgreSQL is the only required dependency. Everything else (Kafka, Vault, Keycloak) is optional and follows the Internal/External delegation pattern. *(owner: [ADR-015](adr/015-minimal-infrastructure.md))* +- **Declarative and idempotent.** Consumers declare desired state. DCM converges toward it. Resubmitting the same request produces the same result. *(owner: [ADR-003](adr/003-four-lifecycle-states.md))* +- **API-first.** All capabilities are accessible via API. The Web UI (RHDH) is a consumer of the same API. AEP conventions (snake_case, JSON, flat REST). *(owner: [ADR-009](adr/009-api-gateway-control-plane.md) + [ADR-018](adr/018-wire-serialization-event-conventions.md))* + +--- + +# Success Outcomes + +Each capability area maps to a measurable outcome: + +| **Outcome** | **Enabling Use Cases** | +|-----------|----------------------| +| Developers provision infrastructure without knowing which platform fulfills it | UC-010, UC-011, UC-014 | +| Compound applications deploy as a single catalog request with dependency resolution | UC-011, UC-100 | +| Policy violations are caught at request time with actionable guidance | UC-004, UC-012 | +| Sovereignty and data residency enforced on every lifecycle operation | UC-050 | +| Audit integrity is mathematically provable to external auditors | UC-052, UC-053 | +| Drift between intended and actual state is detected and remediated | UC-022 | +| New infrastructure platforms are addable without changing DCM core | UC-002 | +| Bootstrap requires only PostgreSQL — no middleware stack | UC-001 | +| Override governance provides flexibility without undermining compliance | UC-012, UC-053 | +| Full lifecycle management from request through decommission | UC-010, UC-020, UC-040 | +| Disaster recovery rebuilds from stored intent with current policy evaluation | UC-030 | +| Multi-region placement without consumer awareness of DCM instance topology | UC-060 | +| ITSM records created automatically without gating provisioning by default | UC-090 | +| Self-service reduces platform engineering ticket volume | UC-010, UC-011, UC-014 | + +--- + +# Priority Summary + +| **Priority** | **Count** | **Use Cases** | +|------------|---------|-------------| +| **P0 (Must Have)** | 16 | UC-001 through UC-004, UC-010 through UC-012, UC-014, UC-020, UC-022, UC-040, UC-050, UC-052, UC-071, UC-081, UC-100 | +| **P1 (Should Have)** | 9 | UC-005, UC-013, UC-021, UC-030, UC-051, UC-053, UC-070, UC-080, UC-091 | +| **P2 (Future)** | 6 | UC-023, UC-024, UC-031, UC-060, UC-072, UC-090 | + +--- + +# Open Design Questions + +| **ID** | **Question** | **Status** | +|--------|-------------|-----------| +| **DQ-1** | Application definition language — How should consumers define multi-resource applications? YAML manifests, API composition, external DSL, or catalog-only? | Open — See [ADR-016](adr/016-application-definition-language.md) | +| **DQ-2** | RHDH integration depth — Is RHDH the sole frontend, or should DCM expose its own lightweight UI for environments without RHDH? | Open | +| **DQ-3** | Spectral AEP linter — Should OpenAPI specs be linted in CI? Requires 3.1 compatibility verification. | Open — See [DISCUSSION-TOPICS item 7](DISCUSSION-TOPICS.md) | +| **DQ-4** | Kessel integration — What, if any, integration with Project Kessel for authorization? | Discussion only — See [`integrations/kessel-evaluation.md`](integrations/kessel-evaluation.md) | + +--- + +# Appendix: Key Terms + +- **Naturalization:** Translation of DCM's unified payload into a provider's native API format. +- **Denaturalization:** Translation of a provider's native response back into DCM's unified format. +- **Evaluation Context:** The complete payload, provenance chain, constraint accumulator, and governance scope passed to the policy engine for each evaluation. +- **Binding Fields:** Declarations in composite service definitions that connect realized outputs of one resource (e.g., an IP address) to inputs of a dependent resource (e.g., a VM's network config). +- **Merkle Tree:** A binary hash tree where modifying any leaf changes the root hash. Enables inclusion proofs (a record exists) and consistency proofs (the tree has only grown). RFC 9162. +- **RLS (Row-Level Security):** PostgreSQL feature that automatically scopes every query to the actor's tenant — application code cannot leak cross-tenant data. +- **Sovereignty Zone:** A geopolitical or regulatory boundary declared by providers and enforced by policy. Resources placed in a zone are governed by that zone's data residency rules. +- **Shadow Mode:** A policy lifecycle stage where the policy evaluates against real traffic and logs results without blocking requests. Used for safe validation before activation. +- **RHDH (Red Hat Developer Hub):** Backstage-based developer portal used as DCM's primary Web UI frontend. +- **AEP (API Enhancement Proposals):** Open-source API design guidelines adopted by DCM for consistent API conventions. diff --git a/architecture/dependency-resolution.md b/architecture/dependency-resolution.md new file mode 100644 index 0000000..4ed9ea7 --- /dev/null +++ b/architecture/dependency-resolution.md @@ -0,0 +1,117 @@ +# Dependency resolution + +**What this settles:** how DCM turns the authored UDLM estate — plus dependencies that arrive by +other means — into **one effective dependency graph** that consumers (ordered shutdown/startup, +topology visualizers, impact/blast-radius analysis) run on. The UDLM data model *stores* dependencies +several ways at the implementor's chosen granularity (see the UDLM doc: dependency modeling); DCM +*resolves* them uniformly so consumers never need to know how any dependency was authored. + +Two orthogonal axes: the **authoring pattern** (the shape of a dependency) and the **insertion +mechanism** (how it reaches DCM's view). DCM merges every combination into the effective graph. + +## Insertion mechanisms — how a dependency enters the graph + +1. **Authored** — declared in the UDLM estate: a resource's `dependencies[]` (including an edge to a + shared node it bundles through), a `tenant_uuid`. Versioned, reviewable; the source of truth. +2. **Discovered** — a discovery job probes reality (topology/LLDP, hypervisor inventory, cluster API, + BMC, storage) and inserts observed edges (VM→host, NIC→switch-port, volume→pool). Keeps the graph + synced with reality and **flags drift** where authored ≠ discovered. +3. **Derived** — computed at resolution time, never stored: scope (`tenant_uuid`) → the realm's + identity/DNS services. This is the only real derivation — transitive chains and bundling are plain + edges the traversal already follows; only a membership *field* like `tenant_uuid` needs edges made. +4. **Provider-reported** — a provider, realizing or managing a resource, emits the dependencies it + alone observes at realization (workload→node, VM→host, reservation→DNS zone), as realized state. +5. **Policy-injected** — an admission/policy rule adds dependencies by condition ("any hypervisor + depends on its rack's cooling domain"; "any realm member requires the realm IdM"), keeping broad + invariants out of per-resource authoring. + +These compose on one resource: power **authored** (PSU→feed), placement **discovered**, identity +**derived**, cooling **policy-injected**. + +## The resolution pass (build-time, not stored) + +DCM produces the effective graph as an ordered merge; it is recomputed on demand so it always +reflects the live model: + +1. **Seed** with the authored estate edges. +2. **Union discovered** edges; where a discovered edge contradicts an authored one, keep both and + emit a **drift** finding (authored is intent; discovered is reality). +3. **Union provider-reported** realized edges. +4. **Derive scopes** — for each resource, inject its realm's `Security.DirectoryService` / + `Network.AddressService` from `tenant_uuid` (excluding the control-plane resources themselves, to + avoid cycles), and any `Facility.Location`-scoped ambient dependency. This is the *only* real + derivation: a `tenant_uuid` is a field, not an edge, so nothing else can traverse it. +5. **Apply policy injections** in a defined order. +6. **Transitive chains + bundling need no step** — `host→PSU→feed`, and *bundling* (a resource + `depends_on` a node that carries shared dependencies) are ordinary edges; the dependents inherit + the shared deps as secondary dependencies by graph traversal alone. There is no bundle-expansion + pass and no bundle type — see the anti-pattern note. +7. **Detect cycles and emit them as data** (core capability — see below). An orderable estate is a + DAG; detection runs on *every* resolution and, rather than only erroring, emits each cycle as a + structured `DependencyCycle` finding on the effective graph. The resolver never guesses an order + for cyclic members. + +### Anti-pattern: a dedicated bundle type / expansion pass + +Do not add a `DependencyBundle` type whose members "attach" and inherit its dependencies, nor a +resolution step that expands such membership. It reproduces transitivity the graph already provides — +a resource that `depends_on` a node is already downstream of that node's dependencies — for no +functional gain, at the cost of a parallel mechanism to learn and keep consistent. To bundle, declare +the shared deps on a node and depend on it. Mark a purely-abstract grouping (depended on for ordering +but never acted on) with a lightweight flag, not a type. + +The result is a DAG of typed edges. Consumers run over it directly: +- **ordered shutdown/startup** — topological sort (stop dependents before dependencies; reverse to + start), with control-plane resources held last; +- **visualizers** — render the resolved graph, distinguishing authored vs derived vs discovered; +- **impact analysis** — "what breaks if X goes down" is reachability over the same graph. + +## Dependency-cycle detection — a first-class, policy-addressable output + +A dependency cycle is not merely a resolver error to log — every consumer above *requires* a DAG, so +a cycle is a platform-level signal. It is exposed as **data** and governed by **policy**, decomposed +across the Data·Policy·Provider triad: + +- **Data (UDLM).** Acyclicity is the declared invariant of the dependency graph (see the UDLM + graph-integrity spec). A violation is exposed as a `DependencyCycle` diagnostic — + `{members[], edge_chain[], severity, contributing_mechanisms[], detector}`. **Severity is derived + from the cycle's own edges:** a cycle whose every edge is `hard` is **blocking** — no safe order + exists; a cycle that a `soft` edge closes is **degraded** — orderable by dropping the soft edge, but + flagged. UDLM defines the shape; it does not compute it. +- **Provider (DCM resolution).** The resolution engine computes cycles from the **effective** graph + (so derived, discovered, and policy-injected edges are all in scope, not just authored ones): + Kahn's longest-path leaves cyclic members with non-zero in-degree, and a DFS then extracts the + actual chain for each. Detection is a **core, always-run** step, and every cycle is tagged with the + insertion mechanisms that contributed its edges — so "authored ⇄ authored" is distinguishable from + "authored ⇄ discovered" (an intent/reality conflict) or "… ⇄ policy-injected" (a rule that closed a + loop). Provenance turns a cycle from a dead end into a diagnosable one. +- **Policy (DCM policy engine).** `DependencyCycle` findings are **policy inputs**. Admission + default-denies a **blocking** (all-hard) cycle — a cyclic estate does not realize — while a + **degraded** cycle is configurable: warn, quarantine the members, or auto-relax the soft edge with a + recorded resolution. Policies match on `graph.cycles` / `graph.cycle_severity` (UDLM policy match + sources), so the response is authored, not hard-coded in the engine. + +The payoff: "the estate won't order" stops being an opaque failure and becomes a first-class, +severity-ranked, provenance-tagged, policy-governed signal — the same shape as any other finding the +platform acts on. Reference realizations already exist: the estate CI's **CYCLE-001** gate (reports +the offending chain) and the estate-explorer `/api/order` `cycles[]` output. + +## Choosing an authoring pattern (best practice) + +| Pattern | Granularity | Effort/resource | Fidelity | Use for | +|---|---|---|---|---| +| Direct edge | any | high | exact | specific bindings, one-offs | +| Component chain (PSU→feed) | finest | medium | redundancy-aware | power, network fabric | +| Bundling (depend on a shared node) | coarse | low | shared, via transitivity | platform services routed through a node | +| Scope-derived (field) | coarse | none | ambient | realm from tenant | + +Guidance: **start coarse** (a shared node / scope) for a fast, correct-enough graph; **refine to +component-level** where redundancy or precision earns the effort. Patterns mix on one resource. Model +power at the PSU→feed level rather than a host-level UPS edge whenever redundancy matters — a coarse +edge silently drops the second rail. + +## Boundary + +Storage of dependencies (the patterns, the types) is UDLM's; resolution and the insertion mechanisms +are DCM's. This keeps the data model free of computed state and lets an estate be authored coarsely, +finely, or in a mix without changing the model. See [`architecture/layering.md`](layering.md). diff --git a/architecture/design-principles.md b/architecture/design-principles.md new file mode 100644 index 0000000..a2906ce --- /dev/null +++ b/architecture/design-principles.md @@ -0,0 +1,210 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Design Philosophy +Established: 2026-05-26 +Maps to: udlm/design-principles/design-priorities.md +--- + +# DCM Design Principles — Implementation Choices + +> **Implements the design principles defined in UDLM**: +> [udlm/design-principles/design-priorities.md](https://github.com/croadfeldt/udlm/blob/main/design-principles/design-priorities.md). +> UDLM owns the four invariant principles (consumer sovereignty, zero trust, +> federation, policy as code) and their decision framework. This document +> records the specific implementation choices DCM makes when applying them. + +DCM operationalizes the four UDLM priorities (Security → Ease of Use → +Extensibility → Fit for Purpose) through specific runtime mechanics, profile +configurations, and integration choices. This document captures the +DCM-specific tradeoffs. + +--- + +## 1. Design priorities — implementation choices + +The four UDLM priorities apply in DCM exactly as defined. DCM's +implementation choices express them concretely: + +| UDLM principle | DCM implementation choice | +|---|---| +| Security (Priority 1) | Non-negotiable security properties wired into every profile via the profile system; auto-approve threshold ≤ 50 enforced at scoring engine; CPX-001 (no credential values in DCM stores) enforced at credential issuance | +| Ease of use (Priority 2) | The profile system as the primary scaling mechanism; consumer Admin API integration point (so external workflow tools can plug in without DCM-side adapters); Flow GUI for policy authoring | +| Extensibility (Priority 3) | Compliance domain overlays compose with base profiles; Policy Groups compose with profile policies; capability extensions compose with base provider contracts; Universal Group Model spans all artifact types | +| Fit for purpose (Priority 4) | Complete lifecycle coverage — every edge case (partial realization, compensation, drift remediation, credential revocation on decommission) handled by named mechanism, not happy-path-only | + +--- + +## 2. Approval tier model (runtime enforcement) + +UDLM defines the authority tier vocabulary (auto / reviewed / verified / +authorized) and the rules for custom tier insertion. The four tiers are an +escalating human-gate ladder: `auto` = no human gate; `reviewed` = one +approver; `verified` = two independent approvers; `authorized` = a quorum vote +by a named group. DCM enforces tiers at runtime through the following +mechanisms. + +| Tier | DCM enforcement | +|---|---| +| `auto` | Structural and governance validation pipeline; automatic activation on pass; no human gate | +| `reviewed` | Approval record created; eligible reviewer notified via the Notification Service; pipeline held; decision recorded via Admin API endpoint `POST /api/v1/admin/approvals/{uuid}:vote`; activation or rejection on first decision | +| `verified` | Approval record requires two independent decisions; DCM enforces distinct actors (same actor cannot satisfy both); eligible reviewer notification; pipeline held; activation on second decision | +| `authorized` | Approval record specifies the required DCMGroup and quorum threshold (N of M); group member notification; pipeline held; threshold evaluation on each vote; activation when N reached | + +**DCM does not build a deliberation/voting platform.** DCM builds: + +1. DCMGroup membership management (which actors constitute the authorized group) +2. Quorum declaration (`N of M` in profile or per-decision config) +3. Notification routing on state entry into `pending_authorized` +4. Vote-recording API endpoint (the Admin API is the integration point) +5. Quorum tracking and pipeline advancement +6. Audit trail with `recorded_via` provenance (Slack bot, ServiceNow, Jira, direct API, etc.) + +External systems (ServiceNow, Jira, email workflows, Slack bots) connect by +calling the vote-recording API. A Slack bot that collects emoji reactions and +then calls DCM's Admin API is a valid implementation. + +### 2.1 Admin API as integration point + +``` +POST /api/v1/admin/approvals/{approval_uuid}:vote +Authorization: Bearer +{ + "decision": "approve | reject", + "reason": "", + "recorded_via": "dcm_admin_ui | servicenow | jira | slack_bot | api_direct | other", + "external_reference": "" +} +``` + +The `recorded_via` field provides audit provenance. It is informational, not +enforced — DCM doesn't care how the vote was collected, only that an authorized +group member recorded it. + +### 2.2 Deadline and escalation + +DCM manages the approval window and fires escalation notifications: + +```yaml +approval_window: + reviewed: PT72H + verified: PT72H + authorized: P7D + on_expiry: + reviewed: escalate # escalate to platform admin + verified: escalate + authorized: reject # authorized tier that cannot reach quorum → reject +``` + +See [`credentials-and-auth/authority-enforcement.md`](credentials-and-auth/authority-enforcement.md) +for the tier evaluation algorithm and degradation review orchestration. + +--- + +## 3. Profile-governed system constraints + +DCM ships six built-in profiles (`homelab`, `dev`, `standard`, `prod`, `fsi`, +`sovereign`). Profiles control: + +- Enforcement strictness (how strictly a security property is enforced) +- Threshold values (TTLs, intervals, thresholds) +- Automation level (automated vs manual trigger) +- Approval tier defaults (which tier routes requests at which score) +- Review periods (shadow mode duration before promotion) + +Profiles do **not** control: + +- Whether a security property is present (it always is) +- Which non-negotiable constraints apply (CPX-001, SMX-004, SMX-008, etc.) +- Whether the audit trail is maintained +- Whether the data model is valid + +**The `minimal` profile is "security with minimal operational overhead" — not +"minimal security."** A homelab-profile deployment still rotates credentials +(longer interval, manual trigger acceptable), detects idle credentials (generous +P30D threshold), audits first credential retrieval, maintains revocation +registry. The security model is present and correct; the enforcement strictness +and automation burden are reduced. + +See [`governance-enforcement/policy-profiles.md`](governance-enforcement/policy-profiles.md) +for the complete profile definitions and the matrix of what each profile controls. + +--- + +## 4. Policy-as-code requirement (integration) + +DCM realizes the UDLM "policy as code" principle by integrating with OPA +(Open Policy Agent) as the default policy evaluation engine and supporting +External Policy Evaluators for organization-specific policy systems. + +**Internal mode (default):** Policies are stored in DCM's PostgreSQL database +and evaluated by OPA. Delivery mechanisms include API push/pull, GitOps +adapter, OPA bundle protocol, or external-schema naturalization. All +mechanisms produce equivalent evaluation. + +**External mode:** DCM sends evaluation context to an external endpoint +declared by the External Policy Evaluator provider. The external system +returns structured results (pass/fail, score, enrichment fields). DCM does +not see the policy logic; it trusts the results within scoped bounds. +Governance policies BBQ-001 through BBQ-009 (in +[udlm/contracts/policy-contract.md](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md)) +constrain external evaluation: data sovereignty check, data minimization, +audit per query, default-deny on unknown. + +Every policy evaluation produces an audit record regardless of outcome — +audit is non-negotiable. + +--- + +## 5. Documentation discipline requirements (DCM-internal) + +DCM-side documents must: + +1. Open with `> Implements contracts defined in UDLM: [link]` when implementing + a UDLM contract +2. Reference the priority order where design decisions involve tradeoffs +3. Explain non-negotiable security properties with clear rationale +4. Document what profiles control vs what they do not +5. Identify the ease-of-use mechanism that accompanies every security requirement +6. State fit-for-purpose scope explicitly + +These are operational documentation standards, not UDLM contract requirements. +A peer DCM realization is free to organize its internal documentation +differently. + +--- + +## 6. System policies (DCM realization) + +| Policy | Rule | +|--------|------| +| `DPO-001-DCM` | Security properties are architecturally present in all DCM profiles. Profiles control enforcement strictness, thresholds, and automation level — not whether the property exists. | +| `DPO-002-DCM` | Every security requirement in DCM must be accompanied by an ease-of-use mechanism that makes compliance effortless for the common case. | +| `DPO-003-DCM` | New DCM capabilities should be expressed through the existing profile/policy/provider extension system before creating new mechanisms. | +| `DPO-004-DCM` | Fit for purpose is a precondition. All four priorities apply only within the constraint that DCM can fulfill its lifecycle management mission. | +| `DPO-005-DCM` | The `minimal` profile is "security with minimal operational overhead" — not "minimal security." Design changes that disable security properties rather than scaling them violate DPO-001-DCM. | +| `DPO-006-DCM` | When security and ease of use conflict in DCM, redesign the ease-of-use mechanism — not the security requirement. The secure path must also be the easy path. | + +--- + +## 7. Common misapplications to avoid + +**"We can disable X in the homelab profile for simplicity."** +Wrong. The homelab profile scales down operational burden, not security +properties. Question: what is the minimum viable implementation of X that +requires no operational overhead? That is what the homelab profile gets. + +**"Security is too complex for our users, so we'll make it optional."** +Wrong. If security is too complex, the design of the security mechanism needs +to improve (Priority 2). Making security optional removes it — that fails +Priority 1. + +**"We need a new mechanism for this capability."** +Wrong starting point. Question: can this be expressed through profiles, +policies, provider capability extensions, or compliance overlays? Usually +yes. If genuinely not, extend the nearest existing mechanism. + +**"This edge case isn't part of the lifecycle."** +Wrong framing. Every edge case in the lifecycle — partial realization, +compensation, drift remediation, credential revocation on decommission — +is part of the lifecycle. Fit for purpose means handling the complete +lifecycle, not just the happy path. diff --git a/architecture/governance-enforcement/accreditation-monitor.md b/architecture/governance-enforcement/accreditation-monitor.md new file mode 100644 index 0000000..31185c6 --- /dev/null +++ b/architecture/governance-enforcement/accreditation-monitor.md @@ -0,0 +1,607 @@ +--- +Document Status: 📋 Draft — Ready for Implementation Feedback +Document Type: Capability Specification +Maps to: udlm/governance/accreditation-and-authorization-matrix.md +--- + +# DCM Data Model — Accreditation Monitor + +> **Implements contracts defined in UDLM**: +> [udlm/governance/accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). +> UDLM defines the accreditation entity, the authorization matrix, and the +> validity model. DCM operationalizes continuous external verification of +> accreditation status against authoritative sources. + +**Document Status:** 📋 Draft — Ready for Implementation Feedback +**Document Type:** Capability Specification +**Related Documents:** [Accreditation and Authorization Matrix](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) | [Information Providers](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers.md) | [Advanced Information Providers](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md) | [Standards Catalog](https://github.com/croadfeldt/udlm/blob/main/reference/standards-catalog.md) | [Scoring Model](../convergence-engine/scoring.md) | [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md) | [Governance Matrix](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md) + +> **AEP Alignment:** API endpoints follow [AEP](https://aep.dev) conventions. +> Custom methods use colon syntax. Async operations return `Operation` resources. +> See `schemas/openapi/dcm-admin-api.yaml` for the normative admin API specification. + +--- + +## 1. Purpose + +The Accreditation Monitor is a DCM **Information Provider** that continuously verifies the status of registered accreditations against authoritative external sources — compliance registries, certificate authority portals, and contract management systems. It answers the question DCM's existing accreditation lifecycle model cannot answer alone: + +> *"Is this accreditation still valid according to the issuing authority — not just according to the expiry date we were told?"* + +An accreditation can become invalid before its declared `expires_at` date: FedRAMP authorizations can be revoked mid-cycle, ISO 27001 certificates can be suspended by the certification body, CMMC certifications can be downgraded. Without external monitoring, DCM would continue treating a provider as accredited until the date it was told to check — which may be 12 months away. + +The Accreditation Monitor closes this gap by polling authoritative external registries on a configurable schedule and surfacing discrepancies to DCM's existing accreditation lifecycle model. + +--- + +## 2. What Can Be Automated — Framework by Framework + +Automation depth varies significantly by framework. The Accreditation Monitor implements four verification tiers: + +### Tier 1 — Full External Verification (public registry API) + +DCM can query the authoritative registry directly. Status changes are detected without any manual intervention. + +| Framework | Authoritative Source | What Is Verified | +|-----------|---------------------|-----------------| +| **FedRAMP Moderate/High/LI-SaaS** | [marketplace.fedramp.gov/api](https://marketplace.fedramp.gov) | Authorization status (Authorized / In Process / Revoked), impact level, authorization date, annual assessment currency | +| **StateRAMP** | [stateramp.org](https://stateramp.org) | Authorization status, status changes | +| **CMMC 2.0** | [cyberab.org/catalog](https://cyberab.org/catalog) | Certification level (1/2/3), certification status, expiry date | +| **IAF / ISO 27001** | [iaf.nu CertSearch](https://iaf.nu) | Certificate status (valid/suspended/withdrawn), expiry date, scope, issuing CB identity | + +**Required field at registration:** `external_registry_id` — the provider's identifier in the external registry (FedRAMP ID, CMMC certification number, ISO certificate number). + +### Tier 2 — Document Currency Verification + +DCM cannot query a central registry, but it can verify that the stored evidence document is current relative to the framework's required renewal cycle. + +| Framework | Verification Method | Currency Check | +|-----------|---------------------|---------------| +| **SOC 2 Type I / Type II** | Document metadata extraction from `audit_report_ref` | Report period end date must be within 12 months (configurable); examines PDF metadata or report header | +| **PCI DSS** | QSA authorization check + AoC date | Verifies the issuing QSA firm is currently listed as authorized at PCI SSC; verifies stored AoC covers current period | +| **ISO 27001** (when CB portal is unavailable) | Certificate document date + P3Y renewal cycle | Falls back to document-based verification if IAF CertSearch is unreachable | + +### Tier 3 — Contract Lifecycle Integration + +No external registry exists. Verification is driven by events from contract management systems. + +| Framework | Integration Mechanism | What Triggers Verification | +|-----------|----------------------|--------------------------| +| **HIPAA BAA** | Contract management webhook (DocuSign, Ironclad, Agiloft, custom) | BAA signed, amended, terminated, or approaching renewal | +| **DoD IL2/IL4/IL5/IL6** | Manual submission + webhook on DISA action letter | Authorization letter received, amended, or revoked | +| **Custom / Sovereign** | Manual submission + optional webhook | Platform admin triggers; or custom webhook from issuing authority | + +### Tier 4 — Expiry-Only Monitoring (no external verification available) + +No external API, no document currency check, no contract integration. DCM monitors only the declared `expires_at` date. + +| Framework | What Is Monitored | +|-----------|-----------------| +| **HIPAA BAA** (if no contract system) | Declared BAA expiry date | +| **Internal / first_party accreditations** | Declared validity period | +| **Self-declared** | Declared validity period | + +--- + +## 3. Accreditation Record Additions + +The existing accreditation record ([accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) §3.3) is extended with three new fields to support automated monitoring: + +```yaml +accreditation: + # ... existing fields unchanged ... + + # NEW — Verification model + verification: + tier: external_registry | document_currency | contract_webhook | expiry_only + # ── Tier 1 specific ── + registry_api: + provider: fedramp | stateramp | cmmc_ab | iaf_certsearch | custom + lookup_key: # e.g., FedRAMP ID "FR2024-0042" + poll_interval: P1D # how often to check + last_checked_at: + last_result: confirmed_active | status_changed | registry_unavailable | not_found + + # ── Tier 2 specific ── + document_check: + document_url: + max_age: P365D # how old the document can be + date_extraction_method: pdf_metadata | report_header_parse | manual + last_checked_at: + last_document_date: + + # ── Tier 3 specific ── + contract_webhook: + contract_system: docusign | ironclad | agiloft | custom + contract_id: + webhook_url: + last_event_at: + + # ── Shared ── + stale_after: P7D # how long before last_checked_at = stale + stale_action: warn | suspend | escalate # what to do when stale + verification_failure_count: 0 # consecutive failures; triggers escalation + verification_failure_threshold: 3 +``` + +The `last_verified_at` field on the existing accreditation record (the five-check boundary model) is updated by the Accreditation Monitor on each successful verification. It remains the canonical "last confirmed active" timestamp used by the Governance Matrix and Scoring Model. + +--- + +## 4. Accreditation Monitor as an Information Provider + +The Accreditation Monitor registers with DCM as an Information Provider with `information_type: accreditation_verification`. It is a separately deployable component — it does not require changes to the DCM control plane and can be upgraded independently. + +```yaml +accreditation_monitor_registration: + provider_type: information_provider + information_type: accreditation_verification + display_name: "DCM Accreditation Monitor" + version: "1.0.0" + + # What it monitors + supported_tiers: + - external_registry + - document_currency + - contract_webhook + - expiry_only + + supported_registries: + - fedramp + - stateramp + - cmmc_ab + - iaf_certsearch + - pci_ssc_qsa # QSA verification only + + supported_contract_systems: + - docusign + - ironclad + - custom_webhook + + # How it communicates results back to DCM + push_events: true + event_types: + - accreditation.verified # periodic confirmation: still active + - accreditation.status_changed # external registry shows different status + - accreditation.registry_mismatch # external status != DCM recorded status + - accreditation.verification_stale # last_checked_at exceeds stale_after threshold + - accreditation.expiry_approaching # approaching expires_at (supplement to existing) + - accreditation.document_expired # document_check: document older than max_age + - accreditation.contract_event # contract_webhook: BAA signed/amended/terminated + + health_check: + endpoint: /health + interval: PT5M +``` + +--- + +## 5. Verification Flows + +### 5.1 Tier 1 — FedRAMP External Registry Verification + +``` +Accreditation Monitor poll cycle (default: P1D): + │ + ▼ For each active Tier 1 accreditation: + │ Load accreditation record + │ Extract: framework, external_registry_id, last known status + │ + ▼ Query external registry: + │ FedRAMP: GET marketplace.fedramp.gov/api/products?id={external_registry_id} + │ CMMC: GET cyberab.org/api/certifications?cert_number={external_registry_id} + │ IAF: GET iaf.nu/certsearch?cert={external_registry_id} + │ + ├── Registry returns: status = Authorized, impact_level = High + │ Matches DCM record → no action + │ Update: last_checked_at, last_result: confirmed_active + │ Fire: accreditation.verified (urgency: low) + │ + ├── Registry returns: status = In Process (was Authorized) + │ Status changed → MISMATCH + │ Fire: accreditation.status_changed (urgency: high) + │ Payload: {from: authorized, to: in_process, external_source: fedramp_marketplace} + │ DCM action: accreditation status → pending_review + │ Platform Admin notified — human must review and decide: suspend or retain + │ + ├── Registry returns: status = Revoked + │ Fire: accreditation.status_changed (urgency: critical) + │ DCM action: accreditation status → revoked immediately + │ Accreditation Gap triggered for all affected providers + │ Recovery Policy evaluated + │ + ├── Registry returns: 404 / not_found + │ May indicate ID change or deregistration + │ Fire: accreditation.registry_mismatch (urgency: high) + │ Increment verification_failure_count + │ Platform Admin notified to verify external_registry_id is correct + │ + └── Registry unreachable (timeout, 5xx) + Update: last_result: registry_unavailable + Increment verification_failure_count + If count >= verification_failure_threshold: + Fire: accreditation.verification_stale (urgency: medium) + Do NOT change accreditation status on registry failure alone + (conservative: prefer false negative over false positive revocation) +``` + +### 5.2 Tier 2 — Document Currency Verification (SOC 2, PCI DSS AoC) + +``` +Verification cycle (default: P7D): + │ + ▼ Fetch document from certificate_ref or audit_report_ref URL + │ + ├── PDF: extract creation_date from PDF metadata + │ or parse report header for "Report Date: YYYY-MM-DD" + │ + ├── HTML report: parse structured date field + │ + └── Fallback: flag for manual review if date cannot be extracted + + ▼ Compare document date to max_age threshold (default P365D): + │ + ├── Within threshold → update last_checked_at, last_document_date + │ Fire: accreditation.verified (urgency: low) + │ + └── Beyond threshold → Fire: accreditation.document_expired (urgency: high) + Platform Admin notified: new report needed + Accreditation status → pending_renewal +``` + +### 5.3 Tier 3 — Contract Webhook (HIPAA BAA, DoD IL) + +``` +Contract management system fires webhook to DCM: + POST /api/v1/admin/accreditations/{uuid}/contract-event + + Payload: + { + "contract_id": "", + "event_type": "signed | amended | terminated | renewal_due | renewed", + "effective_date": "", + "details": { ... contract-system-specific fields ... } + } + + DCM processes: + ├── signed → accreditation status: active (if was pending) + ├── amended → accreditation status: pending_review; Platform Admin notified + ├── terminated → accreditation status: revoked; Accreditation Gap triggered + ├── renewal_due → notification to Compliance Team (urgency: medium) + └── renewed → accreditation status: active; expires_at updated; last_verified_at updated +``` + +### 5.4 Stale Verification Handling + +Regardless of tier, when `last_checked_at` is older than `stale_after`: + +``` +stale_action: warn → Fire: accreditation.verification_stale (urgency: low) + No change to accreditation status + +stale_action: suspend → Fire: accreditation.verification_stale (urgency: high) + Accreditation status → suspended + Accreditation Gap triggered (gap_type: suspended) + Platform Admin must manually verify and reactivate + +stale_action: escalate → Fire: accreditation.verification_stale (urgency: critical) + Escalation chain notified (Compliance Team + Platform Admin) + No automatic status change + If not resolved within escalation_window: → suspend +``` + +`stale_action` defaults by profile: `warn` for dev/standard, `suspend` for prod, `escalate` for fsi/sovereign. + +--- + +## 6. New Event Types (additions to [event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md)) + +These events are added to the event catalog as domain `accreditation.*`: + +| Event Type | Urgency | Description | Key Payload Fields | +|-----------|---------|-------------|-------------------| +| `accreditation.verified` | low | Periodic external confirmation — accreditation still active | accreditation_uuid, framework, registry, checked_at | +| `accreditation.status_changed` | high or critical | External registry shows a different status than DCM records | accreditation_uuid, framework, from_status, to_status, external_source | +| `accreditation.registry_mismatch` | high | External registry cannot find the accreditation by its external_registry_id | accreditation_uuid, external_registry_id, registry, failure_detail | +| `accreditation.verification_stale` | varies | last_checked_at exceeds stale_after threshold | accreditation_uuid, last_checked_at, stale_after, stale_action_taken | +| `accreditation.document_expired` | high | Evidence document older than max_age threshold | accreditation_uuid, framework, document_url, document_date, max_age | +| `accreditation.contract_event` | varies | Contract management webhook received | accreditation_uuid, contract_event_type, contract_id, effective_date | +| `accreditation.expiry_approaching` | medium | Approaching expires_at within renewal_warning_before (supplement to existing TTL-based check) | accreditation_uuid, expires_at, days_remaining | + +--- + +## 7. Accreditation Record Additions to [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) + +The following fields are added to the accreditation record structure in [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) §3.3. +These are non-breaking additions — existing records without these fields default to `tier: expiry_only`. + +```yaml +# Additions to existing accreditation record ([accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) §3.3) + + verification: + tier: external_registry | document_currency | contract_webhook | expiry_only + + # For external_registry tier: + registry_api: + provider: fedramp | stateramp | cmmc_ab | iaf_certsearch | pci_ssc_qsa | custom + lookup_key: # value to use as query key in registry + poll_interval: P1D # ISO 8601 duration; how often Monitor checks + last_checked_at: + last_result: confirmed_active | status_changed | registry_unavailable | not_found | pending + + # For document_currency tier: + document_check: + document_url: # usually same as certificate_ref or audit_report_ref + max_age: P365D # maximum acceptable document age + date_extraction_method: pdf_metadata | report_header_parse | manual + last_checked_at: + last_document_date: + + # For contract_webhook tier: + contract_webhook: + contract_system: docusign | ironclad | agiloft | custom + contract_id: # ID in the contract management system + webhook_configured: true | false + last_event_at: + + # Shared across all tiers: + stale_after: P7D # max acceptable gap between verifications + stale_action: warn | suspend | escalate + verification_failure_count: 0 + verification_failure_threshold: 3 +``` + +--- + +## 8. Impact on Scoring Model ([scoring.md](../convergence-engine/scoring.md)) + +The Scoring Model's Signal 5 (Provider Accreditation Richness, [scoring.md](../convergence-engine/scoring.md) §4.5) is +enhanced with a verification currency dimension. An accreditation that has been externally +verified recently is worth more than one that has only ever been manually submitted. + +```yaml +# Addition to accreditation_weights in [scoring.md](../convergence-engine/scoring.md): +verification_multipliers: + # Applied to each accreditation's weight based on verification currency + external_registry_verified_within_P1D: 1.0 # full weight + external_registry_verified_within_P7D: 0.9 # slight discount + document_verified_within_P30D: 0.85 + contract_webhook_active: 0.9 + expiry_only_no_external_check: 0.7 # meaningful discount + verification_stale: 0.4 # significant discount + verification_failed_threshold_reached: 0.1 # near-zero weight +``` + +This means a provider with a FedRAMP High accreditation that was externally verified +yesterday scores higher in placement tie-breaking than a provider with the same +accreditation whose verification check has been stale for 30 days. + +--- + +## 9. Admin API Additions + +``` +# List all accreditations with their current verification status +GET /api/v1/admin/accreditations + ?verification_status=stale|failed|confirmed|pending + &framework=fedramp_high|iso_27001|... + &subject_uuid={provider_uuid} + page_size=50&page_token=... + +# Trigger immediate re-verification of a specific accreditation +POST /api/v1/admin/accreditations/{accreditation_uuid}:verify + +Response 200 — returns Operation: + { + "name": "/api/v1/operations/{uuid}", + "done": false, + "metadata": { + "stage": "VERIFICATION_INITIATED", + "accreditation_uuid": "{uuid}", + "verification_tier": "external_registry" + } + } + +# Register a contract webhook endpoint for a BAA or DoD IL accreditation +POST /api/v1/admin/accreditations/{accreditation_uuid}:configure-webhook + { + "contract_system": "docusign", + "contract_id": "abc-def-123", + "webhook_secret": "" + } + +# Inbound webhook endpoint (called by contract management systems) +POST /api/v1/admin/accreditations/{accreditation_uuid}/contract-event + Authorization: Bearer + { + "contract_event_type": "terminated", + "effective_date": "2026-04-01", + "contract_id": "abc-def-123", + "details": {} + } +``` + +--- + +## 10. Deployment and Configuration + +The Accreditation Monitor is deployed as a standalone container alongside the DCM control plane. It requires: + +- Network access to external registries (FedRAMP, CMMC AB, IAF CertSearch, PCI SSC) +- Network access to the DCM API Gateway (to push accreditation events) +- Access to `certificate_ref` and `audit_report_ref` document URLs (for Tier 2 checks) +- Inbound webhook endpoint (for Tier 3 contract system integrations) + +**Air-gapped / sovereign deployments:** For deployments without external internet access, the Accreditation Monitor operates in Tier 4 (expiry-only) mode by default for all frameworks. Tier 2 checks can still work if documents are stored on internal object storage (certificate_ref points to an internal URL). Tier 3 contract webhooks work if the contract management system is internal. Tier 1 registry checks are disabled — a manual verification workflow applies instead, with platform admins periodically updating `last_verified_at` after out-of-band confirmation. + +```yaml +accreditation_monitor_config: + # Per-registry enable/disable + registries: + fedramp: + enabled: true + poll_interval: P1D + timeout: PT30S + cmmc_ab: + enabled: true + poll_interval: P7D + iaf_certsearch: + enabled: true + poll_interval: P7D + stateramp: + enabled: false # enable if state/local gov providers present + + # Document currency checks + document_checks: + enabled: true + default_max_age: P365D + extraction_timeout: PT60S + + # Air-gapped mode + air_gapped_mode: false + air_gapped_fallback: expiry_only | manual_workflow + + # Global escalation + global_stale_after: P7D + global_failure_threshold: 3 + escalation_contact: + service_provider_uuid: + urgency: critical +``` + +--- + +## 11. System Policies + +| Policy | Rule | +|--------|------| +| `ACM-001` | The Accreditation Monitor is the authoritative source for `last_verified_at` on accreditation records. Platform admins may update it manually only when the Monitor is unavailable or in air-gapped mode — all manual updates require a justification reason stored in the audit trail. | +| `ACM-002` | An accreditation status change detected by the Monitor (external registry reports different status than DCM) does not automatically revoke the accreditation. It fires `accreditation.status_changed` and sets status to `pending_review`. A platform admin must confirm the change. The exception: if the external status is `Revoked` or `Terminated`, DCM immediately sets accreditation status to `revoked` without waiting for admin confirmation. | +| `ACM-003` | Verification failure (registry unreachable, document inaccessible) does not revoke an accreditation. The Monitor increments `verification_failure_count`. At `verification_failure_threshold`, it fires `accreditation.verification_stale` and applies `stale_action`. Failure itself does not constitute a gap — only confirmed negative status does. | +| `ACM-004` | Accreditations in `sovereign` and `fsi` profiles must have `verification.tier` declared at a level of `document_currency` or above. `expiry_only` is not permitted for sovereign/fsi accreditations unless `air_gapped_mode: true` is explicitly configured. | +| `ACM-005` | The `verification_multipliers` in the Scoring Model (Signal 5) apply to all accreditations, including those submitted before the Accreditation Monitor was deployed. Legacy accreditations with no `last_checked_at` are treated as `verification_stale` and weighted at the stale multiplier (0.4) until the Monitor performs its first check. | +| `ACM-006` | Inbound contract webhooks (Tier 3) must authenticate using a provider callback credential issued at accreditation configuration time. Unauthenticated webhook calls are rejected with `401 Unauthorized` and generate an audit record. | +| `ACM-007` | All verification events (`accreditation.*`) are written to the Audit Store regardless of outcome. There are no silent verifications — every check, success or failure, has an audit record. | +| `ACM-008` | In air-gapped mode, the Monitor operates in Tier 4 for registries it cannot reach. It does not repeatedly attempt unreachable external registries. After `air_gapped_retry_interval` (default P30D), it retries once to detect if network access has been restored. | + +--- + +## 12. Relationship to Existing Architecture + +### [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) — Accreditation and Authorization Matrix +The Accreditation Monitor extends but does not replace the accreditation lifecycle model in [accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). The existing `proposed → active → expired/revoked` lifecycle is preserved. The Monitor adds automated transitions into `pending_review` and `pending_renewal` states, and provides the data that drives the existing `ACCREDITATION_GAP` logic. + +### [governance-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/governance-matrix.md) — Governance Matrix +The Governance Matrix already evaluates accreditation status as part of Check 3 of the five-check boundary model. The Monitor improves the quality of that check: instead of relying solely on the declared `expires_at` date, the Governance Matrix now has access to externally verified current status via `last_verified_at` and `last_result`. + +### [scoring.md](../convergence-engine/scoring.md) — Scoring Model +The `verification_multipliers` addition to Signal 5 (Provider Accreditation Richness) means placement decisions can prefer providers whose accreditations have been recently externally verified over those relying on self-declared or stale verifications. This is a conservative, progressive enhancement — it does not block placement, only refines tie-breaking. + +### [event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md) — Event Catalog +Seven new `accreditation.*` events are added (Section 6 of this document). These follow the same envelope and urgency model as all other DCM events. + +### [standards-catalog.md](https://github.com/croadfeldt/udlm/blob/main/reference/standards-catalog.md) — Standards Catalog +The Accreditation Monitor is the operational implementation of the standards catalog's compliance framework entries. The standards catalog says *what* DCM recognizes; the Accreditation Monitor says *how* DCM verifies that recognition is still current. + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* + +--- + +## 13. Accreditation Governance Enforcement + +> **Implements contracts defined in UDLM**: +> [udlm/governance/accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). +> UDLM defines the accreditation model, lifecycle, and authorization matrix. +> This section operationalizes the governance enforcement DCM applies on +> top of the Accreditation Monitor. + +### 13.1 Accreditation gap response + +When a required accreditation becomes missing, expired, or revoked, DCM +enters an Accreditation Gap state for the affected provider: + +```yaml +accreditation_gap_record: + uuid: + provider_uuid: + required_framework: hipaa + required_for: [phi data fields in active requests] + gap_type: missing | expired | revoked | suspended | verification_stale + detected_at: + severity: critical # accreditation gaps are always high or critical + affected_entity_uuids: [] # entities currently hosted at this provider + policy_response: + # Default: NOTIFY_AND_WAIT for fsi/sovereign; ESCALATE for standard/prod +``` + +The Recovery Policy evaluation runs through the standard +[`../convergence-engine/recovery-and-retry.md`](../convergence-engine/recovery-and-retry.md) +mechanism — accreditation gaps are first-class recovery triggers. + +### 13.2 Authorization evaluation at runtime + +The Governance Matrix evaluator (see +[`../convergence-engine/policy-evaluation.md`](../convergence-engine/policy-evaluation.md)) +consults Accreditation Monitor data on every outbound interaction. The +matrix evaluator: + +1. Resolves required accreditation from the data axis (e.g., PHI requires + HIPAA BAA) +2. Queries the target's active accreditations via the Accreditation Monitor +3. Verifies the accreditation is current and not suspended (via + `last_verified_at` and `last_result`) +4. Returns ALLOW / DENY / STRIP_FIELD per the matrix rule + +The Accreditation Monitor's verification currency (Section 8) feeds into +matrix evaluation: an accreditation with stale verification produces a +weaker effective trust than one externally verified yesterday. + +### 13.3 DCM deployment accreditation + +DCM deployments themselves can carry accreditations (a FedRAMP-authorized +DCM deployment, for example). DCM enforces: + +- `subject_type: dcm_deployment` accreditations registered as standard + accreditation artifacts +- Federation peer DCMs verify each other's deployment accreditation before + accepting federation messages (see + [`../runtime-features/federation-runtime.md`](../runtime-features/federation-runtime.md)) +- Accreditation Monitor verifies deployment accreditations on the same + schedule as provider accreditations + +### 13.4 Profile-governed accreditation constraints + +```yaml +accreditation_profile_config: + minimal: + verification_tier_minimum: expiry_only + air_gapped_fallback: expiry_only + stale_action_default: warn + + dev: + verification_tier_minimum: expiry_only + stale_action_default: warn + + standard: + verification_tier_minimum: document_currency + stale_action_default: warn + + prod: + verification_tier_minimum: document_currency + stale_action_default: escalate + + fsi: + verification_tier_minimum: external_registry + # ACM-004: expiry_only NOT permitted unless air_gapped_mode: true + stale_action_default: suspend + + sovereign: + verification_tier_minimum: external_registry + stale_action_default: suspend +``` + +These are enforced at accreditation registration: an accreditation with +`tier: expiry_only` in `fsi`/`sovereign` profile is rejected unless +`air_gapped_mode: true` is explicitly configured (`ACM-004`). diff --git a/architecture/governance-enforcement/contribution-pipeline.md b/architecture/governance-enforcement/contribution-pipeline.md new file mode 100644 index 0000000..39df7d7 --- /dev/null +++ b/architecture/governance-enforcement/contribution-pipeline.md @@ -0,0 +1,454 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Contribution Pipeline +Established: 2026-05-26 +Maps to: udlm/governance/federated-contribution-model.md +--- + +# Contribution Pipeline + +> **Implements contracts defined in UDLM**: +> [udlm/governance/federated-contribution-model.md](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md). +> UDLM defines the four contributor types (Platform Admin, Consumer/Tenant, +> Service Provider, Peer DCM), the contribution artifact types, the +> universal contribution pipeline, and what each contributor type may +> contribute (the wire-level contract that defines who may write what). DCM +> operationalizes the contribution store structure, the review queue and +> approval workflow (GitOps PR mechanics), the pipeline orchestration, +> consumer/provider/federation contribution enforcement. + +> **DCM-specific choice:** DCM uses GitOps PR workflow as its specific +> contribution transport. A peer DCM realization could use a different +> review channel (an internal review API, a custom UI, etc.) and still +> conform to the UDLM contributor + artifact contract. + +--- + +## 1. Contribution store structure + +All contributed artifacts are stored in the GitOps store with contributor +attribution. DCM uses this directory structure: + +``` +dcm-policy-store/ + system/ # Platform admin authored; DCM built-in + compliance/ + governance/ + orchestration/ + platform/ # Platform admin authored; deployment-specific + security/ + operations/ + tenant/ + / # Consumer/Tenant authored + gating/ + transformation/ + groups/ + provider/ + / # Provider authored + layers/ + policies/ + federated/ + / # Peer DCM contributed + registry/ + policy-templates/ + +dcm-registry/ + core/ # DCM project maintained + community/ # Community contributed (via community DCM) + / + organization/ # Organization contributed + / +``` + +### 1.1 Contributor attribution + +Every artifact in the store includes a `contributed_by` block in artifact +metadata: + +```yaml +artifact_metadata: + uuid: + handle: "tenant/payments/gating/cost-ceiling" + version: "1.0.0" + status: active + contributed_by: + contributor_type: consumer # platform_admin | consumer | service_provider | peer_dcm + actor_uuid: + tenant_uuid: # for consumer contributions + provider_uuid: # for provider contributions + peer_dcm_uuid: # for federation contributions + contribution_method: api # api | flow_gui | git_pr | federation_push + pr_url: "https://..." # if submitted via PR + reviewed_by: [] + reviewed_at: +``` + +`contributed_by` is immutable (`FCM-001`); set at creation, never modified. + +--- + +## 2. Review queue and approval workflow (GitOps PR mechanics) + +### 2.1 The universal contribution pipeline + +``` +Contributor authors an artifact + │ via one of three contribution surfaces: + ├── Flow GUI Canvas / Policy Authoring Interface + ├── Direct API: POST /api/v1/contribute/{artifact_type} + └── Git PR directly to target repository + ▼ Artifact submitted → status: developing (local only) + ▼ Contributor submits for review → status: proposed + │ For policies: shadow mode activates automatically + │ For other artifacts: staged in proposed state + ▼ Governance Matrix evaluates the contribution: + │ Is this contributor permitted to contribute this artifact type? + │ Is the artifact in the correct domain for this contributor? + │ Does the artifact pass structural validation? + │ DENY → rejected with reason; no further processing + ▼ Review flow (per profile + artifact type): + │ auto: artifact activates immediately + │ reviewed: one platform admin or designated reviewer approves + │ verified: two independent reviewers approve + │ authorized: N members of declared authority group record decisions + ▼ On approval → status: active + │ For policies: shadow mode results reviewed; full enforcement begins + │ For registry entries: available in registry + │ For catalog items: visible in service catalog (per RBAC) + ▼ Lifecycle managed by contributor (deprecate, retire) + Subject to platform admin override at any time +``` + +### 2.2 GitOps PR mechanics + +For Git-PR contributions: + +1. Contributor opens PR against the relevant repository (policy store, + registry, layers) +2. PR template enforces required metadata: contributor type, target domain, + review type, justification +3. CI runs automated validation (schema, structural, dependency resolution, + breaking change detector) +4. DCM's GitOps Adapter watches for PRs; on PR open, posts a Governance + Matrix evaluation result as a status check +5. If matrix DENY: PR blocked; comment explains why +6. If matrix ALLOW (or ALLOW_WITH_CONDITIONS): review continues via the + normal Git platform workflow (reviewer assignment, comments, approval) +7. On approval and CI green: PR merges; artifact transitions to `proposed` +8. Shadow validation period runs (per `shadow_review_period`) +9. After shadow period, artifact transitions to `active` + +### 2.3 Direct API contribution + +For API contributions (`POST /api/v1/contribute/{artifact_type}`): + +1. Request body includes the artifact YAML/JSON + contribution metadata +2. DCM Contribution Service runs structural validation +3. Governance Matrix evaluates; DENY returns 403 with rule_uuid +4. On ALLOW: artifact written to the contribution store with status + `proposed`; shadow mode activates if policy +5. If `review_type: auto` per active profile: artifact transitions to + `active` immediately +6. Otherwise: review pipeline kicks off (notification to required reviewers, + PR URL returned for tracking) + +Response includes: +- `contribution_uuid` +- `status` +- `review_required` +- `review_type` +- `reviewer_group` +- `pr_url` (if PR-based review applies) +- `shadow_results_url` (for policies) + +--- + +## 3. Pipeline orchestration + +The Contribution Service orchestrates the pipeline: + +| Stage | DCM service | +|---|---| +| Submission | API Gateway → Contribution Service | +| Validation | Contribution Service (structural) + Policy Manager (Governance Matrix) | +| Storage | GitOps Adapter (PR creation) OR Policy Store DB write | +| Notification | Notification Router (notifies reviewers per active profile) | +| Review tracking | Approval Manager (tracks decisions, quorum, deadline) | +| Shadow mode | Policy Manager (evaluates new policy in shadow against live traffic) | +| Activation | Policy Manager (transitions status, recompiles rule set, emits `policy.activated`) | + +Every stage emits an event to `pipeline_events`; downstream services +subscribe via `LISTEN/NOTIFY`. + +--- + +## 4. Consumer contribution enforcement + +DCM enforces consumer contribution scope at submission time. A consumer +submitting a policy with `domain: tenant` must belong to that Tenant; the +Governance Matrix evaluator checks this and rejects domain scope violations. + +```yaml +# DCM ships this rule pre-activated +governance_matrix_rule: + handle: "system/matrix/consumer-policy-scope" + enforcement: hard + match: + subject.type: consumer + data.artifact_type: policy + data.domain: [system, platform] # consumer attempting non-tenant domain + decision: DENY + reason: "Consumers may only contribute tenant-domain policies" +``` + +### 4.1 Consumer contribution API + +``` +POST /api/v1/contribute/policy +Authorization: Bearer +X-DCM-Tenant: +{ + "policy_type": "validation", + "handle": "tenant/payments/gating/cost-ceiling", + "domain": "tenant", + "enforcement_class": "operational", + "output_class": "advisory", + "match": {...}, + "output": {...}, + "shadow_mode": true, + "commit_message": "Add monthly cost ceiling compliance Validation Policy for Payments Tenant" +} + +Response 202: +{ + "contribution_uuid": "", + "policy_handle": "tenant/payments/gating/cost-ceiling", + "status": "proposed", + "shadow_mode": true, + "review_required": true, + "review_type": "reviewed", + "reviewer_group": "platform-admins", + "pr_url": "https://git.corp.example.com/dcm-policies/pulls/145", + "shadow_results_url": "/flow/api/v1/shadow/" +} +``` + +--- + +## 5. Provider contribution integration + +Providers contribute Resource Type Specs, Provider Catalog Items, Service +Layers, and provider-specific policies. DCM enforces: + +- Only the resource types declared at registration (`subject.declared_resource_types`) +- Catalog Items only for resource types the provider offers +- Service Layers only for resource types the provider offers +- Provider-domain policies only + +```yaml +# DCM ships this rule pre-activated +governance_matrix_rule: + handle: "system/matrix/provider-spec-scope" + enforcement: hard + match: + subject.type: service_provider + data.artifact_type: resource_type_spec + data.resource_type_fqn: + not_in: subject.declared_resource_types + decision: DENY + reason: "Providers may only contribute Resource Type Specs for resource types they offer" +``` + +### 5.1 Provider contribution API + +``` +POST /api/v1/provider/contribute/resource-type-spec +Authorization: mTLS + provider credential + +{ + "resource_type_fqn": "Storage.DistributedVolume", + "tier": "organization", + "version": "1.0.0", + "schema": {...}, + "portability_class": "provider_specific", + "commit_message": "Publish DistributedVolume resource type v1.0.0" +} +``` + +The submission is authenticated per +[`../credentials-and-auth/provider-callback.md`](../credentials-and-auth/provider-callback.md). + +--- + +## 6. Federation contribution synchronization + +Federation peers contribute artifacts subject to their federation trust +posture: + +| Peer trust posture | Review requirement | Artifact types permitted | +|---|---|---| +| `verified` | reviewed (standard+); auto (dev) | Registry entries, policy templates, service layers | +| `vouched` | reviewed always | Registry entries, service layers only | +| `provisional` | `authorized` tier approval | Registry entries only (no policies) | + +### 6.1 Federation contribution flow + +``` +Peer DCM publishes a contribution bundle: + Content: resource type specs, policy templates, or layers + Transport: federation tunnel (mTLS, signed, scoped credential) + Metadata: contributing_dcm_uuid, trust_posture, artifact_list + +Receiving DCM evaluates: + 1. Governance Matrix: is this peer permitted to contribute this artifact type? + 2. Signature verification: bundle signed by peer's private key? + 3. Structural validation: artifacts conform to DCM schemas? + 4. Domain scope check: artifacts within peer's permitted domain? + +On validation pass: + Artifacts enter proposed status in receiving DCM's policy/registry store + Review flow per receiving DCM's profile + peer trust posture + +On approval: + Artifacts become active in receiving DCM + Source attribution: contributed_by.dcm_uuid, contributed_by.trust_posture +``` + +### 6.2 Hub-spoke policy distribution + +In Hub-Spoke federation, Regional DCMs may subscribe to the Hub's policy +distribution feed: + +```yaml +hub_policy_distribution: + hub_dcm_uuid: + distribution_type: push # Hub pushes on policy change + auto_approve_from_hub: # profile-governed + minimal: true + dev: true + standard: true + prod: false # reviewed required even from verified Hub + fsi: false # verified required + sovereign: false # authorized approval required + policy_handles_subscribed: + - "system/compliance/hipaa/*" + - "system/governance/drift-remediation" +``` + +**Regional DCM always reviews before activating.** Hub cannot force-activate +policies on Regional DCMs. + +--- + +## 7. Artifact lifecycle across contributors + +### 7.1 Ownership and transfer + +Every artifact is owned by its contributor at creation. Transfer: + +- Consumer-authored policies transfer to a new Tenant admin when original + actor departs +- Provider-contributed catalog items remain owned by the provider registration +- Federation-contributed artifacts are owned by the contributing peer DCM + +Ownership transfer requires receiving owner's explicit acceptance. + +### 7.2 Platform admin override + +Platform admins can override any contributor's artifact lifecycle at any +time: + +- Suspend an active consumer-authored policy causing harm +- Retire a provider-contributed resource type spec no longer safe +- Reject a proposed federation contribution without public reason (security + discretion) + +Override actions are always audited with overriding admin's actor UUID and +reason (`FCM-005`). + +### 7.3 Deprecation and sunset + +Contributors deprecate their own artifacts: + +1. Notification to all consumers of the artifact +2. Sunset period declared (minimum P30D standard; P90D for prod/fsi/sovereign) +3. During sunset: new use warned; existing resources unaffected +4. After sunset: new use blocked +5. Platform admin confirms final retirement + +### 7.4 Orphaned artifacts + +When a contributor's access is revoked: + +- Active artifacts remain active — orphaned artifacts do not auto-deactivate +- Platform admin notified: "Artifact X has no active owner" +- Platform admin assigns new owner or explicitly retires +- Auto-retire-on-orphan is configurable per profile (`FCM-006`; enabled in + `sovereign`) + +--- + +## 8. Profile-governed contribution defaults + +```yaml +contribution_policy: + minimal: + consumer_policy_auto_approve: true + provider_spec_auto_approve: true + federation_contribution_auto_approve: true + shadow_mode_default: true # security: shadow always on + + dev: + consumer_policy_auto_approve: true + provider_spec_auto_approve: true + federation_contribution_auto_approve: false + shadow_mode_default: true + + standard: + consumer_policy_auto_approve: false + provider_spec_auto_approve: false + federation_contribution_auto_approve: false + shadow_mode_default: true + shadow_review_period: P7D + + prod: + consumer_policy_auto_approve: false + consumer_governance_matrix_requires: verified + provider_spec_requires: reviewed + federation_contribution_requires: reviewed + shadow_mode_default: true + shadow_review_period: P14D + + fsi: + consumer_policy_requires: verified + consumer_governance_matrix_requires: verified + provider_spec_requires: verified + federation_contribution_requires: verified + shadow_mode_default: true + shadow_review_period: P30D + min_shadow_divergence_review: true + + sovereign: + consumer_policy_requires: authorized + provider_spec_requires: authorized + federation_contribution_requires: authorized + shadow_mode_default: true + shadow_review_period: P30D + min_shadow_divergence_review: true + auto_retire_orphaned_artifacts: true +``` + +--- + +## 9. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `FCM-001-DCM` | DCM records `contributed_by` in artifact_metadata at creation; immutable | +| `FCM-002-DCM` | DCM enforces contributor permissions via Governance Matrix at submission; domain scope violations are hard DENY | +| `FCM-003-DCM` | DCM routes all contributions through the GitOps PR model unless profile grants auto-approval | +| `FCM-004-DCM` | DCM enters new policies in proposed (shadow) status by default; shadow results available before shadow_review_period expires | +| `FCM-005-DCM` | DCM permits platform admin override of any contributor's artifact lifecycle; override is audited | +| `FCM-006-DCM` | DCM does not auto-deactivate orphaned artifacts; platform admin assigns new owner or retires; sovereign profile auto-retires | +| `FCM-007-DCM` | DCM scopes federation contributions by peer trust posture: verified → reviewed (standard+); vouched → reviewed always; provisional → authorized | +| `FCM-008-DCM` | DCM enforces absolute contributor-tier scope limits; consumer-authored tenant-domain policies cannot affect system/platform domain regardless of declared match conditions | diff --git a/architecture/governance-enforcement/policy-profiles.md b/architecture/governance-enforcement/policy-profiles.md new file mode 100644 index 0000000..69cea9c --- /dev/null +++ b/architecture/governance-enforcement/policy-profiles.md @@ -0,0 +1,1333 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — Policy Organization +Maps to: udlm/observability/universal-groups.md +--- + +# DCM Data Model — Policy Organization: Groups, Profiles, and External Policy Evaluators + +> **Implements contracts defined in UDLM**: +> [udlm/observability/universal-groups.md](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md). +> UDLM defines the Universal Group Model — composable, cross-type group +> membership and the profile/collection grouping contract. DCM operationalizes +> Policy Groups (`group_class: policy_collection`) and Policy Profiles +> (`group_class: policy_profile`) as concrete expressions of that model, plus +> the external policy evaluator integration. + +> **Universal Group Model:** Policy Groups (`group_class: policy_collection`) and Policy Profiles (`group_class: policy_profile`) are expressions of the [Universal Group Model](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md). The structures defined in this document remain authoritative for policy-specific behavior; the universal model adds composability, cross-type membership, and the ability to include policy groups within composite groups. + +**Document Status:** ✅ Complete +**Related Documents:** [Scoring Model](../convergence-engine/scoring.md) | [Context and Purpose](https://github.com/croadfeldt/udlm/blob/main/foundations/context-and-purpose.md) | [Data Layers and Assembly](https://github.com/croadfeldt/udlm/blob/main/foundations/layering-and-versioning.md) | [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) | [data stores](https://github.com/croadfeldt/udlm/blob/main/contracts/storage-providers.md) + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: DATA + POLICY** +> +> Data: Policy Group and Profile artifacts. Policy: concern types and composition + + + +--- + +> **Standards and Compliance Reference:** See [Standards and Compliance Catalog](https://github.com/croadfeldt/udlm/blob/main/reference/standards-catalog.md) for the complete mapping of compliance frameworks (HIPAA, PCI DSS, FedRAMP, NIST SP 800-53, GDPR, DoD IL4) to DCM profiles and system requirements. + +## 1. Purpose + +DCM's Policy Engine is powerful — but power without usability is a barrier to adoption. This document defines the **policy organization model**: the structures that make DCM easy to configure correctly for any use case, from a home lab evaluation to a sovereign financial services deployment. + +Three concepts work together: + +- **Policy Groups** — cohesive collections of policies addressing a single identifiable concern (a technology, a compliance standard, a sovereignty requirement, a business process) +- **Policy Profiles** — complete DCM configurations for a specific use case, composed of Policy Groups +- **External Policy Evaluators** — external authoritative sources that supply policies directly into DCM, extending the provider model to its fifth type + +The relationship is compositional: + +``` +Policy Profile — complete use-case configuration + │ composed of + ▼ +Policy Groups — single-concern policy collections + │ composed of + ▼ +Policies — individual Transformation / Validation rules + │ optionally sourced from + ▼ +External Policy Evaluators — external authoritative policy sources +``` + +--- + +## 1a. Design Priority Order in Policy Profiles + +Profiles implement the DCM design priority order (see [Foundational Abstractions](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md)): + +1. **Security:** Profile defaults implement security correctly. Lower profiles have less strict enforcement — not absent security. +2. **Ease of use:** Profile defaults minimize configuration burden. `standard` profile should work for most deployments without customization. +3. **Extensibility:** Profiles compose with compliance domain overlays. Organizations add compliance requirements additively without rewriting base configuration. +4. **Fit for purpose:** Every profile must support the complete DCM lifecycle. + +**The `minimal` profile is not "security optional"** — it is the security model with minimal operational overhead. All security properties are present; thresholds and automation levels are relaxed. + +--- + +## 1b. Policy Authorship — Federated Contribution Model + +Policies in DCM are not exclusively authored by platform admins. The DCM federated contribution model enables all actor types to author policies within their permitted domain scope: + +- **Platform admins** — all domains, all policy types +- **Consumers / Tenant admins** — tenant domain policies (Validation, Transformation, Recovery, Lifecycle, Orchestration Flow, Governance Matrix rules scoped to their Tenant) +- **Service Providers** — provider-domain Validation policies for their resource types +- **Peer DCM instances** — policy templates contributed through verified federation relationships + +This is not a special case — it is the standard GitOps PR model applied to all contributor types. Consumer-authored policies go through the same lifecycle (developing → proposed → active) with appropriate review requirements per the active profile. See [Federated Contribution Model](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md) for the complete specification. + +--- + +## 2. Policy Groups +## 1a. Two-Dimensional Profile Model + + +### 1a.0 Profile — One Posture, Multiple Compliance Domains + +**A DCM deployment runs exactly one Deployment Posture and zero or more Compliance Domain Groups simultaneously.** This is the complete profile model. No new concept is needed. + +``` +Active DCM Governance = one Deployment Posture + [zero or more Compliance Domains] + +Examples: + prod + hipaa ← healthcare production + prod + hipaa + gdpr ← EU healthcare production + sovereign + fedramp-high + dod-il5 ← classified federal + standard ← general enterprise, no compliance overlay + dev + hipaa ← healthcare development (hipaa policies active, + but operational cost reduced by dev posture) +``` + +**Deployment Postures are mutually exclusive** — you cannot be both `prod` and `dev`. One posture governs the operational characteristics of the entire DCM deployment. + +**Compliance domains are additive** — HIPAA + FSI is valid and common. Each compliance domain group adds its own set of policies and constraints on top of the posture. They do not conflict with each other at the domain level (they govern different aspects of data handling); they may produce policy conflicts at the field level, which are resolved through the standard policy conflict resolution process. + +**The dev posture and compliance domains** — applying `dev` posture to a HIPAA-scoped deployment does not remove HIPAA obligations. It relaxes the *operational cost* of meeting them: less redundancy, shorter retention windows where permitted, advisory enforcement where HIPAA allows flexibility. The HIPAA compliance domain group remains active and its mandatory controls remain enforced. + +**Modules vs Profiles** — DCM uses both concepts with distinct meanings: +- A **Profile** is a governance configuration: it declares how DCM behaves and what operational and compliance requirements apply. +- A **Module** is a capability extension: it adds new functions to DCM (e.g., a HIPAA record validator, a custom resource type). Modules are not profiles and do not configure DCM behavior — they extend what DCM can do. + + +### 1a.1 The Gap in the Original Model + +The original six profiles (homelab → sovereign) are organized around **deployment posture** — how strict, how redundant, how governed. But organizations need compliance governance that is orthogonal to posture. A healthcare organization needs HIPAA controls regardless of whether they deploy at `standard` or `sovereign` posture. A payment processor needs PCI-DSS regardless of their redundancy profile. + +The new model makes compliance a first-class dimension that composes with posture: + +``` +Complete Profile = Deployment Posture Group + Compliance Domain Group(s) +``` + +### 1a.2 Dimension 1 — Deployment Posture Groups + +Posture groups govern how the DCM infrastructure itself behaves — redundancy, enforcement strictness, audit retention, tenancy model, cross-tenant defaults. These are the vertical axis from least to most governed. + +| Group Handle | Posture | Key Behaviors | +|-------------|---------|--------------| +| `system/group/posture-minimal` | Minimal | Advisory only; single instance; no redundancy | +| `system/group/posture-dev` | Development | Warn-not-block; basic logging; ephemeral defaults | +| `system/group/posture-standard` | Standard | Full enforcement; 3-replica; explicit cross-tenant | +| `system/group/posture-prod` | Production | Full enforcement + SLA; geo-replicated; cost governance | +| `system/group/posture-hardened` | Enterprise Hardened | 5-replica; 7-year audit; hard tenancy; dual approval | +| `system/group/posture-sovereign` | Sovereign | Air-gap; deny_all cross-tenant; 10-year audit; signed bundles | + +### 1a.3 Dimension 2 — Compliance Domain Groups + +Compliance domain groups govern what data handling, audit, and control requirements apply to resources managed by DCM. Multiple compliance domains can apply simultaneously. + +| Group Handle | Domain | Key Controls | +|-------------|--------|-------------| +| `system/group/compliance-fsi` | Financial Services | Basel III, SOX, Dodd-Frank financial controls | +| `system/group/compliance-pci-dss` | Payment Card | PCI-DSS v4 — payment card industry | +| `system/group/compliance-hipaa` | Healthcare | HIPAA/HITECH PHI handling and audit | +| `system/group/compliance-fedramp-moderate` | US Federal Moderate | FedRAMP Moderate — NIST 800-53 Moderate baseline | +| `system/group/compliance-fedramp-high` | US Federal High | FedRAMP High — NIST 800-53 High baseline | +| `system/group/compliance-dod-il2` | DoD IL2 | Public/unclassified defense data | +| `system/group/compliance-dod-il4` | DoD IL4 | Controlled Unclassified Information (CUI) | +| `system/group/compliance-dod-il5` | DoD IL5 | National Security Systems non-classified | +| `system/group/compliance-dod-il6` | DoD IL6 | Classified — maximum sovereign posture | +| `system/group/compliance-government` | Government General | General government/public sector controls | +| `system/group/compliance-gdpr` | EU Data Protection | GDPR data residency and rights | +| `system/group/compliance-iso27001` | ISO 27001 | Information security management | +| `system/group/compliance-nist-800-53` | NIST 800-53 | NIST security control framework | +| `system/group/compliance-soc2` | SOC 2 | Service organization controls | +| `system/group/compliance-nerc-cip` | Critical Infrastructure | Energy/utilities NERC CIP | +| `system/group/compliance-sovereign` | Sovereign/Classified | Sovereign deployment, air-gap, classified | + +### 1a.4 Compliance Domain Group Contents + +#### `system/group/compliance-hipaa` + +HIPAA/HITECH controls for Protected Health Information (PHI): +- PHI field classification enforcement (fields containing PHI must be tagged `phi: true`) +- PHI access control (only roles with `phi_authorized: true` may access PHI-tagged fields) +- Audit retention: P6Y minimum (HIPAA requires 6 years from creation or last use) +- Encryption at rest: AES-256 required for all PHI storage +- Transmission security: TLS 1.3 minimum for PHI in transit +- Breach notification workflow: sovereignty_violation_record triggers HIPAA breach assessment +- Business Associate Agreement (BAA): providers handling PHI must hold a **verified** BAA accreditation (Accreditation-Monitor-checked) — the HIPAA gate keys on the presence of that verified record (`baa_active`), never on a self-declared `baa_in_place` boolean +- Right to Access: consumer data export capability required for PHI entities +- Minimum Necessary standard: data_request_spec on Mode 4 providers limited to minimum PHI fields + +#### `system/group/compliance-government` + +General government and public sector controls: +- Data classification mandatory on all resources (classification_level field required) +- Cross-boundary controls: data cannot cross classification levels without explicit policy +- Audit retention: P10Y minimum +- Air-gap capability required for Sensitive compartments +- Actor authentication must declare clearance level in external_identity claims +- government_access_risk must be established from the provider's verified jurisdiction/attestation (Accreditation-Monitor / sovereignty accreditation), not a self-declared field — a provider cannot self-attest away foreign-government access exposure + +#### `system/group/compliance-fedramp-moderate` + +FedRAMP Moderate authorization controls: +- NIST 800-53 Rev 5 Moderate baseline implemented as policy group +- FedRAMP-authorized provider preference injected as placement constraint +- Continuous monitoring: drift detection mandatory; P24H maximum drift resolution window +- Incident response: webhook required for security events (audit.integrity_break, drift.escalated) +- POA&M tracking: `poam_status` field in status_metadata on all policy artifacts +- Boundary protection: explicit ingress/egress control documentation + +#### `system/group/compliance-dod-il4` + +DoD Impact Level 4 — Controlled Unclassified Information: +- Inherits compliance-fedramp-moderate +- CUI handling markers on all data fields containing controlled information +- Sovereign posture within US jurisdiction boundary +- Provider sovereignty_declaration must exclude foreign sub-processors +- CMMC Level 2 cyber hygiene controls + +#### `system/group/compliance-sovereign` + +Sovereign and classified deployment controls: +- All data must remain within declared sovereignty boundary +- Air-gap capability: required via a **verified** air-gap attestation (accreditation record), not a self-declared `air_gap_capable` boolean +- Signed bundle import only — no live registry connectivity +- deny_all cross-tenant cross-boundary data flows +- Hardware security module (HSM) required for key management +- Audit records: 10-year retention; cryptographic signing required + +### 1a.5 Profile Composition Model + +The six built-in profiles become explicit posture+compliance compositions: + +```yaml +system/profile/homelab: + policy_groups: [system/group/posture-minimal] + +system/profile/dev: + extends: system/profile/homelab + policy_groups: [system/group/posture-dev] + +system/profile/standard: + extends: system/profile/dev + policy_groups: [system/group/posture-standard] + +system/profile/prod: + extends: system/profile/standard + policy_groups: [system/group/posture-prod] + +system/profile/fsi: + extends: system/profile/prod + policy_groups: + - system/group/posture-hardened + - system/group/compliance-fsi + - system/group/compliance-pci-dss + - system/group/compliance-iso27001 + +system/profile/sovereign: + extends: system/profile/fsi + policy_groups: + - system/group/posture-sovereign + - system/group/compliance-sovereign +``` + +### 1a.6 DCM Built-In Extended Profiles + +In addition to the six core profiles, DCM ships extended profiles for common compliance domains: + +```yaml +system/profile/hipaa-prod: + extends: system/profile/prod + policy_groups: + - system/group/compliance-hipaa + - system/group/compliance-iso27001 + description: "Production infrastructure with HIPAA/HITECH compliance" + +system/profile/hipaa-sovereign: + extends: system/profile/sovereign + policy_groups: + - system/group/compliance-hipaa + description: "Sovereign deployment with HIPAA/HITECH compliance — highest healthcare posture" + +system/profile/fedramp-moderate: + extends: system/profile/prod + policy_groups: + - system/group/compliance-fedramp-moderate + - system/group/compliance-nist-800-53 + description: "FedRAMP Moderate authorized deployment" + +system/profile/fedramp-high: + extends: system/profile/sovereign + policy_groups: + - system/group/compliance-fedramp-high + - system/group/compliance-nist-800-53 + description: "FedRAMP High authorized deployment" + +system/profile/government: + extends: system/profile/prod + policy_groups: + - system/group/compliance-government + - system/group/compliance-nist-800-53 + description: "General government and public sector deployment" + +system/profile/dod-il4: + extends: system/profile/sovereign + policy_groups: + - system/group/compliance-dod-il4 + - system/group/compliance-fedramp-high + - system/group/compliance-nist-800-53 + description: "DoD Impact Level 4 — Controlled Unclassified Information" + +system/profile/dod-il5: + extends: system/profile/dod-il4 + policy_groups: + - system/group/compliance-dod-il5 + description: "DoD Impact Level 5 — National Security Systems non-classified" + +system/profile/dod-il6: + extends: system/profile/dod-il5 + policy_groups: + - system/group/compliance-dod-il6 + - system/group/compliance-sovereign + description: "DoD Impact Level 6 — Classified" +``` + +### 1a.7 Organization Custom Profiles — Composition Examples + +```yaml +# Healthcare with federal cloud authorization +org/profile/hipaa-fedramp: + extends: system/profile/fedramp-moderate + policy_groups: + - system/group/compliance-hipaa + description: "Healthcare workloads on FedRAMP Moderate platform" + +# Multi-compliance financial + healthcare +org/profile/fsi-hipaa: + extends: system/profile/fsi + policy_groups: + - system/group/compliance-hipaa + description: "FSI-grade deployment for organizations managing both financial and health data" + +# Tenant-level compliance override — platform is standard; this Tenant is PCI-scoped +# (declared in tenant_config — not profile) +tenant_compliance_overlay: + active_profile: system/profile/standard # platform posture + compliance_groups: + - system/group/compliance-pci-dss # this Tenant handles payment cards + - system/group/compliance-hipaa # this Tenant also handles PHI + # Other Tenants on same platform have standard posture, no compliance overlay +``` + +### 1a.8 Compliance at Tenant Level + +Compliance domain groups may apply at Tenant level — different Tenants on the same DCM deployment can have different compliance domains: + +```yaml +tenant_config: + active_profile: system/profile/prod # posture from platform + compliance_groups: + - system/group/compliance-hipaa # this Tenant handles PHI + - system/group/compliance-pci-dss # this Tenant processes payments +``` + +This is the critical capability: **one DCM deployment, multiple compliance postures per Tenant**. A hospital system can run a single DCM with: clinical Tenants (HIPAA), billing Tenants (HIPAA + PCI-DSS), and administrative Tenants (standard) — all on the same platform profile. + +### 1a.9 System Policies — Profile Composition + +| Policy | Rule | +|--------|------| +| `PROF-001` | Profiles compose a Deployment Posture Group with zero or more Compliance Domain Groups. Posture groups govern DCM infrastructure behavior. Compliance domain groups govern data handling, audit, and control requirements. | +| `PROF-002` | Compliance Domain Groups may be applied at platform level (all Tenants) or Tenant level (specific Tenants). Tenant-level compliance groups are additive — they do not replace platform-level groups. | +| `PROF-003` | DCM ships built-in Compliance Domain Groups for: FSI, PCI-DSS, HIPAA/HITECH, FedRAMP Moderate, FedRAMP High, DoD IL2-IL6, Government, GDPR, ISO 27001, NIST 800-53, SOC2, NERC-CIP, and Sovereign/Classified. Organizations extend these groups or compose them into custom profiles. | +| `PROF-004` | The `implementation_posture` concern_type Policy Groups (provenance model, auth simplicity, deployment complexity) are independent of compliance domain — organizations select their implementation posture separately from their compliance requirements. | + +--- + + +### 2.1 Definition + +A **Policy Group** is a versioned, cohesive collection of policies that together address a **single identifiable concern**. The group is the unit of reuse — activate a group to enable a concern, not individual policies. + +```yaml +policy_group: + artifact_metadata: + uuid: + handle: "system/group/pci-dss" + version: "1.2.0" + status: active + owned_by: + display_name: "DCM Project Team" + notification_endpoint: + + name: "PCI-DSS v4" + description: > + Policy group implementing PCI-DSS v4 controls relevant to + DCM-managed infrastructure. Enforces encryption standards, + network segmentation, access control, and audit requirements + for resources in PCI scope. + + concern_type: compliance + concern_tags: [pci-dss, financial, encryption, network-segmentation] + extends: null # or another group handle — inherits all parent policies + + # Source — locally authored or from a External Policy Evaluator + source: + type: + provider_uuid: + provider_group_reference: + on_provider_update: + # proposed: provider updates require local review before activation + # active: provider updates activate immediately (trusted providers only) + + # Constituent policies + policies: + - policy_uuid: + handle: "system/gating/pci-encryption-aes256" + description: "Enforce AES-256 on all PCI-scoped storage" + placement_phase: pre + - policy_uuid: + handle: "system/validation/pci-network-segmentation" + description: "Validate network segment isolation for PCI resources" + placement_phase: pre + - policy_uuid: + handle: "system/transformation/pci-classification-inject" + description: "Auto-inject PCI classification on scoped resources" + placement_phase: pre + - policy_uuid: + handle: "system/gating/pci-audit-retention" + description: "Enforce 10-year audit retention for PCI evidence" + placement_phase: pre + + # Activation scope — surgical application within a profile + activation_scope: + resource_types: [] # empty = all resource types + tenant_tags: [pci-scope] # only Tenants tagged pci-scope + regions: [] # empty = all regions + + # Conflict declarations + conflicts_with: + - group_handle: "system/group/dev-defaults" + reason: "PCI requires blocking enforcement; dev-defaults uses warn-only" + resolution: this_group_wins +``` + +### 2.2 Concern Types + +| Type | Description | Examples | +|------|-------------|---------| +| `technology` | Policies specific to a technology or provider | kubevirt, openstack, kubernetes, vmware | +| `compliance` | Regulatory or standards compliance | pci-dss, iso-27001, nist-800-53, fedramp | +| `sovereignty` | Data residency, jurisdictional, air-gap | gdpr-eu, air-gap, data-residency-uk | +| `business` | Business process, cost, lifecycle governance | cost-governance, ephemeral-resources, chargeback | +| `operational` | Operational posture, defaults, SLAs | dev-defaults, hard-tenancy, sla-enforcement | +| `security` | Security controls and posture | data-classification, zero-trust, encryption-baseline | +| `implementation_posture` | Implementation complexity vs capability trade-offs | provenance-full-inline, provenance-deduplicated, single-instance-deployment, advisory-policies-only | + +### 2.3 Group Inheritance + +A Policy Group may extend another group — inheriting all its policies and overriding or adding to them: + +```yaml +policy_group: + handle: "org/group/pci-dss-extended" + extends: "system/group/pci-dss" + # Inherits all system/group/pci-dss policies + # Additional policies added below are on top of the parent + policies: + - policy_uuid: + handle: "org/gating/our-pci-additional-control" +``` + +### 2.4 DCM Built-In Policy Groups + +DCM ships the following policy groups as part of its standard distribution: + +| Handle | Concern | Description | +|--------|---------|-------------| +| `system/group/core-minimal` | operational | Absolute minimum — UUID requirements, basic well-formedness | +| `system/group/dev-defaults` | operational | Warn-not-block, 90-day TTL defaults, single-auth cross-tenant | +| `system/group/ephemeral-resources` | business | TTL enforcement, auto-expiry, short-lived resource defaults | +| `system/group/audit-basic` | operational | Basic audit logging, 90-day retention | +| `system/group/audit-compliance` | compliance | Compliance-grade audit, configurable retention | +| `system/group/data-classification` | security | Data classification tagging and handling rules | +| `system/group/cost-governance` | business | Budget enforcement, cost attribution, TTL governance | +| `system/group/sla-enforcement` | operational | SLA tracking, availability commitments | +| `system/group/hard-tenancy` | operational | Full tenant isolation, deny_all cross-tenant default | +| `system/group/explicit-cross-tenant` | operational | Explicit cross-tenant authorization requirement (XTA-001 through XTA-005) | +| `system/group/zero-trust` | security | Zero-trust network and identity enforcement | +| `system/group/encryption-baseline` | security | AES-256 minimum, TLS 1.3, key rotation | +| `system/group/pci-dss` | compliance | PCI-DSS v4 controls | +| `system/group/gdpr-eu` | sovereignty | GDPR data residency and handling | +| `system/group/nist-800-53` | compliance | NIST 800-53 control implementation | +| `system/group/iso-27001` | compliance | ISO 27001 controls | +| `system/group/fedramp-moderate` | compliance | FedRAMP Moderate baseline | +| `system/group/air-gap` | sovereignty | Air-gapped deployment constraints | +| `system/group/fsi-audit` | compliance | Financial services audit retention (7-year minimum) | +| `system/group/lifecycle-ttl-enforcement` | operational | Lifecycle time constraint enforcement (LTC-001 through LTC-004) | +| `system/group/kubevirt` | technology | KubeVirt provider-specific policies | +| `system/group/openstack` | technology | OpenStack provider-specific policies | +| `system/group/vmware` | technology | VMware provider-specific policies | +| `system/group/provenance-full-inline` | implementation_posture | Model A — all provenance inline on entity records; simplest; highest storage cost | +| `system/group/provenance-deduplicated` | implementation_posture | Model B — content-addressed deduplication; recommended; 95-99% storage reduction; lossless | +| `system/group/provenance-tiered-archive` | implementation_posture | Model C — hot/warm/cold tiers; balances cost and access speed | +| `system/group/provenance-deduplicated-tiered` | implementation_posture | Model B+C — maximum efficiency for very large-scale deployments | + +--- + +## 3. Policy Profiles + +### 3.1 Definition + +A **Policy Profile** is a named, versioned, curated composition of Policy Groups that together configure DCM for a specific use case. Activating a profile is the primary configuration mechanism — most deployments should activate a profile and then add organization-specific groups on top rather than configuring policies individually. + +```yaml +policy_profile: + artifact_metadata: + uuid: + handle: "system/profile/fsi" + version: "1.0.0" + status: active + owned_by: + display_name: "DCM Project Team" + + name: "FSI Production" + description: > + Policy profile for Financial Services production deployments. + Enforces hard tenancy, regulatory-grade audit retention, + full sovereignty controls, and explicit cross-tenant authorization. + + target_use_case: fsi_production + extends: "system/profile/prod" # inherits all prod groups + overrides + + enforcement_summary: + tenancy: hard_tenancy_required + audit_retention_years: 7 + sovereignty: full + cross_tenant_default: explicit_only + policy_enforcement: blocking + immutable_fields: [sovereignty_zone, classification_level, audit_retention] + policy_version_pinning: permitted_with_elevation + + policy_groups: + - group_handle: "system/group/fsi-audit" + - group_handle: "system/group/pci-dss" + - group_handle: "system/group/gdpr-eu" + - group_handle: "system/group/hard-tenancy" + - group_handle: "system/group/explicit-cross-tenant" + - group_handle: "system/group/encryption-baseline" +``` + +### 3.2 DCM Built-In Profiles + +DCM ships six profiles covering the spectrum from homelab to sovereign: + +#### `system/profile/homelab` — Home Lab / Evaluation + +```yaml +handle: "system/profile/homelab" +name: "Homelab" +extends: null +description: > + The single-operator on-ramp (UDLM ADR-017) — home lab, local testing, and evaluation. + Most controls advisory only. Single Tenant auto-created on first use. + No audit requirements. No sovereignty enforcement. + +enforcement_summary: + tenancy: optional + audit_retention: none + sovereignty: none + cross_tenant_default: allow_all + policy_enforcement: advisory # policies warn but do not block + time_constraints: optional + +auto_tenant: + enabled: true + default_tenant_handle: "default" + # TEN-001 satisfied silently — no explicit Tenant declaration required + +policy_groups: + - group_handle: "system/group/core-minimal" +``` + +#### `system/profile/dev` — Development Environments + +```yaml +handle: "system/profile/dev" +name: "Development" +extends: "system/profile/homelab" +description: > + Development environment profile. Tenancy recommended but not blocking. + Basic logging. Ephemeral resource defaults. Warn-not-block enforcement. + +enforcement_summary: + tenancy: recommended + audit_retention: basic + sovereignty: none + cross_tenant_default: operational_only + policy_enforcement: warn_only + default_ttl: P90D # dev resources default to 90-day TTL + +policy_groups: + - group_handle: "system/group/dev-defaults" + - group_handle: "system/group/ephemeral-resources" + - group_handle: "system/group/audit-basic" +``` + +#### `system/profile/standard` — General Enterprise Production + +```yaml +handle: "system/profile/standard" +name: "Standard" +extends: "system/profile/dev" +description: > + General enterprise production profile. Full policy enforcement. + Tenancy required. Explicit cross-tenant authorization. Basic + data classification. Compliance-grade audit. + +enforcement_summary: + tenancy: required + audit_retention: compliance_grade + sovereignty: configurable + cross_tenant_default: explicit_only + policy_enforcement: blocking + +policy_groups: + - group_handle: "system/group/data-classification" + - group_handle: "system/group/audit-compliance" + - group_handle: "system/group/explicit-cross-tenant" + - group_handle: "system/group/encryption-baseline" +``` + +#### `system/profile/prod` — Production with SLA Requirements + +```yaml +handle: "system/profile/prod" +name: "Production" +extends: "system/profile/standard" +description: > + Production profile with SLA enforcement, cost governance, and + full lifecycle constraint enforcement. + +enforcement_summary: + tenancy: required + audit_retention: compliance_grade + sovereignty: configurable + cross_tenant_default: explicit_only + policy_enforcement: blocking + +policy_groups: + - group_handle: "system/group/cost-governance" + - group_handle: "system/group/sla-enforcement" + - group_handle: "system/group/lifecycle-ttl-enforcement" +``` + +#### `system/profile/fsi` — Financial Services Production + +```yaml +handle: "system/profile/fsi" +name: "FSI Production" +extends: "system/profile/prod" +description: > + Financial services production. Hard tenancy. 7-year audit retention. + Full sovereignty enforcement. PCI-DSS and GDPR compliance. + +enforcement_summary: + tenancy: hard_tenancy_required + audit_retention_years: 7 + sovereignty: full + cross_tenant_default: explicit_only + policy_enforcement: blocking + +policy_groups: + - group_handle: "system/group/fsi-audit" + - group_handle: "system/group/pci-dss" + - group_handle: "system/group/gdpr-eu" + - group_handle: "system/group/hard-tenancy" +``` + +#### `system/profile/sovereign` — Air-Gapped / Sovereign Deployments + +```yaml +handle: "system/profile/sovereign" +name: "Sovereign" +extends: "system/profile/fsi" +description: > + Maximum control profile for air-gapped, sovereign, and highest-security + deployments. Complete tenant isolation. Zero external dependencies. + Maximum audit and sovereignty enforcement. + +enforcement_summary: + tenancy: hard_tenancy_required + audit_retention_years: 10 + sovereignty: maximum + cross_tenant_default: deny_all + policy_enforcement: blocking + immutable_ceiling_on_all_sovereignty_fields: true + +policy_groups: + - group_handle: "system/group/air-gap" + - group_handle: "system/group/zero-trust" +``` + +### 3.3 Profile Inheritance Chain + +``` +system/profile/sovereign + extends: system/profile/fsi + extends: system/profile/prod + extends: system/profile/standard + extends: system/profile/dev + extends: system/profile/homelab + extends: null (base) +``` + +Each level adds groups without replacing parent groups. An organization extending a DCM profile only needs to declare what differs: + +```yaml +# Organization custom profile +org/profile/my-prod: + extends: system/profile/prod + policy_groups: + - group_handle: "org/group/our-naming-conventions" + - group_handle: "org/group/our-cost-centers" + - group_handle: "system/group/iso-27001" # add a DCM compliance group +``` + +### 3.4 Profile Activation + +**Profiles are platform-scoped: one active profile per DCM instance** (UDLM ADR-007 §5; `profile-resolution.md` §5 owns the resolution mechanics). A profile is a composed **set with a floor** — activating it never disables capabilities above the floor. If two postures are genuinely needed, run two instances; that is the supported pattern. + +```yaml +# DCM installation default (first boot; the platform admin may activate another) +installation_config: + default_profile: "system/profile/homelab" + +# Platform-level — THE active profile for this instance +platform_config: + active_profile: "system/profile/prod" +``` + +> **Rejected direction (recorded so it does not silently return):** a three-level activation model with +> per-tenant profile overrides and `minimum/maximum_tenant_profile` ceilings. Rejected by ADR-007 §5 — +> tenant-level posture splits are served by separate instances; group-scoping is ADR-007's recorded +> *future* direction, not a tenant override. + +### 3.5 Profile Conflict Resolution + +When a profile is activated, DCM runs conflict detection across all constituent group policies — same ingestion conflict detection that layers use. Conflicts must be resolved before a profile is marked `active`. + +Conflict resolution order: +1. **Explicit `conflicts_with` declarations** on groups — use declared resolution rule +2. **Priority schema** — higher numeric priority wins +3. **Domain authority** — `system` beats `platform` beats `tenant` **at activation only**: this ranks *whose + group wins a declaration conflict* (system floors are unoverridable, ADR-013). It is the **opposite axis** + from evaluation-time precedence, where the *most specific* matching rule wins within what the floors permit + (`entity > resource_type > tenant > platform > system` — policy-evaluation.md §1–3, the owner). The two + orders never apply to the same question. +4. **Unresolved** — profile activation fails with detailed conflict report + +### 3.6 Profile Shadow Validation + +When a profile is in `proposed` status, all its constituent policies run in shadow mode — the same proposed policy shadow execution model. The Validation Dashboard shows the aggregate impact across all policies in the profile before activation. This enables safe preview of what a profile upgrade would do to an existing deployment. + +--- + +## 4. Policy Evaluation Modes + +DCM supports two policy evaluation modes. The distinction is whether DCM or an external system performs the evaluation — not how policies are delivered to the evaluator. + +### 4.1 Internal Mode — DCM Evaluates + +In Internal mode, the Policy Manager evaluates all policies using its embedded OPA engine. Policies can arrive through any delivery mechanism: + +| Delivery | Description | +|----------|-------------| +| **API / GitOps** | Policies stored in DCM's database, managed via API or Git ingress adapter | +| **OPA Bundle** | Standard OPA bundle protocol — point OPA at a bundle server URL | +| **External Schema** | Policies in non-Rego format (e.g., XACML, custom JSON) naturalized to Rego by DCM before evaluation | + +All three delivery mechanisms result in the same thing: Rego policies evaluated by OPA against the request payload. Where OPA runs (embedded Go library, sidecar container, or remote OPA instance) is a deployment topology decision — not a mode distinction. + +**Policy registration:** +```yaml +policy: + handle: "vm-size-limits" + policy_type: gating + delivery: + mode: push # or: pull, opa_bundle, external_schema + source_url: "https://git.corp/policies" # for pull/bundle modes + format: rego # or: xacml, custom_json (naturalized to rego) + activation: active # or: proposed (shadow mode) + # trust_level is DCM-ASSIGNED, not accepted from this payload. Registration defaults to `untrusted`; + # raising it to `verified`/`trusted` (compliance Validation Policy = deny authority) requires the PROF-007 formal + # elevation workflow (dual-approval + P7D shadow). A trust_level supplied in the submission is ignored. + trust_level: untrusted # DCM-assigned default; trusted/verified only via PROF-007 +``` + +**Trust levels (Internal mode):** +- `trusted` — compliance Validation Policy authority (can deny requests) +- `verified` — Transformation and Validation authority only +- `untrusted` — advisory only (shadow mode enforcement) + +### 4.2 External Mode — External Provider Evaluates + +In External mode, DCM sends evaluation context to an external endpoint. The external system evaluates and/or enriches the data, and returns a structured result. DCM does not see the policy logic — it trusts the results within scoped bounds. + +**External evaluation can:** +- **Evaluate** — return pass/fail, score, or recommendation +- **Enrich** — inject additional fields into the payload (risk scores, compliance citations, cost predictions, organizational context) +- **Both** — combined decision + enrichment in a single response + +**Registration:** +```yaml +policy: + handle: "compliance-scanner" + policy_type: validation + delivery: + mode: external # External mode + endpoint: "https://compliance.corp/api/evaluate" + auth: mtls + data_request_spec: # data minimization — only declared fields sent + fields: [resource_type, sovereignty_zone, data_classification, tenant_uuid] + on_unavailable: gate # fail-closed — unknown is not safe + trust_level: verified # minimum verified for enrichment +``` + +### 4.3 External Mode Governance (BBQ-001 through BBQ-009) + +External evaluation introduces governance concerns that Internal mode does not: + +| ID | Requirement | +|----|-------------| +| BBQ-001 | Data sovereignty check before any query is sent to an external endpoint | +| BBQ-002 | Data minimization — only fields declared in `data_request_spec` are sent | +| BBQ-003 | If the external endpoint is outside the entity's sovereignty zone, the query is blocked unless explicitly authorized | +| BBQ-004 | Full audit record per query-response cycle, including `audit_token` for cross-system correlation | +| BBQ-005 | Default failure behavior is `gate` — if the external system is unavailable, the request is denied (fail-closed) | +| BBQ-006 | Cached results must include the original query timestamp and validity period in provenance | +| BBQ-007 | Fields injected by external enrichment carry standard field-level provenance: `source_type: external_external_policy_evaluator`, `source_uuid`, and `audit_token` | +| BBQ-008 | The override control model applies to enrichment-injected fields — a compliance Validation Policy may restrict or refuse external enrichment on specific fields | +| BBQ-009 | External enrichment requires minimum `verified` trust level; compliance Validation Policy authority requires `trusted` with dual-approval elevation | + +### 4.4 Policy Sources and Policy Groups + +Policies from any source (Internal or External) participate in the same Policy Group mechanism. A Policy Group composes multiple policies into a named, versioned, reviewable unit with explicit conflict declarations: + +```yaml +policy_group: + handle: "pci-dss-v4-controls" + policies: + - ref: "card-data-encryption" # Internal — Rego policy in DCM + - ref: "network-segmentation-check" # Internal — OPA bundle + - ref: "compliance-scanner" # External — calls external endpoint + activation_scope: + resource_types: ["*"] + tenant_tags: ["pci"] +``` + +### 4.5 Policy Health and Lifecycle + +- **Internal policies:** Health is determined by OPA engine health. If OPA is unavailable, all Internal policies are degraded. +- **External policies:** Health is determined by endpoint availability. Each external endpoint has a health check (HTTP GET to a declared health URL). Unhealthy external policies trigger their `on_unavailable` behavior (default: `gate`). +- **Deprecation:** Policies follow the `active → deprecated → retired` lifecycle. Deprecated policies fire with a warning in the audit trail. Retired policies are no longer evaluated. + +--- + +## 5. Lifecycle Time Constraints + +### 5.1 Concept + +Lifecycle time constraints declare **when a resource should cease to exist or trigger a lifecycle action**. They are a first-class field on any resource entity — not metadata, not a tag, but a governed field that follows the standard data model precedence and override control. + +### 5.2 Constraint Types + +| Type | Format | Description | +|------|--------|-------------| +| `ttl` | ISO 8601 duration (e.g., `P14D`) | Relative — expires N time after the reference point | +| `expires_at` | ISO 8601 timestamp | Absolute — expires at a specific calendar date/time | + +When both are declared, the **earliest expiry wins** (LTC-004). + +### 5.3 Data Model + +```yaml +lifecycle_constraints: + ttl: + duration: P14D # ISO 8601 duration — 14 days + reference_point: realization_timestamp # created_at | realization_timestamp | last_modified + on_expiry: + metadata: + override: allow # standard override control applies + basis_for_value: "Consumer declared ephemeral — 14-day lab resource" + + expires_at: + timestamp: "2026-06-30T23:59:59Z" + on_expiry: notify + metadata: + override: immutable + locked_by_policy_uuid: + basis_for_value: "Project deadline — resource must not persist beyond Q2" + + # Enforcement behavior + enforcement: + warn_before_expiry: P1D # warn 1 day before expiry + warn_notification_endpoint: + grace_period: PT1H # 1 hour grace after expiry before action + on_grace_period_expiry: +``` + +### 5.4 Precedence + +Lifecycle time constraints follow the standard data model precedence chain: + +``` +Base Layer default (lowest — e.g., "no TTL by default") + ↓ +Core Layer (e.g., "all dev environment resources: TTL 90 days") + ↓ +Service Layer (e.g., "ephemeral compute: TTL 7 days") + ↓ +Request Layer (consumer declared TTL) + ↓ +Transformation Policy (enrich TTL from business context) + ↓ +Compliance Validation Policy (highest — may lock TTL as immutable) +``` + +A consumer can declare `ttl: P14D` in their request. A compliance Validation Policy can override this to `P7D` and lock it immutable if organizational policy mandates shorter maximum lifetimes. A Core Layer can set default TTLs for resource classes. The provenance chain records every modification. + +### 5.5 Expiry Enforcement + +The **Lifecycle Constraint Enforcer** is a DCM control plane component that: +- Monitors all realized entities against their declared lifecycle constraints +- Fires the configured `on_expiry` action when a constraint is reached +- Records the enforcement action in provenance and the Audit Store +- Emits expiry warnings `warn_before_expiry` duration before the deadline + +Expiry enforcement is a DCM concern — not a provider concern. The provider does not need to know about or implement TTL logic. + +### 5.6 System Policies + +| Policy | Rule | +|--------|------| +| `LTC-001` | Lifecycle time constraints follow standard data model precedence — layers, request, policies | +| `LTC-002` | Compliance validation policies may lock lifecycle constraints as `override: immutable` or `immutable_ceiling: absolute` | +| `LTC-003` | Expiry enforcement is a DCM control plane function — not a provider concern | +| `LTC-004` | When multiple time constraints exist on an entity, the earliest expiry wins | +| `LTC-005` | Expired entities that fail to execute their `on_expiry` action enter `PENDING_EXPIRY_ACTION` state and trigger an escalation | + +--- + +## 6. Cross-Tenancy Authorization Model + +### 6.1 Default Stance — Closed + +Cross-tenant information sharing is **closed by default**. No cross-tenant relationship of any nature is permitted unless explicitly authorized. This applies to both operational dependencies and informational relationships. + +The hard tenancy spectrum: + +| Setting | Meaning | +|---------|---------| +| `deny_all` | No cross-tenant relationships of any nature | +| `explicit_only` | All cross-tenant must be explicitly authorized (DEFAULT) | +| `operational_permitted` | Operational cross-tenant permitted; informational requires explicit auth | +| `allow_all` | All cross-tenant permitted — requires justification; not available in sovereign profile | + +The default shifts from the Q59 model's `operational_only` to `explicit_only`. Informational sharing is no longer implicitly open — every informational cross-tenant relationship requires an explicit authorization record. + +### 6.2 Cross-Tenant Authorization Record + +```yaml +cross_tenant_authorization: + artifact_metadata: + uuid: + handle: "tenant-a/auth/shared-network-read" + version: "1.0.0" + status: active + owned_by: + display_name: "Infrastructure Tenant Admin" + + # WHO + authorized_consumer_tenant_uuid: + authorized_actor_constraint: + roles: [service_account, automation] # null = any actor in the tenant + specific_uuids: [] # specific actor UUIDs if needed + + # WHAT + resource_entity_uuid: + resource_type_uuid: + permitted_fields: + - field: network_segment + - field: vlan_id + # empty list = all fields permitted + permitted_relationship_natures: [informational, operational] + + # WHEN + valid_from: + expires_at: + + # WHERE + permitted_in_regions: [eu-west, eu-central] # null = any region + sovereignty_constraints: + must_honor_consuming_tenant_sovereignty: true + must_honor_owning_tenant_sovereignty: true + + # GOVERNANCE + authorized_by_policy_uuid: + authorization_level: + # Hierarchy: field_specific > resource_specific > tenant_global + # More specific = higher precedence +``` + +### 6.3 Authorization Hierarchy + +More specific authorizations take precedence over broader ones: + +``` +field_specific ← highest precedence — only these exact fields on this entity + │ +resource_specific ← this entity — all permitted fields + │ +tenant_global ← all entities in this Tenant — broadest +``` + +If a tenant-global policy says "allow informational sharing with Tenant B" but a resource-specific policy says "this resource is not shareable with anyone," the resource-specific policy wins. + +### 6.4 System Policies — Cross-Tenancy + +| Policy | Rule | +|--------|------| +| `XTA-001` | Cross-tenant information sharing is closed by default — explicit authorization required | +| `XTA-002` | Cross-tenant authorizations must specify who, what, when, and where | +| `XTA-003` | More specific authorizations take precedence: field_specific > resource_specific > tenant_global | +| `XTA-004` | All cross-tenant authorization decisions are policy-driven and DCM-enforced | +| `XTA-005` | Sovereignty constraints declared by either Tenant must be honored by all cross-tenant relationships | + +--- + +## 7. Rehydration Tenancy Controls + +### 7.1 Tenancy and Sovereignty Are Always Current + +Tenancy controls, sovereignty directives, and cross-tenant authorizations are **always evaluated against current policies during rehydration**. They cannot be pinned to historical versions. The normative rule is UDLM-owned — `RHY-001` in [udlm four-states.md](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md); this section is DCM's realization of it. + +```yaml +rehydration: + re_evaluate: true/false # governs placement + policy_version: current/pinned # governs resource configuration policies + # The following are ALWAYS current — cannot be pinned: + tenancy_controls: always_current + sovereignty_controls: always_current + cross_tenant_authorizations: always_current +``` + +### 7.2 Rehydration Tenancy Conflict + +When rehydration produces a tenancy or sovereignty constraint that conflicts with an existing cross-tenant allocation: + +```yaml +rehydration_tenancy_conflict_record: + rehydration_request_uuid: + entity_uuid: + conflict_type: cross_tenant_authorization_conflict + original_authorization_uuid: + current_policy_violation: + policy_uuid: + violation: "Consuming Tenant no longer has authorization for this allocation" + action_taken: paused + entity_state: PENDING_REVIEW + notifications_sent: + - entity_owner + - owning_tenant_admin + - consuming_tenant_admin + - platform_admin + resolution_options: + - re_authorize + - release + - escalate + policy_override_available: true +``` + +### 7.3 System Policies — Rehydration + +| Policy | Rule | +|--------|------| +| `RHY-001` | UDLM-owned ([four-states.md](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md)) — always-current tenancy/sovereignty/cross-tenant evaluation; realized by §7.1–§7.2 | +| `RHY-002` | Rehydration that conflicts with current tenancy/sovereignty pauses and enters PENDING_REVIEW | +| `RHY-003` | A paused rehydration allocation is not automatically released — requires explicit resolution | +| `RHY-004` | A policy may declare automatic resolution behavior for rehydration tenancy conflicts | + +> `RHY-001` (and `RHY-005`, UUID preservation on restore-in-place) are UDLM data-model rules; +> `RHY-002..004` are DCM realization rules in the same coordinated number space (udlm +> four-states.md notes the split — "the rest is realization/policy" and lives here). + +--- + +## 8. Open Questions + +| # | Question | Impact | Status | +|---|----------|--------|--------| +| 1 | Should organizations be able to submit custom profiles and groups back to the DCM project registry? | Community | ✅ Resolved — community submissions via PR-based workflow; Tier 2; documented use case + deployment reference + test results + named maintainer (PROF-005) | +| 2 | Should there be a certified profile program — profiles that have been validated against specific regulatory frameworks? | Compliance | ✅ Resolved — certified profile program with third-party certification metadata; certified profiles promoted to Tier 1; applies to artifact not deployment (PROF-006) | +| 3 | Should External Policy Evaluator trust elevation require a formal approval workflow in DCM UI or is out-of-band approval sufficient? | Security | ✅ Resolved — formal approval workflow; profile-governed approvers (1 standard → 3 sovereign); P7D shadow period; POLICY_PROVIDER_ELEVATED audit (PROF-007) | +| 4 | Should the default TTL for dev profile resources be configurable at the platform level or only at the group level? | Configuration | ✅ Resolved — overridable at platform domain layer; per-resource-type TTL overrides; on_expiry action configurable (PROF-008) | +| 5 | How does External Policy Evaluator delivery interact with air-gapped deployments — pull from internal mirror? | Sovereignty | ✅ Resolved — signed bundle model identical to registry; Mode 4 in sovereign restricted to within-boundary endpoints (PROF-009) | + +--- + +## 9. Related Concepts + +- **Policy Engine** — executes the policies organized by groups and profiles +- **Policy Layers** — the assembly process step where policies execute +- **Artifact Metadata** — universal metadata on all policy artifacts +- **Five Artifact Statuses** — developing → proposed → active → deprecated → retired +- **Shadow Execution** — proposed policies and profiles run in shadow mode for validation +- **Override Control** — policies set override control on fields; immutable_ceiling: absolute for non-negotiables +- **Ingestion Model** — policy profile requirements may gate brownfield promotion +- **Four States** — rehydration tenancy controls govern how historical states are replayed + + +## 8. Policy Profile Gap Resolutions + +### 8.1 Community Profile and Group Submissions (Q1) + +Organizations may submit custom profiles and policy groups to the DCM community registry following the same PR-based proposal workflow as Resource Types. Community contributions live in Tier 2 (Verified Community). + +```yaml +community_profile_submission: + profile: + handle: "community/profile/hipaa-openstack" + extends: system/profile/hipaa-prod + description: "HIPAA production profile for OpenStack deployments" + policy_groups: + - system/group/compliance-hipaa + - community/group/openstack-security-baseline + contributed_by: "Healthcare IT Community Group" + tested_with: [OpenStack 2024.1, DCM 1.0] + required_for_submission: + - documented_use_case + - at_least_one_real_deployment_reference + - test_results_against_reference_implementation + - named_maintainer +``` + +Community profiles carry the same lifecycle as Resource Types — shadow validation before active, deprecation policies, sunset periods. Organizations adopt them directly or extend them further. + +### 8.2 Certified Profile Program (Q2) + +DCM supports a certified profile program where profiles carry formal third-party certification metadata against compliance frameworks. Certified profiles are promoted to Tier 1 (DCM Core). + +```yaml +profile_certification: + certifications: + - framework: HIPAA + certifying_body: "Coalfire Systems" + certification_date: "2025-11-01" + expires_at: "2027-11-01" + certification_scope: "PHI data lifecycle management via DCM" + certificate_ref: + vault_credential_ref: + path: "dcm/registry/certifications/hipaa-prod-2025" +``` + +**Important:** Profile certification applies to the profile artifact only — it does not certify the deploying organization's compliance posture. A certified profile is evidence that the profile implements the required controls; it is not a compliance certification of any specific deployment. + +### 8.3 External Policy Evaluator Trust Elevation Approval (Q3) + +External Policy Evaluator trust elevation (increasing the mode level) requires a formal approval workflow. Approval requirements are profile-governed. + +```yaml +external_evaluation_trust_elevation: + elevation_request: + from_mode: 1 + to_mode: 3 + justification: "Need OPA Rego for complex placement constraints" + + approval_requirements: + standard: + approvers: [platform_admin] + min_approvers: 1 + prod: + approvers: [platform_admin, security_owner] + min_approvers: 2 + fsi: + approvers: [platform_admin, security_owner, compliance_officer] + min_approvers: 2 + verified_required: true + sovereign: + approvers: [platform_admin, security_owner, compliance_officer] + min_approvers: 3 + requires_change_control_ticket: true + + shadow_period_after_elevation: P7D # elevated mode runs in shadow before active + audit_record: POLICY_PROVIDER_ELEVATED +``` + +The P7D shadow period catches unintended consequences before elevated outputs become binding on production requests. + +### 8.4 Dev Profile Resource TTL Configurability (Q4) + +The default TTL for dev profile resources is declared in the system domain layer and overridable at the platform domain level. + +```yaml +layer: + handle: "platform/dev-profile/resource-ttl-override" + domain: platform + fields: + dev_profile_resource_ttl: + default_ttl: P30D # platform override: 30d instead of system default P7D + max_ttl: P90D # consumers cannot declare TTL > 90 days in dev + on_expiry: notify # notify (consumers can extend) vs destroy + per_resource_type_overrides: + Compute.VirtualMachine: P7D + Storage.Block: P14D + DNS.Record: P3D +``` + +### 8.5 Air-Gapped External Policy Evaluator Delivery (Q5) + +External Policy Evaluator delivery in air-gapped deployments uses signed bundles — same model as the registry bundle system. + +```yaml +external_evaluation_airgap: + delivery_mode: signed_bundle + bundle_contents: + - provider_registration_yaml + - policy_artifacts_zip + - mode_specific_package: + mode_3: opa_rego_bundle # OPA Rego files + data + mode_4: endpoint_config # endpoint declaration (must be within boundary) + signing_key_ref: + expires_at: +``` + +**Mode 4 sovereign constraint:** In sovereign profiles, Mode 4 External Policy Evaluators may only call endpoints within the sovereignty boundary. External AI service calls are blocked by the BBQ-001 sovereignty check before any Mode 4 query. + +### 8.6 System Policies — Policy Profile Gaps + +| Policy | Rule | +|--------|------| +| `PROF-005` | Organizations may submit custom profiles and policy groups to the DCM community registry via the same PR-based proposal workflow as Resource Types. Community contributions live in Tier 2. Submissions require documented use case, at least one production deployment reference, test results, and a named maintainer. | +| `PROF-006` | DCM supports a certified profile program where profiles carry formal third-party certification metadata. Certified profiles are promoted to Tier 1. Profile certification applies to the artifact only — it does not certify the deploying organization's compliance posture. | +| `PROF-007` | External Policy Evaluator trust elevation requires a formal approval workflow (standard: 1 platform admin; prod: platform admin + security owner; fsi/sovereign: dual approval + compliance officer). Elevated providers run in shadow mode for P7D before activation. All elevations produce a POLICY_PROVIDER_ELEVATED audit record. | +| `PROF-008` | The default TTL for dev profile resources is declared in the system domain layer and overridable at the platform domain level. Per-resource-type TTL overrides are supported. The on_expiry action is configurable. | +| `PROF-009` | External Policy Evaluator delivery in air-gapped deployments uses signed bundles identical to the registry bundle model. Mode 4 providers in sovereign profiles may only call endpoints within the sovereignty boundary. | +| `PROF-010` | Platform-admin **capability admission** (ADR-PROV-003) — approving / provisioning / denying a provider's *declared* capabilities/categories at **platform level** (coarse) — reuses the PROF-007 approval structure with **profile-governed** stringency ("default safe": standard → 1 platform admin; prod → + security owner; fsi/sovereign → dual approval + compliance officer). A `provisional` disposition runs in shadow (`AUDIT_ONLY`) for P7D before promotion to `approved`. The action is gated on the `platform_admin` role. **Default-deny**: by default no provider use is allowed — a declared capability is unusable until admitted (`effective_capabilities` starts empty), and no profile weakens this. **Granular / conditional approval** (per tenant/zone/resource/context) is **policy** (Governance-Matrix `ALLOW_WITH_CONDITIONS`), not an admin field. DCM enforces only `effective_capabilities` = declared ∩ admitted ∩ registry-enabled ∩ Governance-Matrix-permitted (the intersecting ceiling, mirroring `accepts_roles`). Every admission change produces an immutable, append-only `CAPABILITY_ADMIT` audit record (actor + reason + resulting disposition); the current disposition is the LIFO-newest such record — never a destructive edit. | + + + +--- + +## 9. Recovery Posture Policy Groups + +### 9.1 recovery_posture as a Concern Type + +`recovery_posture` is a Policy Group concern_type that governs how DCM responds to provisioning failures, timeouts, and ambiguous states. It is the fifth concern type alongside security, compliance, operational, and implementation posture. + +Recovery posture groups contain Recovery Policies — a formal DCM policy type that maps trigger conditions (DISPATCH_TIMEOUT, PARTIAL_REALIZATION, etc.) to response actions (DRIFT_RECONCILE, DISCARD_AND_REQUEUE, NOTIFY_AND_WAIT, etc.). + +See [Operational Models](https://github.com/croadfeldt/udlm/blob/main/lifecycle/operational-models.md) Section 5 for the complete Recovery Policy model, trigger vocabulary, and action vocabulary. + +### 9.2 Four Built-in Recovery Posture Groups + +| Group Handle | Posture | Appropriate For | +|-------------|---------|----------------| +| `system/group/recovery-automated-reconciliation` | Let drift detection converge on correct state | Dev, standard environments | +| `system/group/recovery-discard-and-requeue` | Clean up and restart on any ambiguity | Consistency-critical environments | +| `system/group/recovery-notify-and-wait` | Always notify human; never act automatically | FSI, sovereign, regulated environments | +| `system/group/recovery-aggressive-retry` | Retry everything before giving up | High-transient-failure environments | + +### 9.3 Profile Binding Defaults + +| Profile | Default Recovery Posture | +|---------|------------------------| +| `homelab` | recovery-automated-reconciliation | +| `dev` | recovery-automated-reconciliation | +| `standard` | recovery-automated-reconciliation | +| `prod` | recovery-notify-and-wait | +| `fsi` | recovery-notify-and-wait | +| `sovereign` | recovery-notify-and-wait | + +### 9.4 Override Hierarchy + +Organizations override recovery posture at Tenant or resource-type level without changing the deployment profile: + +```yaml +# Tenant override — all resources in this Tenant use discard-and-requeue +tenant_config: + recovery_profile_override: recovery-discard-and-requeue + +# Resource-type override — VMs get aggressive retry regardless of Tenant/profile +resource_type_recovery_override: + resource_type: Compute.VirtualMachine + recovery_profile: recovery-aggressive-retry +``` + +Resource-type override wins over Tenant override wins over profile default. + + + +--- + +## 10. Zero Trust Posture Policy Groups + +### 10.1 zero_trust_posture as a Concern Type + +`zero_trust_posture` is the sixth Policy Group concern type. It governs authentication requirements, credential lifetime, revocation check frequency, and hardware attestation requirements for all DCM interactions. + +See [Accreditation and Authorization Matrix](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) Section 5 for the complete zero trust model. + +### 10.2 Four Zero Trust Posture Levels + +| Posture | Boundary | Internal | Hardware | Profile Default | +|---------|---------|----------|----------|----------------| +| `none` | Perimeter model | Trusted | Not required | homelab | +| `boundary` | Zero trust at external boundaries | Service mesh | Not required | dev, standard | +| `full` | Zero trust everywhere | Per-call auth | Not required | prod, fsi | +| `hardware_attested` | Zero trust everywhere | Per-call auth | Required (TPM/HSM) | sovereign | + +### 10.3 Credential Lifetime Defaults + +| Profile | Max credential lifetime | +|---------|------------------------| +| homelab | PT8H | +| dev | PT4H | +| standard | PT1H | +| prod | PT30M | +| fsi | PT15M | +| sovereign | PT15M + hardware attestation | + +### 10.4 Hard Data Boundary Constraints + +The sovereign profile enforces a hard constraint via the Data/Capability Authorization Matrix: **data classified as `sovereign` or `classified` never crosses any interaction boundary**. This constraint is declared with `hard_constraint: true` in the federation boundary matrix and cannot be overridden by any policy, profile, or operator action. It is enforced at the matrix level, not the policy level. + + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/governance-enforcement/registry-enforcement.md b/architecture/governance-enforcement/registry-enforcement.md new file mode 100644 index 0000000..cedc460 --- /dev/null +++ b/architecture/governance-enforcement/registry-enforcement.md @@ -0,0 +1,390 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Registry Enforcement +Established: 2026-05-26 +Maps to: udlm/governance/registry-governance.md +--- + +# Registry Enforcement + +> **Implements contracts defined in UDLM**: +> [udlm/governance/registry-governance.md](https://github.com/croadfeldt/udlm/blob/main/governance/registry-governance.md). +> UDLM defines the three-tier registry model (Core, Verified Community, +> Organization), the proposal/review/publication workflow, the versioning +> and deprecation lifecycle contract, and the Resource Type Registry +> extension contract. DCM operationalizes the enforcement: provider selection +> tie-breaking, artifact lifecycle management, and the review queue and +> approval workflow. + +--- + +## 1. Registry governance enforcement + +DCM enforces the three-tier registry workflow through the Registry Manager +service: + +- **Tier 1 (DCM Core)** — write access restricted to DCM Project maintainer + identities; PRs against the DCM core registry repository require 2 + maintainer approvals + automated validation gates + shadow validation + period +- **Tier 2 (Verified Community)** — write access scoped to named community + maintainers per artifact; PRs require maintainer + DCM oversight approval +- **Tier 3 (Organization)** — write access scoped to the deploying + organization's contributors per the Contribution Pipeline (see + [`contribution-pipeline.md`](contribution-pipeline.md)) + +### 1.1 Validation gates (must all pass before review) + +The Registry Manager runs these gates on every PR before review begins: + +1. **Schema validator** — artifact conforms to the declared type schema +2. **FQN conflict check** — no conflict with existing active entries +3. **Dependency resolution** — all declared dependencies resolve +4. **Breaking change detector** (if version > 1.0.0) — detects field + removals, type changes, semantic shifts +5. **Test case coverage** — at least one valid example payload + +A PR that fails any gate is blocked from entering review until resolved. + +### 1.2 Review period enforcement + +Per UDLM Section 3.2, DCM enforces minimum review periods by change type +through the PR pipeline: + +| Change type | Min review | Shadow validation | Approvers | +|---|---|---|---| +| New Tier 1 type | 14 days | 14 days | 2 DCM maintainers | +| New Tier 2 type | 7 days | 7 days | 1 DCM maintainer + named tier maintainer | +| Minor version (non-breaking) | 7 days | 7 days | 1 DCM maintainer | +| Revision (config data only) | 3 days | 3 days | 1 DCM maintainer (or auto if CI passes) | +| Breaking change (major) | 21 days | 21 days | 2 DCM maintainers + community comment period | +| Deprecation | 30 days | N/A | 2 DCM maintainers + affected provider notification | +| Emergency (security) | Waived | 7 days minimum | 2 DCM maintainers + immediate notification | + +The Registry Manager rejects merge attempts that violate the minimum review +period. + +--- + +## 2. Provider selection tie-breaking + +UDLM defines the registry as the source of provider definitions. DCM uses +the registry data plus runtime metrics for placement tie-breaking. When the +Placement Manager has multiple viable candidates that satisfy all +constraints equally, DCM applies this hierarchy deterministically: + +``` +Priority Factor Condition +──────── ───────────────────── ───────────────────────────────────────── +1 Policy preference A Transformation policy injected a + preference_score or preferred_provider_uuid + +2 Provider priority Numeric priority on provider registration + Higher value = preferred (default: 50) + +3 Tenant affinity Tenant's Policy Group declares preferred + providers for specific resource types + +4 Cost analysis Cost Analysis component has current data + AND cost is determinable for candidates + Prefer lower total cost (CapEx + OpEx) + SKIP if cost data absent or incomparable + +5 Least loaded Current utilization from reserve_query + If utilization differs > 10%: prefer less loaded + SKIP if utilization data unavailable + +6 Consistent hash SHA-256(request_uuid + resource_type + + sorted_candidate_uuids) + Deterministic — same request always resolves + to same provider in a stable cluster + Never round-robin +``` + +### 2.1 Cost analysis integration + +Cost analysis ranks above operational load because cost is a business +decision. When cost data is available and comparable: + +- **CapEx:** provider infrastructure cost allocation per resource type +- **OpEx:** operational overhead, licensing, support costs per resource unit +- **Comparability:** same currency and time period; if not comparable + (different currencies, missing data), skip to step 5 + +Cost data sourced from the Cost Analysis control plane component. If Cost +Analysis is not deployed or has no current data, the step is skipped +without blocking placement. + +```yaml +placement_cost_evaluation: + enabled: true # false if Cost Analysis unavailable + data_freshness_max: PT1H # reject cost data older than 1 hour + comparison_threshold: 0.05 # 5% cost difference to trigger preference + cost_components: + - capex_allocation_per_unit + - opex_per_unit_per_hour + - licensing_per_unit +``` + +### 2.2 Provider priority declaration + +```yaml +provider_registration: + provider_priority: 100 # default 50; higher = preferred when equal + cost_metadata: + capex_allocation_per_unit: 12.50 # USD per VM-month + opex_per_unit_per_hour: 0.08 + currency: USD + last_updated: +``` + +Provider cost metadata may be declared statically or sourced dynamically +from Cost Analysis (`REG-011`); hybrid mode uses Cost Analysis when +available and falls back to static. + +--- + +## 3. Artifact lifecycle management + +DCM enforces the deprecation lifecycle per UDLM contracts (REG-DP-001 +through REG-DP-007). The Registry Manager handles: + +### 3.1 Default deprecation lifecycle policies + +```yaml +deprecation_lifecycle_policies: + REG-DP-001: { value: P30D, override: allow } # notification period + REG-DP-002: # sunset by tier + tier_1: P12M + tier_2: P6M + tier_3: organization_governed + profile_locks: { fsi: immutable, sovereign: immutable } + REG-DP-003: { value: P90D, override: allow } # migration window + REG-DP-004: { requirement: required_in_deprecation_notice, override: allow } + REG-DP-005: { value: reject, override: not_permitted } # retirement behavior + REG-DP-006: { value: deprecated_runtime_state, override: allow } + REG-DP-007: { value: P30D, override: not_permitted } # emergency floor +``` + +### 3.2 Deprecation flow + +``` +Resource Type in active status + ▼ Deprecation proposal (PR + 30 day review) +Status: deprecated + │ Notifications to: + │ - All registered providers implementing this type + │ - All organizations with active realizations + │ - All webhook subscriptions to registry events + ▼ Sunset period (P12M Tier 1 / P6M Tier 2) + │ During sunset: + │ - New requests: succeed with deprecation warning + │ - Existing realizations: unaffected + │ - Drift detection: continues + │ - Provider implementations: remain valid + ▼ Retirement (status: retired) + │ Existing realizations → DEPRECATED_RUNTIME state + │ New requests → rejected (REG-DP-005) + ▼ Migration window (P90D) + │ Organizations migrate realizations to successor type + │ DEPRECATED_RUNTIME entities can be decommissioned or migrated + ▼ Post-migration window + DEPRECATED_RUNTIME entities remain operational but unsupported + Drift detection: continues but remediation is manual +``` + +### 3.3 Override defaults + +Organizations override via standard policy priority: + +```yaml +policy: + domain: platform + priority: 600.0.0 + type: gating + rule: > + If registry.deprecation.tier == tier_2 + THEN override: sunset_period = P12M +``` + +`fsi`/`sovereign` profiles lock REG-DP-002 as immutable. + +--- + +## 4. Review queue and approval workflow + +The Registry Manager exposes a review queue: + +``` +GET /api/v1/admin/registry/review-queue + ?tier=1|2|3 + &change_type=new|minor|breaking|deprecation + &assigned_to= + &status=pending_validation|pending_review|pending_shadow|ready_to_merge +``` + +### 4.1 Reviewer workflow + +1. Reviewer picks an item from queue +2. Inspects the PR diff, schema validation results, test cases, breaking + change detector output +3. Records decision via `POST /api/v1/admin/registry/{pr_id}:vote` +4. On final approval: Registry Manager merges PR; artifact transitions to + `proposed` (shadow validation period) +5. After shadow validation period without critical issues: transitions to + `active`; available in registry feed + +### 4.2 Auto-approval for low-risk changes + +Per profile, certain change types may auto-approve if CI gates pass: + +- Revision (config-only change): auto if CI passes in `minimal`/`dev` + profiles +- Minor version of existing type: auto with maintainer sign-off in `dev` +- Major versions and new Tier 1: always require human review regardless of + profile + +--- + +## 5. Resource Type Registry — Information Provider sub-type + +The Resource Type Registry is a specialized sub-type of Information Provider +(`provider_type: registry`). DCM treats it like any registered Information +Provider with extra capabilities: + +```yaml +internal_registry_registration: + provider_type: registry + registry_url: https://registry.corp.example.com + tier_1_source: https://registry.dcm-project.github.io + tier_2_sources: + - https://registry.dcm-project.github.io + - https://registry.partner-org.example.com + + sync: + schedule: "0 2 * * *" + on_sync_failure: alert | use_cached | block_new_requests + cache_ttl: P7D + + offline_mode: false + signed_bundle_import: false + bundle_signing_key_ref: + service_provider_uuid: + secret_path: "dcm/registry/bundle-verification-key" + + sovereignty_filter: + enabled: true + permitted_jurisdictions: [eu-west, eu-central] + + vendor_allowlist: + enabled: false + permitted_vendors: [dcm-project, vmware, redhat, hashicorp] +``` + +Sync is event-driven via `LISTEN/NOTIFY`; the Registry Sync worker pulls +upstream on the configured schedule and applies the federated registry +model (organization mirror + air-gapped bundle import). + +### 5.1 Air-gapped signed bundle + +``` +Online workstation (with registry access) + Pull registry delta since last sync + Sign with organization private key (via Credential Management Service) + Package: registry-update-YYYY-MM-DD.bundle + + ▼ Transfer via approved secure channel + +Air-gapped DCM deployment + Verify signature against organization public key + Import bundle → update local registry + Emit: registry.sync_completed audit event +``` + +--- + +## 6. Registry policy enforcement + +The Resource Type Registry is policy-governed. DCM enforces: + +| Policy target | Example rule | +|---|---| +| `registry_sync` | If resource_type.jurisdiction_compatibility NOT CONTAINS tenant.sovereignty_zone → reject_activation | +| `registry_activation` | If resource_type.publisher NOT IN approved_vendor_list → gate: require_manual_approval | +| `registry_bundle_import` | If bundle.signature_valid == false → reject: unsigned bundles not permitted | +| `registry_sync` (prod) | If active_profile == prod AND resource_type.version_delta.type == major → gate: major version upgrades require manual approval | +| `registry_sync` (audit) | Always inject: sync_audit.required = true, sync_audit.reviewer = platform_admin | + +### 6.1 Profile-appropriate registry policy groups + +DCM ships built-in registry policy groups, activated automatically per +profile: + +| Group | Profile | Behaviors | +|---|---|---| +| `system/group/registry-minimal` | minimal | Advisory; pull everything; no restrictions | +| `system/group/registry-dev` | dev | Warn on unverified sources; no vendor restrictions | +| `system/group/registry-standard` | standard | Block unverified; Tier 1+2 only; sovereignty filter | +| `system/group/registry-prod` | prod | Strict version pinning; approved vendor list; major version manual | +| `system/group/registry-fsi` | fsi | Exact version pinning; immutable sunset; dual approval | +| `system/group/registry-sovereign` | sovereign | Signed bundles only; offline; no external connectivity | + +--- + +## 7. Version policy enforcement + +DCM enforces version constraints strictly — never silently resolves to a +different version than declared: + +```yaml +resource_type_version_constraint: + resource_type: Compute.VirtualMachine + version_policy: exact | compatible | latest_minor | latest + pinned_version: "1.2.3" # required if version_policy: exact +``` + +**DCM never automatically upgrades across major versions** regardless of +`version_policy`. Moving from v1.x to v2.x always requires explicit +consumer action. + +| Profile | Default version policy | +|---|---| +| homelab | latest | +| dev | compatible | +| standard | compatible | +| prod | compatible | +| fsi | exact | +| sovereign | exact | + +--- + +## 8. Resource Type Authority enforcement + +DCM enforces the Resource Type Authority model: + +- The PR submitter becomes the Resource Type Authority unless `owned_by` is + declared otherwise — authority is **conferred by the tier approval** that merges the + spec, not self-granted by the declaration in the PR (Tier-1 = 2 DCM maintainers; the + submitter is bound to `subject.declared_resource_types`). The PR field records the + intended authority; the approval is what makes it effective. +- The authority is the required approver for all future version PRs +- No new version of a spec activates without the authority's approval +- Authority transfer requires a formal transfer PR + +The Registry Manager refuses to merge a version PR without the declared +authority's approval (or successor designated by formal transfer). + +--- + +## 9. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `REG-001-DCM` | DCM enforces PR-based GitOps proposals with automated validation gates that all pass before review | +| `REG-002-DCM` | DCM enforces minimum review periods and mandatory shadow validation in `proposed` status before promotion to `active` | +| `REG-003-DCM` | DCM applies deprecation lifecycle policies REG-DP-001 through REG-DP-007; overridable except where locked by profile | +| `REG-004-DCM` | DCM enforces version constraints strictly; never automatically upgrades across major versions | +| `REG-005-DCM` | DCM applies the placement tie-breaking hierarchy: policy preference → provider priority → tenant affinity → cost analysis → least loaded → consistent hash | +| `REG-006-DCM` | DCM supports federated registry with signed bundle import for air-gapped/sovereign deployments | +| `REG-007-DCM` | DCM activates profile-appropriate registry policy groups by default; organizations may extend or replace | +| `REG-011-DCM` | DCM supports static, Cost Analysis, or hybrid provider cost metadata sources; placement uses freshest available | diff --git a/architecture/images/core-petal-integration.png b/architecture/images/core-petal-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab97ac1a5509128bbbc2f2fd03e3ca347de683c GIT binary patch literal 415680 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ɬ)8*W*=lt!bB&jYluwj&h^@U~(?A>RO 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

4Ac!{E+&>?0rtP6IL9thZ~L9M$e{O4Q{lf zci0mR{|U|gxBQ{Y8E_va%nL4+3pcsjcx3jlR|z5Vxmc*UH5nM=Jceo}%)sq;I-WFD zX2!%vg%Qwnb)QLlG}tMkN22$6T3do&h@w`75Ee}Q2cmi5YQ07YZ~;M8-iC?dSh(@t6I%+^`PvUOF>Ti6;r7);7=sQC&E&rG3UlG9k5=vA#@I6~*5+ySg>!@v-)OPi7MKc*V z)$oPrzRi%hEl&Kw)b6S+gOaST@>)m?dfd8eZEp9{Uge$>TvhHboj+Z*PTiCcI^juS zB{U6>NV^|N@6gbmuW0}~dz3obcCzs`nh5Ui#f%ilHyBKHbtdN)!}^emG0z{t;_YDD z_eI_c@1-WE?>Lj7TOG5{9yl>Qg10Q33tb|6t^rwbnZNaLSThXNy?YX&b?NXy`| zJaj`s2_uk`aGIOjjLHbj4YwxEdeDW{o`Nl);3{fxZLN|l+SY3fbG8V$LeH}SwQd0W z#8Pn<3Rn{`en1Wp;w?YQNn|V3%&e4OPu0Z6j1oOiDNbiM%n*p`tYH|uH$c`CfY=^+ z^DB**HN$_AsFztA!T&Q|2fES8xH+!DO1HMUKlG8vui~xxin#X_cVj z{erpq%>kkHfUUp}23o6M92Y`x<&d2&5F}zG8cN$ELj{GF!V;a`$^QV}Ld2186pjj* z(gkJ4UAvlr)i?z~YZp!9MvI7op-Lh-v^nEezZQm|nX7MKLc@}7IFHv1Lk=(5sIo1p zy`=%~UZ+l~zi=;EHZjh6x%VB#sCrQaPC8%meU{*hKVPmw4Y&{e0@%)Fu=ApcP%Qf< z$#i;cMAmG1wAyz!Z6}}~U%xKJ(AM>?$u{euY01h z$0#u~WOUC^o*BDSV#_mNb;19St+x!TE7*bs1HlP$fFQx$-QC?KxD(vn-8pD*w_w5D z-CY9&m*DOWb4c#JZ{B?KpF{WF-D|B{Rn@(3YWb-gS_qg!zd6%U(YM2VeXzf_T9L3k z)yp|}xFN|=%Rj!c6*%puF3KLjm5-Nv&Ndfv_8ybF_5Z(^lXnqo3>!j-5MQ;Nrh5b) zI0bU6p49Exob3!MyoPK$-|Td?zoS}zPIAm-NC7+lgbR6w-@ganW0N6^ivUaB8J-CasM zOjsAf!$%wHu`ynAG1N#=TIsVj!hsAtJeNxG{vy8a%6M)4S$>`{;gfO>kxLKTBf?{g zGRHslLlzo-mdelO?a@dGx$Ifdo6!(6NIbwHc}--W&7wgZ9hZ&73&VunKlwC8$o-u5 zMQyi?bCLf)h^P~WVz+in)KZ#kuo|R6t$92-kPHq?&#%Qm`_z4=x8Vtbe)#gq1Mtbs zzPFyW=_IND3ImNgm5>1*xoY~JrWj&0fDj~qp2sST=+&aHDZr$^p6FGb%x3otb$zfn z?N~T&GLNRH!xu|=m(|NLfGTJCG)7o>WB6YE($+~wh$x*28(`H z@1qsf6fYk6>!|&|bDASISV`I_=*9=t?>lhcF|m3_fKpRS^P%F|i$MHmCvL7*sA_X* z=8mU8+9pG5($gkqSO-Td9)?ECH3xod0BHNL%}4Bjr0q}`Hx7ej&uJ;`XZANg)3?LP zs+Zcg(cwOlpX;}X5~Lt)4g4bzG74ukjAj8nEJhD+{*_m4EJUCshgsKub^ z*+BtP)bV!FmVX6*RI$mAFvhEw|mCGBLIU zbjUkiS7qI<7oMt|#o;@yUdGkO&iJl9kD6?6kEtVfhLBU$_izESJGpRHx{-P z7gV^@Ce(WT2Z|XEN&WXXVuJ2ZLjKs&VBHJp2 z6`a3)B&`7HMF97R49ywUz8^*U?&Zo1P6>v~1sHCCur);bUPsP5npbNdZ z+(@nsPufTRCF)<62OX2BI@`teotzXu+cnV{aX z35(4uj@KrK6#b(g@aFi!SV7V)LaAq#r}E^VImb&-!24J3Et^wK=hFEi@&0NpcHD?X z!kQg1h=~j3=f*ymMW?sCxncM`7g+Nu_sJlvQb{d_QFUeRhx#v&kIamOb5#iYd{x#t zy6F__*j05?`7Y7E>2$Hh+R|a>zZ0GgPJ#CSY^Qh-$otE1;_X_xNl_Cwzb z8nnulIoA8@AB58t_R0UR@8f6@AP8(=?#4z%V zPk*a7{MvZql3P%OYQ3WA!G&XAK3PNtm^Jc(s=IN{wRJ59o5ciDQ#OS|#uM zzX^uKTx)4{d2h}TQagwUTT!|ww9Cx_;^jnZ+I_>|lgmi}e?GE1yv?>HY-7MIcgo3s z(C7v-h~@}W{hGgf>KV6x(4+6j%*AFc@&dZ1$d)DIendKUTr?4jv;dW9N4ZP(TQ%CK zqBV)#Rh|a!}ifHRu+FtQJ=fiUAyFEeskf6 zHUI77dVQy_?*@aAX)R~KD^hx%MYt4$be*dg#5&0=R^wqYbHz#j{lHLtF#k7*6prtj zaH+0X6>O>vbJOqx7NZN|B1{`HU<#2Sv*meErGWBk3L%7(Z?RF?Bdlo))9vyOVt-MRoUO0liva>2Aop!fO%I^xTgsf2@J z`BqdTv9`){L}w%JJ7wWRkkgb3eRwo2(>iN8Unc0s$VQvV1^&kGc?O1Q>TL3YFa70) z>9GB7i|9_b3u@N$3>Mak&6$1Jre+xQO{4F=HBY3iB>q9ML2E!p%uj3siv#WK8Z{BW zJkNAqv-cvZjR|3~wN@prwc;t{rG_qb2xUNm3rER4lg0e%e|&x@PAG!)uls_al@_w|*?3w*}A|5@HvF(Q+8Vfr(0Hu*X8;p#WgH8!KkSEV(nDq}6a1{-<-O(_EO@tB^3If0;Hx2QTvZ&_Fwb#T*H zV@?Cc{jNmN3nKy-3)+R`4!<7tJo%hZ_Vj_=y_)*4P-_nMMfCjInUp~eCQNIj2l&gO zTEbEx>q*eMgez=hEZMH664-O~>l`i>N&HT~=6Q?bm*aYRMeh5oQko$Cc$R9Zb4U-| zGX`*@h*~b>P_jesr1J}oow#;$hokz<6bWa{=I8#N4L?34X^eYv{N7U|19fyzAR&A4 zGt_=#t0QR-n$5jKDX$CLQgK*|a`hJqJe^QIM}@U41cSYbnqBJ3Pn7LC^u~HCM}0>J zyC8bp4FkT{ul?&t;_@aGQYY@HC;({e z1Q(ikqpka12m;8A2`g**V49V(*cs;kdlx`5A0r9McNd1LdiyO&L1)UJ53K}T1fKvM zFOuBf%~TYWL$;QrAZpRu+lpur(NBwDgS|R3J5m3*%6zc=RTLzapGElzJNlF@UcFF^ zEN9FC6_p98jA%~ddY8?S(Z9W-0G=P=C*lNM^IDr-Nxfz0#b|1R$!0x17#9hL z!=n_P=^0UNsDZQHEX-hJ0pr!2p-YebHB^DyF&wt|gLgQR->EKaBq_Ly6F1aYYN%ve zQ^HFRGlkb3E(bIe(c`Yx!5a4!_?-HeX6#liu`tr~!B4#iF;Ln>IIeLT>(O(d( z%I%#loBdba!5XTHd_+en~zZ}q1? zZTC2Es-58h9q|$m^RAKDOTQ;$){O>3;??L(BoWM-qiDVc<|QM^g$#bdsD}{@WU+7` z=nDdJkY23yJAbAqEoC#j(HD0{3O;0;FQm(LypXV`OIy;E1tsg;r6}jZkCbM?4-f+J3duMOAT#n)c)0C0&7~?c>G}>AvMT^^n!MN zqiGe1lU}r|1~=3iJjCA)Igc_3`%rYC4ppf;`QcB#E5C{dlI9{hj!u;XlNd? zT%s8a#1mWzr#fNdTZ3c1i6DHqDIyknx%8;fB3gYcf5b8a{40tqSA|dVc?`7RHw{Kl z3Sad`x$eBf;6AB=d6Z>tEa|;(oh}Gc{mB$-S9Wn3IanbJr4J(`>D30xbS@{<-Gk{@ z=L6j07aOYwVMP+O^?5H|tNON;@eJhq2YJ1C0s@tINPf9UpiIL758igZ!*AKptdM-Q zRckghBp@vHr40qepeSHYu==4R3-79DdX@MxYdIdov6-K)Xc|M3l zU7gxv=0mPi*~j=vE5~8rT`#LWjVDJdVaVxFT-sb?g}FJyjfEqX`o|cdy17Dh0}uV9 zLd%-wc7%E-D|D6QRIXo2>mE4@v^Hkp=xju+kcav$_`U9DV?&$%HUPlyg4>3s;`UYE z&Cz{Z4HLU~#k6XNl!n2;)yOx;qx@o9 zTc%EWL?!h`1s+{Z?a-k|betQjfi22fK>s?iWm9=FpF8yIHkpyyyr30V9jfp1yf0C@ zjLWxEVa(O}8f^zRoQ~>CxroVEjH{{rj=zfqif%7s@XKGCef#GO^gM2ppH`f#;nrRl z;M1{2UoNpjr|0LRT5Zz}Ma=9)wc0Zb28Sz%y-43iyk`(7{HJ9EAor)eoa$_)^wCgb zfo}2l{+`dNo$2sy)vl`_D;M1s887Y1nk2*+bx+bjd&4O0LpyyN425$Rwlu$}PIqWk zV-wN0k$gKO3H({&*@?debbtxWQ%qZ2+YuLhZ(O{4*;X-9H^zf2zksdo$Fo~sUso1* zWU5MuTDTmpMagH{Xp`n~V79~ILpRcod8e;NxFcVFDt2@IGLC$$u(4ra>~1h7{m@ok zi(4PLJ8pV>_xda&fomC#fWMf5341*0F}HR7=?)=J0uS7EKKrx3tc3=!m=@!zP5Uu( zZi}d1sW{14?58ZPgIA|M?{FD?z`evM(s4YWD!8ez{;#YkC)`GYKyxr>p2;0vg1CoN zCq|$fwT+qmvwR^8_smnt_p^e?86IY^UZ5lxiY>j+Rc@Y@i_v#U))c`(ClsA=rqplP z`K_cC+#~#$vKcU2yZb_oPA%b!uhaE+>38FZbn#jmk=Ml{^QjF}pW9*hy-)1pZ8pRe zHP^B)Ut_-oaKQC9el zZra6d$hq=7Gcma9jQu_%Fw*nSupAHfM7I^5sT=WrE&1ZpDe+PB-%*-^L%P#aU@R@Z zWVC_K^c?W*iD>546I1#OTQ`~XmE58e%c(KGYFVfN%~nP4>J&ReG!&CKr>8@{M7R^} z4|_X`Q|shrm3=+3K0JnUe@J_rCCUkx@cvFzzk+17_5Ie)J{bRK2TEZhsij#pVj(&( zx}L_=c7@6Xt@uf2{$pc)Ok$TE#Gkf!OwW?`H9R{Fx8B-}j84exogsnzUxdEkinKxJJN?FIXLocqp}YBzrU1 zKd&V6+&z@voJqcY{kxK2qzv;gL#CE8KiE5-hIr5tSvK-z-HXet25xmk7uze*l$Abi z9-Tx4x$!HLYvRmNiNw_Bm%Y5yK&be=*8Fke1r?aF5b#PnCC`a;k8lvoPuKH z;dM8(Xw^~_rT1ZEc);6o=BLNpW_o>1qD)O8io92wvQCaO$?)KDtKZl7Ju5s;2?CSS zw6x4AEs*d<4#bZXnqQRNNXdw=FL%oDc_D8nA`5p!Nv-!lheEVa&Zrv`rfSy9%TDNDj5K8d(8tX%te)bE;o1EpG|P2AV(kJ{Mbi-QYF(44{bVtIg=NVU`#$#x8o4)YDCT z0~GYb(z5=l9f6a{-2WWEm(fGkX?}b_Wn+k7DlcXOY<*B4hD9}{-?|F_Ib(qP#6GF% zV!@3?>Tb5j=dz2(c)E`u?wZ=h1!nYvg4)&ybLf&tKn&cc-sR=_+>B-P#@l*F`aw@_ev|QgldBAlA4StIv}g1s zimNioO>>dS>#$4z&DW6_-~ambyC`39jaEKJB=*+)Vw zB+cS_^6Gsqn;qTcc^c^H7UWae-dE%wrmBnf=MS6qxWxkwx}vOM*b$}2uSPcP2 zN@0cotp2XNG&rwp6*|H;=z(k4b!ixq;0o z#uO5y@8iMyDOPWt?!j*lBNH7qc;cw0e{>=2cGgwIax-0tVVQRsD=DCs>cjB&U9^dtpQqi zIxPQ=QyZ4OY=P5J`Weoe&(c}$U=@V9T=@^*aRIB(DP9+>3ns*-~ursODj`PVPJ zA1;PF@apD`bItB-rxup|Qy@D7fUMD5wJ&}J*SY_hss;G-Y*T490Aa8;5?SZsz=Z7O zzB?@;;5z%wP{mb;+m#Fq^_;se5z$>&kd^8+$UoWvy07W)XEGP2-3OX8B zn3$eJr}-OTJK(yV_Wa3XX$o{9tuZvdI6eNL-YbVTPriAa5+BCNpE+F0cTQu1*gyVR zZu$3=-@uC1pN?xDi`(N6R#F7!*U8EaGE|zIt23UN4yW`Q*pWcbDe*YX!^H>*TE+ou zv`Z^iCu5Cpp(KeN)UTu*x~f6PmqHQ##)WH@)>>OhE;Z^hXw}@Sa}{zEVt;DePbK{6 z1L(entE2nV^HKD`gf}eQ_#C5Ry7?ePrt%p%1dW+&G!wbjv{^U%#~6nzFK-2~k$1R9 z2h{uyqG1u?ZeL3s%{=V&I~hLwcxS2Qs-OKWHDLV}V?@_2y2SKE;&E0FV7H8`t*C3T zzL>oU3On&6?6ydrBH4Ga@!nOrkrN0xF$eFrLyw$~q=!}+xiSU>^2TOt#Nd0I682!N zm&?LNvV$L9;M8t9PCq<|1bs51S=Ya&>Z@(RfUS8(iQ%@S`n4RUzt#LEj_lZ24pz zsnEd6?PE%@;pi!k!@N}|La4Pb^qIVYqpIMcMJo;&$Dr?kGSV6S?!Zp=Prsx~yo|Tb zljQBLPtyfLBZBwqGYV#OXIwz&eXZNZLM?m!ZavZe`(3s56j*Gb*kv*0JK-cElS%9N50y^F9y8&hlz!+y+iM&+jpGU|uu;#5& z$jKW%C%s4102x^Lv8B1CF#Y=WS{ns_-12aI`@R{E|Ih78<)DfX*L!2T8@ZrT`7M!w zJR`MtU=t|~u4Ga8st(nXp003v0p&EOQoABVFO)(^A1Q6z zFpA|04dXgmw&y*sMHAWRLTbu3kutFHN5yv{*BC%|0bYG8uyh)}K()T9mUQGdnaU^| zsSsx@V08MmyDc~^Jvse}GD=ZIAl$>n-hoMRN=|d$s!2F^(ihDC`RxvyD<`|)V@oeb z&PUtUEBN`T3$9Gf`s41!-6#Rx%)*eg1Q-bzs0yz{VB95ZHJ82qqBHe zbchaa;vg=($>EZRtCUjR@p~Zca#MN?>1}IM1-K|{B85@ZVc~6(vf@A{S|d;$wJD{M zHWm8u2qt2Dw|3X;%*)$C3cac9!^=Xo#0+kSpBh;<%Z z>FJ*a4_PtJ+Ooq}pQO)UTegSBDh477>{Hk{F#(ag4UPOS?KU`{^gX!`8p| z`J8F1cF=t#&_?QCHWDKS1LPSq71UN{hi`#6@z1PJw|OA}b#GT1L)Szx7;(9#|F)W#P2IC;-e3XhjZ`*9V)2n*?)i{(G?P>4v!?;LYBt3ya?49xyQ z$eOVE0&Bki=}q2L9z*ry0YXhCHS}A=t6l+XUA^6 zr4X%eeS4Oae7;^z7=_}bgsd{7!;vu3dMf|vQ|z)}MMT}UqqPe4C&ujBo&?crt}M=< zsiCXx^s!ExC=ryE0w=EUoE@b$wvZLT9=Gh~+bR5ECG}BOzn^Xb!kTuu9B=aKn_%jp zk%;tfaX5;B4nkR|TxOsS2sZ3qJF^y!_7^l(V3hf=~v z`<{qEwKT6*`x*{Vy9wwJgtbj%x?oHmTPC17wZGae@IS$KcsRQ`Cll1|iI zr(T2qd&NB+8BapBO>?JsL3qp_Sscn2qv*~IJ=SrAp;9VAV@q1xt7qq)gaG-E03Ju#w8i7Etix z_G;Hb-Z%>p7N@QuUZ#!0R zcN?~`q2h&yFRm{BR{`N*2j6l+)UAW)n4KZC+5dU6tkAGYtfCx=*m+4cAKH;YRHsYA z#s0J98gyh8H9+*!E#6iFs(o!KIh>>6Y@`D1RvpZ2p`x}`j=o#oPc+D1kryUou8xDI zLSz^y_KG+QPnWe-q?Y{sCl$);ULqWci+gqvGejv7vF6SP7&ry;L=2i5K(3)tj1DIk z>GH$ysoPAB5C*u>we0Xprd>9;y5(s6KGiik>GUWbIjE}38)~vi(njQC&Ny?j4|BEX zO(L#ZLvkGkc(fC}26CTLPymzn*T`0*d_gM_hILa2_`-s&{eL*i-guqHJe?hR*SOD1 z0FAWIt=b}c1(eLwLb)FDYmD1r7zSP*GECq+ZQ~G7sXt;T9dqLVXNgX_l^IOJ%yqE#v|B&R)AUujKx0^oM{f4#{{LqkY0@~AOW#fGyB7)*|Ta`u& zCvX(KpMY`gZ0?(YaUp~lH`wPK@h8B{E2Y1^fPjb@`v5?S#bDa=EbrS(=*!LJE-k~> zH3w!Mq9Cjc47fveO+k4SV>J2Z39J7h?p2SKr!d{m8a;7SM_8V~yqUH|QTIw=ib!t~(PX5@WDVP_x9Y>9iCC`i z6O=M&wm{KRKCrVaflpOimfrywC%_@Qas=ME$@pp}m(MwaBXKi6RQ`-|0$WYONjjhdW6BCuhq8Zo-adU{ zVvf5%XWyLAoOYNNOa~?Q{B%E&E91Sr{L)%%c$0C=)lA-)c1zP^mxEmX{L*?HgW1lH zF*%&ii3LPG?jmLBf9b49cW{^J=*VqEu@&As!po3!g~w@+1lzT+k<8`I&)I(WPxt|e zcNMw0RoG$>I9JF{AB>XxM7)+2{p!xyn%rl~zkIvt2YtV|f|h3DK&Yj-mKsiWrIEvg z6ly7K;WMe3nXQ;Nysc9=TYcjf*ZSh$GB$E32A7Fu?zr^m+LfwhHuXS-{D_@vE| z1v}z(NCR0*?iKPOu0Bx}E{D4)ga=@c7)y^9(JY+T)zO7BAN_tqu=RU7O=DJlX1oy5 zfuB4p<4WnB4U-q|!;((qZ4%k(T{~T>MD7PXNdt8<6p0_LEaY5A{W^L=faa+B0o+zj z%0$F+L}*WSG7V(8Jx}odothM>KM6b7xy>PfKT3_-L3i;Pa|F{X^|A;QHqyra_LjQTnRHxw6^`J6iZ!sxB@l?rb)&e;rs`DdHeKM`$go(3INN(s3YW9Yiw5ZvNZ z^*YkycYUQsiA~!_LP9D6-s+etEL@9|G7!Bd^uBz0Dlk$zLKqO6p9-{fm@aoac^Y!x z)F9!xcLtI9C8&==9~P{s{I5j1466xPD26!LCT7XRZ%jN|m#5TQKI@$Pf{|)Q%G&a3 zn5~q{f6Ou8`@3xUVSeqU)P>KpG1D4)F~_EF(ILm%WRMho#%9k&lR@G%N7W#kA3|h^-Cj=eIkJ=kN0a+DmJ%gl;5P0;n3W90 z-css9x3m3pMRDH-)y0b;#7MzR@%83MTIV@(rTN^}=SG1o4{TvZB7d`QydUbeD5{Z> znCaexW2M}!b8h`3!_VkW#5+H8JEROSZs@(MHjLGbe-K;-0$grwP2y?dF6u+(-J$f5 z=Z>ffV;Z;Jt58ecUJo%>8u+JpKM}K;LI^SfFn~X4&C(z*B(o78{>`cg;nzcCpoCU0 zrfJDn8ri;4&3Trj$3ARkm-IoRxg||xOVrROs7v&3htj}(YFP^d=xJ75!D+65O83@q zt%uq>{4)wkY{lzQnEb7++ad`bmFI@z)Hr(*Cd|GdB%R4cNwuVq3_wA0UkcB7`>5m2FGCfZ_>IUQ1 zbPg-PHb!+}-H$K0bQ9g9%ltVE+M*YH5R$oyyLryOInCEjC{N;^%sOJ(rc{EofSvD_2<^?xLwXYj{8d#nmY|@R7kWMD6J}rboB|;oI3vA zB#$E4!0q^i7_Ag~UJDr)E){*!Cf`%+Az?7jyA)n^IO5vLgr3EyPh??Y=Vxu zCuz)^Y^^oaes(6~{;sf3>#jB5dsC8d;g(k3u&=)$6B(zQ;smDY%J!EdAg{CfSWLO~ z_k3vgU5&Rr>L?p(08y-jy{h&E`-FOgH+gh!U2YnkDZZGUIA$DYh~lrU;v{s@-1~vl z(MU6zJV&_gGSV6C0mtQxoiLM5Xtomeb`p24BPq4C;!BoGU=9KOc+a|IbE0UZbPThV zC{5DiQumtA?hSIpuXRm(>Sh_D82uFnN6P*SD~{XWgynT$Ympl+d9j^_k2p2gY!_=F zBxncXdB9sLIqEB0M#GNdBEdTyhn>8!NBSF{iqjFi-2pl2o1K9MlRsY*HZb#Z zMZ0j-O@IIH2CO+50gu$Ss&U<4!XK+jd}*Q&`(h=2g!Ip_;`DjUW_PlYdSgjdp|f9N zxAOI5wSD5biONn@xBguvdymfbagfmtyJ2sK^Puzjt$DEbc%h*pmtWtv;biEl!5iW9 zNr8Xw?UtRwca!tl*@@sl-dD7%+%-c}uYuJr(?7<#a@fy!?lO&KxnmsZe>eqHvE(5B zY&)%YE45t@5IXWZJ22pR+p&Guz|h=onuIN&^YVu&?s4?*Iy3>j6%J|eNL)u)7@_FH=BIo;$7dcKYEtL7P{2?)fbUB&}c! zVDcCLkBN3CVi;b-!((K$Lx2xFOQ19>!~_W}=*Len+1=BJfjOc;fcL0%RZ+acd(zdD z(dPgti+pQFK+_VvJw7~Ob56ph5Mgg@%&XmVvT8gYAL>DqGH#|B@Ub1`6CrobebB%e z)3wPOz@k-L4NPw;iN`YKO|;qg*5XwlR^Q~syZ@uZWr>JJF2l#-DR2ST2IpgT1xqPVQ2dGjs=^ozC)AU^4EZX>FD@anh_?yiD3*TM6XCZUp^3E}Pp){30ya32b0{`JFJE%YFY6esp>1fz{Hi?S# z3?D{i@;~$4zCOvvz1HfPp=~zj&J}=O_B|W~R~k^!V*~cug_ZdAl6>PB>)~@lVJQe_ z{H5?P{XbNqhj$$VaN+O5DAeP>D`wlH4T6wj4d{>2InH(AHuPdZdZ84{sF7 znp6H)==$^YzJZbCJ~2iyZ(x$^ZV(_TFS4_q`$+a605kyTYXJ`3<)y95eXpFHQANN_ z95qYGFA;np?fnRfj=mLJSHYaYPNn~u3Iv$FNGnum*YF^QjRByT-vv!W{sjvw<}J%+ zzaXsX)T8PCp?B@K#&L+Sm0&igRoW0ju0q{o5;bf_u*qE2kpPDEf^=Ag44X4o4aZ!N zJy+u7AQEN3Qw=S@qaNV>j7WR7QWNs!TQbJh>PtGI;<}QnMp{uRwk2OEF)a@|wsY^%Ema^wd@!50YzLx*=3z#PG4e z8|3OfZpT1p4{|Y{wfDGns#&$_s|U-JOLQ=1s|*7`$2Zs>--y-c0*G7 z5y(;)Z#o)h4N^NWn7W3>ZL@iMU6G^SXDnvAErioThBi}KH)!V3g6k*u*w1k% z5jB=ML$wJ-U7Ut9jBH8`ZcQ4PhDRbc1{_#H0L;ZL`zVlsHm@!{9@s3YCsH+E_&;DS zV+C$gfliZw0Sm}q_qd~QIinU-GeyE{^5iNkgp{@7;Chr60!5m1fR5`=P^3w`^B~AI zPFNnx%LmZO!EA~tbfvBuNlP}gr_)r z(?da2a_#}gzg?|QgOU^W&E7Hetj-B;dXp@x)oi>qBcU15SsaO2e}d<}V3MISs(;fQ z})Ys?L11royJ}=g4L}x)+PC1pNoY=b)otGXb zPc{5YD(N_3@*7Z3L&0>%oh)K?yTi9dAu|XS0y<376x>rU5~a1Dz~=l@hj*+UX6b)+ z2@3yk;jYzjz?DgGZ*MQaashuZ>!rZW`WN8-v5A1D&vMIeDlQrh6lXCJy*ZihrWFP- zU@NqjyL~w&`$8Xb)*qS6sU5X0VlvgtsX&8+NvRDYfSsX`pvmDEHpTKYxY7g*SH8VUd|Xt~1P;K-8_XLjoZ%129c zU2K-OZ^ZRP&5N^}syhJD$cl=?8?VbuXryuLS zsuG-@dTwH2R}x)g^3^TfR7-aeG5Wo}O#2$SqGjJ?M91%fjtxwBFE@!kZ`lTfzOlU8 zOjn8-y)GVp_`9RF!~8zR`B44o{^VGpBmgeKr8yP~UGm0AXDxp1e^@=oqK z|F-+p;sOx*w(Llb*o&SexU}_!{j95EYIS{XNWXqS-Ar$3e@g;MMNaV=Y@v$l?q3Uv zt!;iVtm+3@{LW%2p~gi;ju4qokIoNV`W1Jxv*Ui-JGhGainmr(!%roHP4hoI;Lj-8 zH9d`IKh1Z3;UX##3$5#RLO?~OW%m?io|1|Ohk^03Y#@WT$T)A_KEgGCnBt-aD?ym64(x7kg6Bi=Y(c*?%|nSTw#E=Acf=0&@ACw1KOCCFtE;7*n!}A>W{Y^^(@hAP zXz)e+q@KFcz{-qotF$T;Rdpv^0^Kb2^!ut}Ma@eAVy0(XrmnHHfB0B6$h@?a`Z)i{}B87R>+`EGB7))v;N(X4Zpv%6tOpoan+pC-|kzv8Hh<0LczpWi=!tY zTx|gDx8w}J{&57rf_A7RS_f0M7dYo6e_(>|Gom(lAf5Yq-A7P;ij)OsqG!TQ0Wi`? zQ}^ifMAY1y6%k`6ue!WhZ*2F9D1baf#Lz4Zk)$qX3cGH%K+eRB2bR!g7PcHdwr*< zY0-JF@6=#`?B2>2D3{yY0ocsD5nx#J6<$*W3~lz0f9cFrbc19G`)YbV7T&c}#SLP@ zuU*vAH0v{$j>LZiLj*8;QE4TGo`nvux8^Iw>$O&j7c*W>I5=dl)TUfhP>qKsQa^It zQFL6aBbM68Lyd(XZbq=8i~N&;!Qa0E#kW2}aTfUIxhcsuXxwHo4g+#;G$8Vy(aI(Z}hIC_YvQrvnlxIJVB5LWLJ20 zEfbdY53=l5u-DVb^{mK&0YtBRdqfdGpN$G9)XDPfQR3;@g$_6nt<(v}tMlHL0;L4# z>Vj!H!B6U0hkUP`w~xa*ZV;OwAWKKL^YCC-e+Q9v@rzm4Zy(?L;CP;pc(iCLB;LRs zeQW9Fzi{F3?2l|WFpK$b7W5JmIOKbh%Y?F2Vxnra5!_7swfHpaD|pO%m%8U&B*#9z z8qX1~+#wBWpg|k)lX%zoFl4QfGRSnnL?nYxPu*8NgUWF|3|&0I5y00{%)2`#3U4W9 z%h`fm+Td+=_;u6+T^B|k2H5?l7=k2dpldue+MJIbW9X%=4RDMr;h)xXjqH5XFGves z|6HDn(F=_S>0JgfK@S$bcDDQHd)hqIw}d^zmy66rO*@!&wETCh#J~na6CpR4At472 zA0||$hpA07QB{P@yRt{luM5E24PLg95_YzxR&7rQD?#;MpjB20!ov>rlDSoa+-G!O ztech6-$Fr#u6w&q-M6t4IqCkmZ($<{a68DLNsdIJ7t;<_!TQDRh zqMOm3T(|My`l6us%!oVX6k5Ow!Ja*L)o8!^{a-<$@Rye8S{sHX9HnCysuOpj@zB&# zZ1P||jKK>wm7(#XPy!)OJMg3(lv^d&au6UV`{>WD_{^EKks z#Gvo0?y*7xVS@VnahUOsgDF~?ewU9_XsSth&H10fB9GARxmNg7rq?D0tSN7Pjz_dH zoQ?taE;T<{Dnffab?cH%rXWOEYJ%gs8F+6XUcVbu_hbZu_9CU^C@9Cpbgf|IRVxs|QhtWS4B9rSyhH(Vo)Rzg#G{V*CHoHSWk zsBl0;RZytF3k&pZ-p5inV%s3;tm;sdmW%ipGfG)dQ7AQHPd5h}I;md3{+ar!qoNaX zJAtTsoh5QH3L6Jz5N0%fKw#_3vB=%k1Ny*6qpu!XWj`QQN8Tg|Z|f6RweC~A zHdwMEChl}~N5?%+D>G0teFhp9rl*}X>|E=Dh6nNT5c@g?utPs-x*o@HIka2gI&4%| zVjIvJ<+Y*-kU9a}{JwG!-%(%2K_$ThoQ@8C#Y6auQOl~Bwmir+uqOpVua2aJqo4sn zRdcbm>6~Z@MIvJ(ZqO$*u=Ala<<+*+Pr7QrJ|^Phk4(&Dh>Q#zz-K%$@}5QDxmCqt zAjtO9&8tuKyk!&6U;hTYZQ@bjqWBNAKZoc$z+v#DYuiFqXf{eO0nnndQ#bfX0Unux zbgOGR`L2V=3*PZbQ!E4y?eh~M1x}p5L3VS#d99)4*|Er1VFW2kIW_kAwEI&&F>v+< zwoNkwM8rMQ&EEF(=|T8oCFP}&;xpuRfuIM--qrr`J}2oqS8Y4A?;5^btW0ZRzc3tV zTC7PK@L;TX?UCiJd~pX$;i9A+%%5en<5-Zovt)2`F#jPQK>Y1zd-b*OzIy_=Hk&VBC?N>^MMk)qcqBxxdJoO)DKsQ8lM>l146hlnB zlYe_c(#asxxV{Pi8Ng+?Nwmn#X*AjS0{nlN`s%Q#w)Sm7l#=c)5orPGl5P-CI;5n# zTa-@8p^=vEp+ma6Yp9_HhGyvb#&gd5yT18*_P+L7>xuh``&rFn_aO1^sTJQ(SDM24 zDE*j7+Lo5Dd;6og2L33xHO&30JY{g58WBEM6!3k={!i8IAE$h#`prR-$&p6Zl`~A6 zGGsqX_~XML-$Abju1|I=e_e7Kde%18k61N?Fes^gSRH$;1V06?Wg6njs0-24CS{qS z@w;0`Y(bCm-l<5ml;#qT*|YbvuSt-&n|x-*-m&-sJCj0m>}&`nZVaYRuTvYH!Y)nh zt!Xs*^;R=C2}@>lM|^DTFtTRoOJmxH>HtsS);RqPL79h=HFmpkxjd#)UaxMz;@jx?Z# zziW*@gty%ND=bwgbop-&FGB2Y&Qadp2{g5Iv(n)qLVmPV9ujZ{U*753oRvuSkOp4^ zTI)HBfRkzQKfnw@b)x+a5OsW>(btExWfzN)gr5KCVC^i?Sl_sruB)W37Uo8x^y>2Z zDi2tKB!?Mm<~VL*C)766RA`Yw?-1DJh?(EgzR@f1*!4W;B-%dYJ}#RpRuSPOv=jIftJKMo-`4s)>y0~-WPTzt1 zgngwKFU=|$F>e`%Nn1eEX+5cqgvR1T()2@;v)oG2H>?+<-T1zkyRx|j^%2OI&TTD+ z2u9>8o-CbS2EG6d$|e8a?XTY}2*ACOfSt~KK~C83aRFKA>0)#e9=$Zfombp)ZIGO_x)?S@n;Ey=7k{?8#sNq)Ns*$a2SlTn{hhFbNW-ap3m{_!#jG&tme_q^( zo=yv7GD5k0y(X5|!BBZkUoM439wV-A)JryXn*I1_-|m_<(V_6V{`o>pM!^Ds-6*2n z7XPPSjlSwpPpg|af|R#e8@yy0KEchMwqN6V@9}<~ziVg+9h(@ngJ%4pRnUJo+Juf$ z$O-qV!QIN%{s#|J5K4_mHfIbSamrZ&&~lHX?RFTF$`eTK(6K#l(&$=awAg6N3A9_4 zR$rpdH8F95iSU%_C?bFQ{Mlp=NJaP?uw_oe)SKBSa!t_e?R9eO$rX4eIhrc!`bl^M z471vuGW%NFrP2tB)OEG&Ijh^g`W!2T+4tCR(lN1p5;am~aQ2z65Q0tnMUFv+W}+u^ zTkvKE{(uzV7g*7BsAvR(2IdU!ef|lc(Zot{{ zAY1BktnrU1H^z1BLw6a;bbmM9tQexEsrak7j7$aGI<>I!-1dfBpgHW+Gss98NHhrc@+alR4DYNIaEe6lC>2(Ib0t1^#-;^gR=CFMukCa;Qsgbp_zF{n1r`8$NFQ znT`eZJPjxWfjQaK(xYB*&8Q0Ipu(1ime6qmwrvSNlKe?VXL+tKpDMlkyuH2M>et7$ z=~YvXh9RNz^}2?xjSuC&%oalqSk@oL=|+y%J1}iQ-Z&0WsO)*Xx9+WxsRa=yO5BOTE(HfY^|ZCi=(Z*jG0vat)XSK;A(g{pnGV9Wki61 zUznr};l&UXeH4p}&59ab2|#uN7D0 z2z=awvJWM)+Q-sVNs*vT#94k+>ReY z9iy~-4%_Yn7sAg2;?IVf8TVQ#bNWMH)}OEbbnOj#`If9q+3tM9!(h!|%r`gj9*<1m ztw1<^K;TXTWxgJDe&zVMCYzx_*5)>3#2NN0drygfA@Sk^wDq?6HH4p+&jn6Z#L4Yo z!tS5wtd_wwro7>GcqYF6j>&&izy90a1EBztY6A>Z znQY%-^%t^#Z!Df8NG9Y!#emR&Ew-Bh@Fqf%2}0(|syb67ZSm*Z;dCQ%X^hbfz8U+Z$j0@bVkj3Lq@OH#6_&L>3lKx$-QX zt;=sqhYP;5R_R87wGD&BpP*bvi?zvHHmqWPj_k??r!@fWtt9Qlh77;*WUuU*kSkb&PY7Z0rg5m60T6XA;)Vt1q`Z4K$jK4B)zTkzG83nz$SV}`5Zc+Ff@RreqN`Q&d;8|0@Mm5j3o=d(9W%Y;oR_MFE+Y~iEidgPJX)|N5(Y$FMj@8t-5;}%6LELyfD+?4b z5wA|~s53Hrl7>m`U&rtmG9{;B}pZ+as2o=Z}ia#Y7sjp`FJe|d(XDVjOcxs_*>xd`Px6JULqz=L* zJDs72GxSGrST!rSp5c7({mgg{3Lv<3Y=5~h`3{9$?^Tu8d>l6R)HELn9QT0wZxTxl z<+YJ;ueL*wl!LmBqFStK|MaeRIsclCjk2eQdtL!BLDG#Hqf~KneJO|<<5k}r_-!dm zkI#G>F_NOGnv7R0PK>0Bmk8BTN5AzOY?X8_nrC)udQJ2U=Y8JLI(i2;Y}Eh$EdC7j;SE%ujE&#Iu#F z;@(eq>9FJVAK|~>ZU+!=a$csfqr< zfA>NrRWvYGSGE8AypEGR0N=YKbl@fiV9bk{;T>6}QKpabbYAPCNl(W{|4NrEEpxIu znPZ#0U?SCFk+M*LZ6!yJYh_^qCvXQPG`dI58MHuMa0B^a=4a5gQ0Fz?-#d`kR`5!= z5lU-m#Pc=LRSMV|vvOP@z_HLF{c640pQ*fUUc_RVaJxgi>p~`TYO9yTq%#=VZT!rR zI?B9fIt1GlufhU91ThJ7ARyKBcEFr9dUrG@Dq2deI`BknyO$)aPMO~8Dg;myMnKu- z<$xh*z;1BB!A*EV8HYEK`jBGzY`&E&{8giAfBnrsOV?zqAg_X9pt)ARH}< zj9uw~rZ-B?e%`mcHuv^!!(YI4H&<@-fXvg(@zXNG{~aJ`9hqqv15Lu1MQ1qCQ_FWg zLX#T)sb?##xZGeo9RopDL=g8+t>iDRrboK1WCPo>we9Nxou41_#L>Y*_Dob%B~G^( z{n*p5^23N7Wml^a>5#ux1dI}0mnK;g-Uoc+?z%!c)c(%>4b$c{lFFHGcG>SLSwy#0 zETK7>`tokVdc5~D{WvJsRCz5HfnP6fDT6#~{YkbUh4ds|npp8vdbN`)B1QGZt;^5QIQ|!q{&v62t*j;m$Bu12 z&24SdT7JiyUf}*7w1>VYUdVzwWEd8$!*aH|g|VihuGYIH*9nX3Pu4Tgv0Epg-yx$* zCjjrX7$M^8t+yV5dtsn_d2$EJye)IAxbR;Lvn$e!#VT2*!Ji`DJ>|iYrA?zSNF}%o zGV8XUjSr1Wko66c)qUOAbQCo;0-%9^++$~QQ6GfO4ieL8<@GSWY5VR=rX>Qg0E3R`$0wZaQR0Ql@JY^xM_mRDPMz9Gv}t@Ac2zIun_(d? zXRe##oQKqI+nADEcHSzByzjI0u`zM{rUCB(f}_q#D6nZFK-$?dZDff=CS)3LP|p0+ zAV<9`9U|YOxjLw^`{4~frI^$B99f7)%N=`()1vD^XgnD(GT&ITLde~vDCU}77$Q~ldCV?)^M{b`){_rXxd zw8eK3C!9;y4>FieYbyNd+qa$YE8+Tg9wM3d2^Wk4uY*sr-p_bOU;|u*1>Pr>Zz~(R zC_GV=3N8xL_Kwg*1jR3Aw$ga>GD@-Y?YM3QXQvPcQ&cMSS3QB=h`@%CynF;0#O6H* z!B+~5r!s$Y)%22gEeIVlOZXN-Gx|4qPcx3snEJTNBk#2jISr4bPTZbSWeuA8@GH@H zUrAtlHaT!V`YTdPSp|NB_le834gcz6aqLUCWz>HYSBO`A|0eCKpO~;YjA<03tNWIS zPG}p=1_v4j&%6z9gZeON1Wxs>adGJ!7yMP;FDS^@gfB%5R!7Hxso`T0CS0j~0*(Y= zL`o2V{K4IfiW{TWx%_QdoNdgHmM90{+5nE}MSPYUFrukfuQXaCD@%arz}XtD_KxCA z!bZ_|hMDS!;F(vw~n!oD3|isG|>}28uQdBekXgSE}kAExA1$m*;yT86K$hL-jWKo?+r~+Ag=@73$d!w z^7nXChS6s*k8`f8K$%a3d!@4#Db7mAMiD=auI@5+MU8chfTm#$~9fJIo*=g#g=jA-0QJ9uNHfA@C8v-XXfoUdHx zv$1@3dRr$ew6#O@eiPQKIvFPb`2>DGyUHD8XUA@gW6 zA9ikv$6Ewkkm`8_9Ie;PgimTf^eFr2`9^B-#h;Lvxa8rC4c))HSBqxzh?zQOM^ogz zm}@3iS52iiZlj1zPb$|C5hl|ZAMxazsO2BO3$JQdojay6Vnz_001nnEh%ghQW1bSh zE<|IiG%s=GAi$D-o3cDX)MoUCfJE8~a8dBKb29vnn?sQA>dL*Ss6km7*RoX^A2;mo za*1cOrsd#Te{vY6uJx6e8lEFgYGTw+d$w{ehF>`@<;0sseyR2?45uiJX?v#a`To&? zDTbu*s^HmyU3yg13+o}IR0;k|pkc^r@$-(0F>f@?v-kc28TR|!ie0iBmrN1IWbOab zjtP*lGebyPwiGYVa5_hsaz1ccx)k|AR-ABX1YN8$5fsy0lyPAsO!%`aFu=hwpoY4l zqN}=zoa#M%VPN*ZXfp^o%Zi**j0ciKGanrnp zjG;;l=|^LN(O384yw2a1HjHty&kSoL8L^P|jDLMtMs1f8_jCU_>*T7rC$@aaz$yUZ7G2Fx#BOmz2;UFMw z{Ce~hhEKYxU|A8KL4T;IfNPELvcJ`?v&_e?s4p_n9JGWap8xnUHgO);R3md5aE=og z8s1DVFLp5ts!5I?0NqaEMC-7dt~pgSwl0Jxo{JGke7>8KLfo~1E4-nH+9$EpSs^;$ zUk?95m1kzAoMo)5hGN2?J51phem$O1rkn|s_UF`T8w?M7loYj4tz>9$_xJb5!dU{S z5b}ZR)lQbtfcP*2wJ9UYTBc@4>x6YDpbF5jzvoeL_D){KhA8Z#c?0hzduNzko;}qI zmpZI#bs#g_c+t?EeYxdY9OUvDBXL_^{VQst?0#!@=h^9(EvGWGfNkEUEb@hKBzJE= zj-2@&l?tPSSSY`IaqA%CwWiCUu{V1D~@0uzjzKDHfq~FA1Zym2t18%ln!OQ7nndTo zzK3abDrrCBJv?Z`t$PC{1sEeUl0$C?`nFJLv^k;n`02v>>L0Zu>o=NAQl1`WOz4cf zc5-#!mRWTp`ta~J_F}_Ma?m-|1hI3BjdIkHd+-Dv6}VRM5tYxsmi!f>z5y8j^&)C= zjFTJ@KE;lTi^m3|c49YHJB5GvrnLl46`=gXX_hsELDXBdkEjKATby$MZEJ*|Asc`OsC%DyM(o?fM>MaDs;NJKIVo9b__Lpm}^>Eizy)2 z_Od(>wT1U{5~e>Ho=b9xTf8j#xOna23RkW^0Ch;8znqphh8)=UIx%$l0c{!HWkEy5 z(z{TEm_-&Ed0n@j+foN!gGe@UH&ya}*Kx;AJ^JqQ)^+`&CBQqT*^kRFw`Apw7xj+$ zRbHP|GtTJbOtECh{JpT(U8&baWINmcZJT1|d4N!M;=UU@IH2#5<$K-iH=yS}i8eSB zm``eF+Z?@5ya`*cT8sO=Z>5ntqSc=3}M)!qB^{x}9VI}X}XE$ik7Ea)OqIcj3nxU?S)64*c ziv8iks)$g#Ys_iqUCie<>w@1a?MkSQht3p0g#pgepHP-{!-Qa7u@Qijh4Ee3wCGMrOqLr_a@$J41c5#jS}`Lcr%048^PwDLc*#4y_KeEA(JQYenzujhJ6JX zaUklJ7xmmIZ$f6Zs;Yv3%BS~Qu%lQ_tuDK1o{`?D_fW3m0;edYF3H0-W2jd*Y9KYGj9P*PNxf<(yMDQI;TI9M7d16%(?m0#V9SyrIR|fOQSEb z6urOkluQKhoeIe0AzJhQEju_Fp)J;ZHD65DrrW6n|6Xo*{-R7fH`N`Ls=YkC{emOd zsN*}Rj8X9?Hg-F`yHP4c_HUGAj3SNpvt@#xQ^(rjhup&YT*SjL{ZX#1Q2skqm5MO| zb)(=P2%be-C4TZ%LlJg^!A0XVQTtna>}fO2Xf$@@CZ071y67|PD@^XZdOJ{fEFoUO zbv-ab?nCbmw1bB#tw;Umg<@uEMLYW&?wbY@rCcq1YE;a+2uJzwW00q=)lmZ8^)LeWIpu>zH`u3YanZ+XLU52<|w z35mq&{Oib;rfM`!vBwAa#JcY@E%!`oc$LqIA?-&Aw5hU^{+HWO0cTUD*ctu|JD>#w zzz+A*uNz9S;{!SHEv?n7G|U)m;jbvHR{#0pLxT1?{ll+y)`uKWI3|hi;->?Qu|<+K zm+`bT%W-eZUH^5{9i4uqOoQ3r#eleT2NFO^)8xX=;nU&|np)3}Jux{e2ZP2xP;Ue$ zl=8ZgN|-7L2ao);vT$Pd$DYu3aHDYOGt}gfn<=>f`e`E<@Ggb@!Zp?P563|50=s1S zNoR71I|t1f2tGnmF0pgOB!0sMBf~MRemEBoRA?^l-0Y4OKMg`P7dDlJa>2y9FlHZ5H?>+A-J__c-nw+-l_QpPkAIV@i`VTfhTyq}vSC~wx z5_}u)>g6}gyLm19ThSCizVGC4Z$C^eHaSaeL+EyUPAxDYWoQ9nbZ8^XkZJ#o5sMi3Hs+l0meAP(OsC> z6W;wQ{89gYn&hc32K_a>=!(J)d|Wm=(AQElju%gDXK>a#LERu1c*#R@=nuAUY6(+fh!c$a(f78AVi6uMq-RZtOr@Su{*SF4rF942N zjg2kQ&!#*EN;RTVU>%&)K~WO4TaKSNcDv$5ithLEgr81f>@^0MtoB~*lHRZ7dKQlH zNZZjit|$RnW=#B*js1qg?DmSy&67dpZGLBHUMRA>$i?@`Ger(%ywneceZ?xk%RJ!* zqf-x$e4mg0+oH(SNO9lfXm@bCScROsrAOM&-b5*=)UD4G3qR~kfMuo~ABP1=D<;?Q zG&qFDz7965^A9p?;5BKWq|dk6RIC6OtwZF-+VllCi&Z+OmF_MkXmN?~4kR5WII?XT z$J?i%x?WLh?mde7I(!3f-u8NG*@UW4YyWmyD}*+QhQUTM?ihW~uZ)*wAjb6o;E=$6 zFJ76WeeENCr6)~geg4$|l{MCxiYnyW0L|x@gGA!b7C4cEH`d+U;*P6?P5aWe?XPdR zqln}AKK8oGQNWqTA!5%S%+`PekqNtAE5~o1*?1Q9fBU)b%n`<&JugG=JJj&1s6KG2 zmNA8M6*c@sm36j(WUWbhZ}^K$fsjd6UDA(?pDR4UJj@IDNi^hb=qiSJejv|v-^NxZ z;PB$J3HQp|c(qr!Be%m`D)RP+>}(vhb_a%<=D*~1Y{C~0cr05er-QiY1_Zq0%#s71 zQJ&&+urSxg`JeM2&kH-c>Ll0-%8|ZdomK^Oe{_vQTu-}0K;ynS!R7T@*)#TBjx$N3 zcjugYK}+89UC3{a>`$iP7S!g(1Jrehbty$pnCCj&0dUc(J@tlmi^=+E=++;RXgV~? zwkW}n2)HOEa)=gkR}nd+8yE_Vk&x^hG>F|Zdfmxqbh641o+((or+UMiM=ZJgzhhSuVpBWc^v5mT&ehcWGmx8PD#p=fAfC>MTB*Y}5$_ZQT z&aZKUUAfgwlVmhbaPE1j*)@xlmgrbOeWRFgy=IVs_PVx^D!cOFfyi4%dT&` z0kJR=+@9d0)N%= zHLd_-+14_zZTZ%Uv364zea7{Dpj8b<;&zl=7sI<2ynTnVn(fvOvo-cPK8NcQUCGcS z6B3=^IB53Wyf2E#f7&gL`;#SC+emya4nBJA4+}ITg`PF8LnU1ic|mBmXPCT_KM&%+ zL1n=o<~kqp=lo6rkCR-?Rc{X$*!O}^tEQ75EjWmg&ZGE-Zo&+v?_;%eTP05tJX>^g zQ#?OuFJ||kmdkBw(P7nUOxxQ?l&G*~+M(B+#Rhvwdj!-FfnzmQ86dbJz=eHnds#vju4Ka{3l}(|@uy^hSMmqAATyH6)_E98CPv*mk(5D0W1L}~QV-jY6Nt0Gj>hkK7hx6k3+=rRq zt#L(#x`Z}KfSHggbvW0#7-defrI!)Oo~i&vhj6dS>DUFD0Uk0=!2To0$;lzW$b&B4 zl_*7U5%V^Z1FCk7X7C*4l#Y>KxVbvwqf^h8%(&Ep-R?z#UzzJ~_ce@$iH?M5c#_7J%9P>AuECUPgVsego6{@9IRhsDnl%zT6HIUEkT-9*67&+N+7(^)BHq z0bJPwW97aG;L-+OsU?^B!aEdwL8vsfx5N6XVuzb-1eh9jPnmxAPzL=Y5Zt%Ie;3EW zO)xL44y&VbD(oB6FF8=^E%9G<)J6(I16vNYdKY4Lwu3JM9UULQIr>lRWzp3^Q^Q88X?*fV8607&3q{Bf8@0K#h2xHz{yq^Pltnazu|? z-pINgh5b-YB$_~ln)J!2Vd_|tFVpU1j*rJr#N zNt#OshvZZ+@(7M`CurEqZW8)?>gBem>6_`-&GBW|zXV4@KVyH!Fy})jS9`y()Dp=K zRQ2vKUM#gDw$@Ea`018yX;q##yvf`yZw@Qpa*@+&jk*aGLC!}*IzJD1XcV_@TyqZ_ zAqR3D|{OVA}FMqCiPZ(6+FRfw4U`{rnMcQ~MB?_vc*Y3LyoR^vJ=ll$Gm`WTNb!#2@l6zE?gxc^-XIiz8a~ zZfwh)sWi2C(x484JP#U&Itcq6r51+#jPJMBy@t7E{+xcj8b6`VC6@Vg<=l8{^p6-K zRQ$Kp2MFZ!t@k(>+^nsr+iWqOZOhBqX*C!3)O*P1SlgDNr8DJ!Qqx4qTckz~a6rvgAn6l`REE zDDSlW*_AH}$IGCCo!2329S@Y_B$@+#l!<^0^OKg=RS#%o1BtfwQlP(W`{Vck$#Qgw zAc<>>_*cZh4)lF4f&Gq=zh>+f4N+&bu7f|+0U@0oreWDW-68Loo3XHSve7;ysp&-f!KIrJO!Njtl`@n4R8*kX>Kh)>bu>tpSabQ|@n6G5h( zbn*Lv`G*QG%XAZui<0Gk@%^Yx2-alvR5kwMMZ1w`c`rs2aBK}ir!JiU$0+J?Ynm8lg=)( zD#v>2`Q!h@L6RKFyGMqRQTJ|Cm$>3|wc<1C(FQ?W%jBv%1*jU`69fr}8XI+KE|6;B z>~Q{{F(D4Jl8GHrn((OV0{A~Zj;}O#6i)eodDj*eESU)VeJ3^_fsjctTgZ`$3cB>(oaQHqWv;B=)J(Qvd)vt|_tfKAkoij#&g}-x!Sy-i}ztyEk}U5PHiYjrhs6ae|h^ zVsYS^=ej+fy1G`UMWRtg#pxPMe|)QOoySLcovvdhH~53+iaO*##2Ve&f|w|WN>Sy) z{x1gj4}xwWqv3joSFfPoZnHIeCCoXlm<||wPuUpxNEBQKkOijbw{?3Y3JWO;=1mZj z1p}#s_T`UK>)jIdSC?`gDE=oe>?rh%kO7OG77}*h81+EMIQDez0X^YmL+Ji^wu9?! z0c+#R9|E}w;{sl~j<{K>y06|Uu>G6eYvQQ5n7R@I#lPs3v6n@opX`jLh<~SITyd!S za_18ma4kEO>rQer|M!a*c#t@IXcD<8KR<4iExSW<9QV{(oCWdx=NO=DF1yK@7;S6^tU* zzvyW9*txYlzB!yD#>@Hgf*V;Shf6BAH7aY933A#FdIa|E#nk29(^RK^qS@lSlv`?? z{KxdZqQ!lapS!qE3j6-9=z}64hEq%IfN`D`=V0cJ^CN9Ot1b5O9m2C$6)gj)7wuQd zSjyF@XWYlI=BL(5-s)_oa85u%=o`?tvl4#p$j3$Ryq8my(%Sxf{1>kLRRaSGlsx(# zdmryuclWC$68QG#`w7SLy7F?Aj8E@r_o`GXo=Wr4|N8KvsdM|kK0FjFD11?85A3Hn z-?;Jw&M9v;ZOobu4cB>IAoXj` z!y>Y9KoD8aTKlS7a-rg!gDigAiGYUWL8<$1M}`M~!d%b8VRa22KhDK- zs*jGsl4!Usdsnt8{)+Ygz65awpZE)Dp0H%7^epb{sjKdW80I~0PM0ur;uhL&Y?YG2 z+zbLbv>mc!`v22lGgKrOF#w6OOVx)e^tH$3O04ViYj=;l5>=zA;!$wAn9pRu#Ym2` zL`Qx%1ES+!K>w520ysq#UxJEB?O?T@jUp$_7Tp=0I3Vw||hMKU^?GoRv=g5EXcW*W`dFKX*}W{$yZDCpMjBSE6exdfZS851(c@jQJqz zBjWe?pMye#eZ*+E-+jA9VJzZqecmYV-H$cJ_D=`pGcJR0SUKI+8-A}F&-|xEvSi43 ziitqKeo0u~L2*H=wS!}gO0w{Ap}lREL$Tb!oDI3nv37=WLzuBo_noxfHlt`o_2{lj zI`mv_Kw3svvgNg79z&rhy6CV23djKA0=_Kb$t#BgWiBv}#`TqsOZHff^g$r2Kp07* zj)B2!bz`44sSx50Xd$=$?#5okXQ0uc;4>*FaYLk1#9V<{2A%VF!mZ^2E5v0hO`CSC zWdU~tN;!Nz{UGl+k$3)2&2?Qj{0r>JQyAJQX(`0`kC0=V>eSvdVmzMn=HF7txFEFu z!#f6HXkU<*19ncmZBI8GpnpRxMLI2})kkclG?6~~t*!5Y*C+oA(4rYW$RF4D5`u$` z>{!2YxLCQtt``x0%HH>iT6o^68*dUR)#q{rVS_z3Zs-79%S%cMDtm(3 z{-x18=GX#knEDy=DMEe`M@uS4%gH|^_E^$44zNAG>$zUVO!k}4Dtk^`~ zuMs-$0OcbGjjxLdPtTe%>8(^W!jqOo1~EN9U7B9<;n%A%SuP(CdFlqL#WfHjyqjWC zpTmlIygO48W((*#U~fgMa7^CI;c5T)x1+Ze?XMVTgHHuuMy7*gEA;?pk3Y@_C!Zav zG}}}7Hi_dWb8n9t++jMOr4o5PJ(O}c!LS#oGw{EP6fA;#_EvW$k)4NbonEf_dvb%J zrCQbJ%<{&RN^0SX>dX#AGDPo(r#4l6fVWWC)pbWnjX%|~)?=^PywgLI%=#6F@Mj&< zHz)jvgb+27>+pJJfi&y5d|kLx&W|Y&ff)soNa}N=QD?3iK5v8g37>`+iZ2Q97kd%_PS=`A3g%Uo` z=~o$_4IC6OHve;Qq-AL4mTwjZ!~`Z{sT>GQY;W&JE(Vea%GZ{?3U zxz$CXFWgs&i~znO!rspJWKDyp(aajA*Juz2`^3rO;;uTyQLoDU9|kTneVD zy)zC!{(hTd(JYVcONt};HD`$C4HPlebVc3Jgo4#m$G%6wtPwz~agQ(5Ygw)XN@@k8y%U*aGJ zCnCIW^*6V%{=oL4dGGNC&z!n**v`dglsiQZMtUJO-i0lKX}dtZN^m{D{{53;nH{tq zxWCBSTr0l7;N(%(l|rXzez{J7#m6PuC)Vy4l>D_Y&S#>ojJKCcC3(IWcXcyfBReGS zL8C}LR8s9^)l%)SLCwe=^)P*$hxGjX-X^B4Gxjei$45h%)p>|fFNoE?rm5B4b~%EU zBp!{QD`-@QRyJCzxR&|bu4(z27&jb?8CjBIyfAm!IFbKy2x)td(ybarJDBZsT`;IQ zJh!xF4Y`P9*Fxb{&&#uTn@<1zSB+`h@i4igp~k~K0#ZQGP(~b+{%~qh*Be7CYK0j) zPcn4BF3}e~r_ME_Vg&`peRzkwX_8m;gFc~8dO$}{GIES9a|;`iKyn$Ot7LEk1y*{< zd=j(|gTyXj>KoN*1FJOalO2xNEV`Jg_AS@Fro83k!cWK5CK@bec2|xPN;ueVth}FM zGey33E;lXnY1?V%s0~XQn2cDMQ{h$Yzh2Mhx|t_SfsI=mzf67<^(QXEfp`=z9acc> z526v*Nm(@Nx4n9cD0)S|N9Ibv;b&*BA1?Lry&N)3;gq~9LPWhpL@mAp@%=|W6$nZb zaDlb9!x&LvlYSOw+4e4=+BxjeZiRZqc)`k0Pd<3ahg-Qn`Fd|!XeplnlS|p*lCV>AX|pf+qc4356t!KG)7kSU;LbJdZ|3t@VbMr?1yXB?AZ8eBP5;15 z^G}H|_}lzRF}TLNw4yJz!F{o2{s&OorLCfD1)5{%@STYzj(n-FTZ(;38iJy)IT#$3xRdGz2)6F+4x6_mQ(UU5Ye6+B>K<)&!uE0nOpS9^cdVXAKv-u zCH;6b2o87cd^I85woA)TYus^rr{(MB8-fVsb(Et+iVJNSIFy{2N#4u%%+MPD8(xvV zMEU7lEVGeKJhV`O8@*ynal~2%s%DY#zgeT$)3-4w?;Dcxc|F3+yh%>@^*-X*m*}%q zm3^h4i3-M_WAnObgz1$u(YW!c7?6*hZYasHTd$A+T=O{Ll0F@$%F!yno<3OA5mupg zA39;+9=^Q}JqF5!CeawZ>)9pRvCYZ7f}kC7_CIWRJ*%WZp-n7S$t+*^0PrEoH8VO; zZgn4znJ=fDe&qfsq-P9qQ8*FTvPVpZ;{aUN+z|jPY#Mz*6@&yOiVQV=cang7A+HG< zy0tHD*QsZE6Sy9?8W`plfWsbF;UNFR6~genBH-5*=Pz-OZdW|OR^V{A%eDQb=IpFNe5GTd6!y5VtTK!RGNs8BUsL3s39FK@v^gZC z3pllk0h@n~ev9Ov?u58}@f9UmTTH%AV-j<=IyEPAr_W%_|EJI*f^3G5Gz3Oa`zj>@MpEDAHBMlU3^tfdAW1m{ z_)XchR+KMJZ-cT^$rEpGRS}8g=Rt4dd$+k=R-iRTy_55uU+X^NMUj&g> zR*JIGw{)|_QdWC_wFA6;+rlAQ09!In?eN|Mf{}8+?$;z88I<$R!7l;)h(S%D;cq*i zEaQ%~A|n^@&+QH_U!=sBw?keGGsmah&xSHBJPdo#EIAJNkh6REg~~AX>ozfEbADSM z42a?PadT;t>6O-Xv;k06I#!1n2Tac&Em}2lHVqLcO}O-9GOQ>X-|NVH@e7D-KUw2B zDgzl<#y!|&Q0kcyrne57nmw-Dm9G*66buZ@-v@*eBD0B7L~dK|W=Q9QTyx}$P3aOm@jMbk;p_AW zcmzfQfdG*qPvTQjcr`TE+hd9ZfpJcDQoPN~%YI8G=(TDx23r5Ce9zyLS6^MXqEaNK zxNzHnbT-$jUVmMWfJeY1FlrExkz^PlS@jB+EbnM)uyu;GgmFAk1MX0yr4&W8@WmtG z5xDvY;LV(pkmO#2uihtb=_Ywe-#l}%wTPtdRjN0`O#6BFWXN=fmEg#R9lNw_?q!^4Mv8FQe1z}t`f+#MD^KyG;QdGPS~ z&mIAfz*s;)lNsh{5afFP@#bB2QYkCKQ&Q#m zoUF(~7%KT-%wyqp^nLUQcmysD0n#d1YMOG0_S^R3b@nY8@IXG_JZYlE`iK#i2BtsS zBXETXKpbZ#r@I%I!sEHUyVH3~H+QtU&vnfWwxLp?^AA@#lA5sEdqgVl(r1~Lxct#c zO651RXo@W^pKEc7c+l9{YK_fJ)-NLyqM22-9kQRj{ESHLW4275&@)TQ9T5$g9Vjnv zS!1v7-fTT_VV01e;X*qP_nI!y7XSD15QvP8w#0Fn)~4KHdyXEk-3RwsN&ZA5ceyVf zfssHUCMMEuUs-Bp$HPyD}^+T4Mon+~0{+=9(@fhwphhK9$+&%R5c?3cXfnIqo zZajQUIrBDHqdY}iPC6C=o_r3mk@C&)2!s{_ctV?UZ-({s*fE6_eSS}sEu1*cmKGHZ zTSE!+6{Gx_p-{rZ$>Di!qdXjK>QOGNZ?D^8PwmvE-{lld9f({p%p}Wj^wrnl5%36j z1TG2z%?sg)$_uUaZA;w=XCPIcn`s}IH+@(`ag+CXrwEjj?J^CDSBeIOsWfsu`{EJs z2zUfM0z)I9^(!H{!(|NL*HCXekDatY5Wo{UX+oBD9ImtVd$(Ho#8U4CePxjPv#$_=+?*s^abuC~*j;Nsetd_OmSosX z%Zje>s{ZuRfPg2UN5d%jHeOc<)U|ckBb#^HiPm-tLqVnxJf3{Mu7=om#v^cT5I{*J zV--BQPRSE`-NB>wGvy$jt&L$q^vHywzBV1bxT~H%-G6ZhUd5w!NHO<@!W0t4A}oot z3Ep_4CK)UXoJFIFCO^J1SOV?aJT&%V)$ zr*F4Mz$4%h7%l|ZUq~MjE<&rdrNtS_*S55K0(!Xc_w{%LE&+k4$S6xsN_DTmX+^mc z6t;VjOVLFb?`nnUeren5wtB}p_g$Ct1`Uc*)7{bGj0FSdk#qXciKF(T7k+N9Y<=B6 zeES1-?7aYxJ-rS%1Mm3I(eJaws*eyfKl*Ggf`m~ zQxfe&N0+tr^uDRZ^#4NTLbzP$c>n#CArKggp>PkrPXotYs5Z_IgpMMlRd5NMd;Yfr zK|2S37ku8Zzeh`fWz3QzTSD-_xAkpQ!g7s17xe>ukt$RgDm5HR4QGFUdf)wKuC zICqJ+I(9Vw=KA>-_fK%&-h^w=k7uDi^IhO`;Eya4yw`wxocdPm#)*8X% zGbDCF7Y_96g7bwc8a$@K*AKc>XuVat!;~-fRBxYK12t(4)GH&RsEFZ>G!$B(_||v? zMh60s@^FrfI2Xc}{Lgbrrr96e_i@SM)3(3%uzl(0-?W!DuCWfS(Si%|3L?7d*kSwP zBVV%~k=6SSS6PIJ=g0)*e7*)z8YOMcG$9~S&$TG{8;&~v^WgLJS^vLT=AJu_^8@t_ zy$d~8_ockcO;qme$XMw@8?|3NWl1p!*3#WB`8lxnq|U2Z-=(x|sAD5yqhvr|XTMnY zf(yevuWaT}hxzY30>gqpWMsH~^r5+Sps_W=gF9st`$*WffO2mK+I-HnO~qBep{tL97^FeN1+wpQ(k1x?mujg zS5${a!!NNFiSiiztL5|TYa4didMPb~7q^*GT$Q9ITaT1|5ExpQTU2VmnSYXnex0NMpv|R*|!A z(9Rsl*!D{Q?IjZ0-+qaD}%;Wn~_c8!f4SpY^!?L3oH zGE)t?+9bvF4$Uj1q6m~6DK|>=95=`qXpcyi%PF@OkGH!gO|bv->(^~YR)*cL`RNBc zDs8pA)!(jUCB#~Ge4On(+34iVos$aeutP$&fMb5g%J>_tZgBGf4*a{ttg#n6ZLFIOEG$_%cdj8?bQQ^tylRI2Xa1N z>l&}@`dN~P#k!|Y>OQU3Jso@>#t1KDCV!C!f4q36{o!+K&RhRnS?xog4KX@9C&Sgd z^+cUB=3#wwNZ#xnBBcfw*fZ2?ecZ3V$a$W*cAXpCzrokJnxA>D zCrB>7Z)%B?tN$&7UyPAPx|F0R%hMd;mDG~QTmu9gvDZ~xVm-CTcy)~>ml zTQQ_!-JBuAVF+Tzu${@!G*a6OgX14BUSTG zm)10m?hu}zNgC&Zq(s|NTd#d$mumxii85is+|NE{D5K}#I!5%pp6PJ)tyUO4Uva=@ z=4ab@?X7(A2zUfWF9OJ(gt&OiOv?~SJ+K}N?BfISlFu3XhYR3DtS z%I4-yw*PqJr>>22^nUa4y;?J!u!N{s?KQHjqW;j8RB=_$WsJiVj=QJ;ZCX*c z#YV?m)jIseJOY=3Kvrgg&6$}mFX-L&%9?7+$xigSpf3f$QFyE;phw}L`6iEE1P)8d z^wn244tPJ4M^ziWP%CVd7fA{Gsku|_p~rvilof;9_)8I9yhd|D83u_ASvgoqF+VTM z{&MLYJD~A;yrRmHm&sbNp}d%ym1bXDyUm`HqSKeFhd`1H0!q?ELUnaHj}@W?uuz^V zCG2OG%yx>izkOwcy>jRraSVxo5y3~Fc+t|NT>Je6)1C73r9(C6DLdh1TDp7fU`wlg z?&bx~dw=%-{KSzI^k7**uKl0Iv+RZH8vEkwTb*(!LIwvA9v_}D*?y>ezgwkD3Z)XJ zFsI>=Ax*>EuCDHGYiMi|_Zn;IsVO50Nnclk!iWu@A;^!FI$@2xpHJ()j8OkwPVNy! zV02WTB-!~Vi)PuZoOCG~dhMM*dCnSx40x96`TOe|=h^d`Q$F*n*WVXyQM z`}}L0?J?zdN74OOg}M3z(@Ql+ciaDN-K*o@Oc1@*`Jwn*oQSMBBEZ%iJ#m&~rkyJb z$Jy@)v-Ki2zPV+OGh*O-jF6Du5MFDK)H-{&-`Fmt(E!;Wr>F*hrTY-#`p;f?-FeeK2=xoo@Hah~Pgr?6kcy$_OsBGe(MO3>kYr7(21gK=$+fUu&Q7p&6ylfZ-dOYaptF&0?7!I%@l5 zK$dQQul>YcMKudNpUg+UrM>fS&Yt4d6JXyZ?|a7KFPF}BMopi4b(1jL-v+R4n zeAF6y+bu%jaM8tF?UEIrYVV$MlZ@BmWrVf$yvq|lytSvpdbNHi&ni>|m?C?u;uUM@ z>ae8+Gwgw>H`{B6wo6`DtIgqBpHH$|i|1>d@|eARaErx>jJ|jBGMksjoLen}s0v3! z-;h6526mI|XM0|j1I@bg>bx2s4mQ`yxNBfujne!>yY4GnZh!i#|GXNPzbe-VlW|&n zZkn|pth1N4t`PxU=5l^tm5cj}cmysJfd}uOX*(-U*pDCEF7i3v?!JAp#m9}k)Pk1@ z->5sy6VRh>$b5sZ2L!_8q2Y2rkJ8SRYqLHlH_L6-$?-Q$q%4-?guBC2s9TH2A8fC5 z%6z(r=i362x)6pD+T3E1xS^&_9@Yco8JqT-C+6FDd0kUj9M{OB3qlaD3#`l+K+#pLFpRIuvl+r;aDiOJ8(@tT)+a9zLL4Fk z<;5=#9C1Vd*WD)1y-$neoRE^>JQx;_&z04DvJ@R*P67VX!5T+uLtNrX6)Qz7Z6Pcy zUe+A9MCZJr_js&9e4`{=J^_!dKHDJWdb)^+n?*vfc%}46d9m?Wo%4d>^W9PgZqjk{ zg-3qkfVT-AVO$fB20X=Xmp34F2MX84>yR*ni`A||c}BG8*i_YxCpcbZ&%1v1IS)Ya zzEN!&D5pi{92FJ{YpH@a41EiX739YY)kmEd&%p7Q_b;9^ADK1T_2aQU)#s5ZY<(E7 z>;!8F8x*OZUvY!NBt=UxB<}=`&v+@TAb?OP&69`p z%kr9p5XW2kczcHkiVN3wdA+>~XR#r?`g_8+ZCG7HqrI|jy^YVyv*imHyzTMV>VOD^ zyb6~k^KcfF=)HP|!ti_!dcKCU4WqR#JXA3rfy}@wix8a{nGiagR0zmq<`SM$Jf?XT z7s&I5!hC=yGhV5E@~FbQYjS$3BL$IPhvdzL9Hm6+xxjO9uAbfXQiNCP971#7Su(+S z7;_BIGxC@ap~ynyQW-z$%~&ByE1zIYkJbjxaeAml9s=OlS?Le5YQ%Ny_&wFgB$ zeL+^z7uuF5`Ad0D(LwMsqd&ogcHUJwLF99h{qx!__WkYq&K|?#{Ql{aoOe}}>cYEs zq2@J+DlGc@Wx zPT~ANS~$}_^u!C+KZt;@&{%S>+&`t*&8f^iLl(8?m^>bTQF+*58GMX>vUn-fVudgB z*snErooKIdeZ0M7ynT1uUKibkdyvAI#0j%5tv6~evrq0qW9*+Wt<}O5j(hvD6D}7t z(POSd8|&*gw7%S+H7e!P^JnNek&M!Ml4p=(kdb)C;(^8bleOe4G7_AU zk!CkY#s|Xt5?$b#eTSUv%~C{)VtJ&p4rjkXS_S3-&P4_xi@8?dy41Z*MuK>+7Ry@` zgB9kG+a%kcl#vfn4LFuHWS~I&V^|1Y3xy{#Ek<(-^TNvqkGXMJESa`Za{}`RWlL?H z3!9CcC1ORJ3|v-gUPCq@E0Ndifhc&%J}Vj2rhYNLoP&&|PwQo1L-Yy^vFH=?;0mq# z@v5cJ*Q&-=dr^A|Jjxkw$le> zmp5sgXa{YgID=zdn|UBmAXHyJa>A{_%XN*pd6|YZ;{f`<Kzi6RxL`7}lIP?3y>YV60m zUX(l@h^%$2wO;QVr9&Lr`x!6l$>{}}A95{PIkxe{u4%2)JiE&^1sOOcV}jNsg-#y! z^z~XsZ1UN*;&nYCIa~8vi`D~KS~o0`@sZX!UU1Q@NCy$6(Ihpo*^QXH_H{>!__jf5gYPv{7LRsHZINm<3b7p2|I5J_H zlxFWND^wWNf!~v*tiS^s1riD~tkLf%DG;x3@13&t5``SJ<5j(mn_7`hB78Cc$;>_H(yE;)$f1Swola@;d%qJ4JR zTt{N?8O03Bh)>KZckP8JfslBY`nFQaC6o~Fom%V^h>ii!wNX61L*92Nzfj11a?VtD zE;z+1H^vPjns1&MAcM_5B1Pn%%Oe;i8+}9Jh4&r`I|`m3AC#vBb<-wDfj`k$Vc`$y zz*sPLoRclZAo%{?f@!W@EJ{nIxccKoGo|p#cFMSWOAGBU0s1KzIiY=}WnL^us zOP*<0S_rYty@Fzqb_N$h2BPr7`yA2*rP&1w8yAfiV;214yDk|sKrVAWq!Y?U>O_%< zae*Lr_Y&9U7!-;q(k?a}?giU)9sSTPh3m=sdTVWK9noGzCybM2_Q-?-d-JEs03^fB7tu1t;T()#D9Ad96=W(z>f*i4e8s$TxdpPDehpQq3F859X8wM=f&wdmZQw88q4Xl8Fhmf9 zH-&nCXWleNFfo&pdV#r!{uc^&et)w%?Cn}GhJVLcP;+aQJpCF)#$4;tB}HJP2*1IE z>b0)-O0Ex~hv7$tUZf97mS&#W zH?Zz}kBm={yU5@_xM7Cma;lS!giB^lK)&*ZN^T&dA-9=R_#Sz6 zr*K0yLd0VHqf@0wPIM7=-YfaZIkc6?NUY(>-^_XMoLnSDce;}k4=MCC#xbmmvz69? zhz-m<6p<%Zro?)QPOk*R^Q-`*l8{%&7}m{?Lp`PL6Xv*bxb6uI5AxV!-0v!W6@( zp-Q)88!`_(kYO4SC+{#0!t)x#UF6$b?bZHJ`+<1viKv(P?H1i182r*k+V@AonK_Po zh~L@IEYaSNIf+7lKdO6$IR)9wvA5{{#0#0Z5aVKwW&VD5S&`fSFeY;V?^jqyGg74+CbI6&A03$1KBwZEvMx>0y z@G_?_kp37vfS22&>b~N6-z0MVf`(H=HFku)V|>DNe>kH9J^QRpeDMf)1V%Rk5!$yV zXl{wkRv7EzBAZ(_!?Kn0ojp+C6|VQr*aSH$qB>0T!nG<$71kU9IeYf+1yZoa}Q*^1-GT7Vm zrY>U6iBC=j{)z|@u67nV4pJ2f^@bA6iT(t$k- zWb@L3nVNIcHBZNAZ6_UtoAU?e%6V!tA-fAx^4#@H(#C16b%UH*a$I|Ibl!V){Z&Qt zT$=}0FK=qebuh!FXR!pd<3GT@4jhikxeO8$ohSC_VTOM*4%O-{ru(E1lb}7wJ>iqCsEA6Wrce>5_ zJLL67Xfwj{)hRsRQ7PX)JhRj(IdZCFy;2+vD9cgAW9>dT-xdmiA}P{Q?pFX3kkIAD9ZweNgcrieVd`n86*VJZ|F0>Bbc8pDxm<$( zsxZ=c=&^>Mb3D`jV%cm*Adu4=vI1{7JZA8UDHRTc#=Tpl3*-lRNoh0TuJOzuj~U8B zJlr8~@O*z&UORYZK;WWW`m93N{rbSsD-P)gCPQs?@QlGT0Kj@i3VDPQ zkxfmYKp_8EeIxf8`P>L=3F-fH^&f8{lx90m){CHucS>9omux;z(BZLx62_MS1Txdp zY{inrmXee-qL9R;N)a6^g3%2#{|t5KT%#uH-f$Y~YjpYR^|EYs33&`D@^9<5%PTF+ zMaO_>A*uti9TJ5o1f<0Le+r*k-PGpfSz%PlF4`nBV&lpz}#}T$lV`mKE(SexM1u;1dXl78(e>bf8#wKS`_Bh zqw*w#q$ULD2`PagsqWESiZK?E9PrjAQph?yy>-l|WOOxEa|O5&jRR8VBQwjS5Xp6^ zBslI}A^<2Df2C-iB1~?;1956*x{Ek+L~UlC`rmC8PW}@u0|Ndb5k?S1vg4(uRWXL-b;T=wa6x&7@!H?rv_SL3xv=CYsqlDyZvPSI zvB_KpIm11N_dR3|>j6C3Ama&*jse0Fk@0I3iqw~jM_@pK87>mF$9b4u{F#?`WKZ8I z>(G6AZq~oN>kRiXbSfRDA9$81IvJS<|m}ebX$zB1uT(5n@GLd|DN&d0E z#-lh_Yvr$O*dcG-T1UoDmrMf#2tJ}N(HElk5CWd#F>w8xS2jBDaORIMu3Drpof8%H zpxf1lH}|uOF2UNF^*wFC=n@(7O|9n}h1;LW&~>fm5%5QL;_V9=y+a}YS+j!~Yi4kd zm9n3;IbO&ZIPQ{HdWBxkR338I4$Lz|_d$OCM4`EVuDwN{CO_7t7@HCT9j|5f0<6Ek zU)$iuoi*!&GETzKfW67!H7a?-iIjnt_;{tMIjJ-Q?61Hh5Sb%O^7;eXn{fU&w(NHM z0@{sn)^yDyT!VX$b|U}SfA#5}!GHmS0_I-4yy+uhrkNixqGj&onivV-asKF@1NI%I zh=`Q|F>^5YIOR`dq=md?-*H^yj9iD1#`uD$9ambor`}Zb9eWufhP)$kqT5?S3J)%f zeYW(**Rc1y=-eIH7ttw+(Tj_sDCM1hQo z@3DOoXKNlN0^n}Vt%o#k4@CAj(cUC^yv=SdoTI(MZPGC|YHuUMqZ5^q{o99J`{4b2 z^TavYN5&~~$vWqi3<1sgD+=do-rcQz&;;iJ{lgv4EAroF$@WOCx2C$k-~^V;mz}-c zTDvtULR78h=rl!qtk9Z3ks$NSwMMB`IPq82rUvaTb7Y9ON`_EVG)FhN{bx!{f~Ci& zXx=0x)aZ)p6H51rZ_DUIAU!S4RxU5Ls)G&o>g&~Z%S|%qOdc>2AAP;?Eq*HmLThGu ztCsrTJOUnpkVOFE07W|?;n=6*HA+ri_OOIH#&Z>=TB*XZq1Zv;g0c->(F zTCT!MLnN?ShgcxQ9SS6r@NC{uSfFU+Iuw-W(@_FRk^K1;3*Gfl)}b)pbiCdv+=78a zCjpU39T#4Bn`;}&Fgztt7(fURniZu3N)$Yz16%SA5u^~6w1IltMWPc9knrKOmym0- zr2s3F(g@`fgfbzqIp?;b0!KO!UKIs+naGuo%`S7^A=)Dt-rzB6FUljrAyOdYQR1LT zghU5($P|<&DD?1FAs;gB!2%!U7K)HSA>?3nQ>)eKT$FMs>>U91Nkv1qD zP&{#6EZ`y0XXmC%xu52|Ah;fi#T>ovm{f4d1T=X5-Ock|vj_)H=v)+U%jBhkXV9a{ zgG!-)D9{eKwuufkapD^-e! z7AX_jojhkQLg~)D6PQ0Ba*)H!d&o>kqe#Nsosl`g*Q3tUr^B2a;(Merp33i*=NzO0 zGKm7gjL{bJ2jmeRZ^%ZJ-N+^CWbS0%fVAc~<`?E4JS6Xvml}$1h<4@~3d(Yhqae4N zG=E@Z1PRML!SS0lXF}M63D?Kljroe>@y=sTriF|PMqm&xkOjzW43~~*UJ8)tuA;a3 z2SSX#L7>q;=4Hm`Kkr=T+6r0n9|{K?D3J0Pt9@eb6k9Yt+Zm_@ehdDK5-3Uc69!v> zLYpB+7$e4z@y37(l8yO+v1iVHQTHeWJ?n%_;R0^V558Oq0t5FkAq%)2xDI*`2yJpVfVNQNW3YDCiDXtS3rJpFH?9nkd1_3{+J>i5J`cD zaf-Zv!}S~yT6?WRCu69>dT^DzP$6&$xew9G^UM7WnLPLzMeYQ=h>@w2Gm>4z2-dnp zSwQw~6ZyGWU?L6g(=368~@Yp8oI8g^^7bGZ!Fvb`jvVLaW zhP-Oki{JOuH|qU(XN(G2`ml_Bcy75ieF?}$QrM(wFM$y&ggoRRasZ+lEHLKzwa()n zMV2DpSuX|(A+lc+sm%8n%3>f3S&C6(kK`lxvvx&ZvgSu7{ehx8>eH3LdpjWcfk={crn)=(4K)gdad>Zkl%P> zGhKa5MotxKEB6)H1`2Cv3_%Ht4(UrDS8MM=y%fT)W1#Z4BFMoUg98jPn3FE2V2q1b zJ@YPeB=Z^13hjsd59HjYdQwMh7GaLTEqhe@eSXn>q;GlVAe94!mUI7s4TbSwKZe0W zx?YSs`!v=9j5CG3Bl8F$yLsM6Q=z|ZyatABG&S9~)g#~$xIzR5TqUmXB5!dzp2N2k z&eL3ZF3KN=L*RaI`}5jY4a|LkOE8~QH66F_ZhuZPK2JKibQ#6Y(we7C$2QtG6qfs_ zLS{o$zoNX-hg(lb$CWCgH&+oh%48Iz2vN$*OrGfNCk_NI>;6K<7pCTFkE3*d(XrCw zbviHRiSkZ9s?gn(#53R5=ri8fog%dfx1A}%IJhw949xlclAovazF&LKZ0R{=%qjx> zcw4;;kcn{9p!q>M%;*d6f-CeC_Pd%4$k+7wf@s0cY_sc zkhFMC1Q(Rb%cY1x>5d1(g$krPN`zlj9??Q^K#@S3AtO*a1ed_x9P$$-ikf{^5d#+d zN1x*LJ6^Y6xnr4>Wk+3q@Iu4$0V{h#D)RT4eTSX0CQt{M;XTcwmQpK%BcR-H-w0$B zkSK)6>v@}$YR?J_6e}50eB_JhJzqVAQei+>%imu9Q9%KZaRB`cD2WEF^#_!CggA%5 zAS5Ho$Uym)JUmczUTSGq4w|Qx^N)g8H|^c4djf?YeS{={7@?qGLxG8xJD~{wX8C;Q zVd#Dg`iBtPKNX&5y5xT0p1?SQ!ni&zV*C9H4S1(Wb3EtqvfOcEfb2oB3*nQX`-FQp zkmt^w?Ei738w$0NE|D=(x<|xWL-n(km_5tl)2}=X{7Cm{EYy!I``l}rY@s}}P~xM^ zg&>A}K)DS`;^cs2EV43Cp3-}WK#XpfGaxRJQAjT2GYamZ3W{OEaYGKgq4@=`Ry^Gx zJT9hiuh*-c!SeQu7(VZGWOG1fBCFgnLp&xB8IAH%M6N+#b6&IBgP{vKSvdvIFrtVM z3KhdFjDTFOVi^YE9TqUAIj;fwfKnV~KX?XvUPIh4fA$HiAMf7p6#o+?Z^>(nK@{m2 z@CK&cCp$VEsqtGgraH2o9Hx*p5chZm2kM5vfcPoWxNVPVaB>unYz(U|rGOJ;3La_X zt!2Cjt4qDeYDkT4dA9}%cx_QV-`lp&5#YBeB|~6r&K)B!HQhg75sBZZ^Mea>3Wagl zrl=RwWK1-rkV#c;gWg7K0>mfn)gK%jF_4u)et5v#;m#xSyYuM?TSY z7K*%tY>Zb3Zb&VRJA#p8Jxr7Vj9q@Ih%4K{j!G@?Z!38tN(_fFK2T{qGEGVifu>P=fL1JLZ-E&a8di+E{%Un0W@y;`|vhg2hm$ zRx+BlVFbBQ1SrIpZK?_Ge$( z=jP_X@%&|eV~%0}Mxn`!AB86KB6m3B-=XUU%6`pZms8lUv3H8rc#s1+UiSn03+g?o zXC~Nif^mICGCfi0RG4R-@t8Yupl0wTn!xuJN*iNb2ltsq@vLDSK>^=2BJ{b&96iI# z+plOI0V~!o?5CDZkkOW~Ag?&aX``(K_kFar-M7~x;1LL21o{;!ySnL^*4Nw4YhnlpDvZ#m%b!SD5519KAfclLEF;s^VR z!2YvG^8Vc19elmB?={)QMa9UVDOS3;b4Hn5f%ZXcN1qAf-K}&o$JBmbJOX18fsFKc zTX|!#?Nu1=7gtwVNm07J@4gvh5sK@&&ljhGyRISdo%0BI1g<3lPT`})7rB;EjHAp! z*+u9;6cZ4zcx|C1!4nB34_;f4hA1fTGe#MfqmX$hTiDn`v<3(S6a|nPISNnryh0YD z=t5D42MEfJ!OCe!R>)2~fKjqssGy)g@%*&HtOgV)7dkfZJ$0k_N9jZ#DYOL(N37x@ zX(3SvTZ)Gj3U2zlPxTzrZ&>ByMfS>}8kZB5{zGa*g3yM5B49xM75L<;asQy~m@DrW z$Xh%OzzeU3i_PJKX9eL9@%p3v)JeW;@TT2(K|n^}6+?JP3SI^rOIYhmEqFZ=&YXgm z#%y_G;Gy!=z5&cp+LXFq2mYZAgx|*N;|~=Y4?+n8h2T;rPw9Q?LqO3O{2TANe^4Hw z%=^&v$xg`x;eaOwAr$e*!4ru4hVKU(8w|dl|2$F%DD<&%EHyVNEM!;HNHxOOdl?8k zqv!kU_uQ;#674Qp1PW)Dr&8CdZR@iAihh8HG4dR*DGFYAq<283AxkHwBs{T(yOYJpbfoW_CGgu{5$~=?6uuRY z$2;VW7MQc}RO7mk@E=rq3S>UvT_GayzD4#zz#zZG-?%ck4a|e&^4uxnYQU=(53VRj zP(UuXNai75KP}nG+z9!+PW6%^gE^Pqs0aBvRo>J@@_-;<&L*5Fl!2paR6hhSvT=zF z3GkSMP=ER0Q72d59))v9&>sjjUKH|azE_@|PwF0_KSLGzLfBNsmQ)=O*BGCjD-b2U z&XW!!hoK5Y-Nu?a8KdRN6F<)73;)CC*Qn1u&TEl;p5&j!^P5n?Z)L1D)b;&$9)XKR z055;mzaNnICb^yQ_+@=sufM?sS;#ZTGyd9^Yh_3=^4k(V9#2-* z$gG`s&UwzZZpZuPe1$1qqda>Ug(7cw##r0^nT-92On?_P>*dQ0AB?PW1gqY#9>t)g zL~`Q?GALwUz3i!?+; zFv2J)K-vbA-fwj++H-qxfs^CU?my%_zcC&FR}7kFYMsqqficI68>3m)+^m^dbF<#3 z1PoK%hco}U^|S6Fj8N~^{qzl`0wVpxZs9xlo?;J0E^PK&VOHy|P21Sp@f&k1WA{>Z zjf*VBUI&lkV4^sg*fn3Lk%_Z*BqlDyKXG0bVC zU()_FCP{PYP#&5=Gqxs>`p#y}0gZ}&Le!E7&98krPw9VD-_e#jtnhMUeNcybDDki8|&=p=PK;(+sZ64Ves=hCg8YkJ9*CR_qy$(@0drx zBXBtgpbRHWc8Qcvffou9LdOLZL>F^iJWuhAf?$ATBuwCY6mIlS7R}HiFvBU1P$I?Z zx~@KjS|ubOmEbkpA;kcCfhnA;-%y4@NaMjkK@s)bfx}LTg7+`sJIR4Z7`!_b))U26 zpwMm<94JAsct_E9H46kZq&1lJx-~Q z;snnSFfUa&ZoE?PRv0Rr>)^JtI0lw%DZe-$yz$^7yf*pc0*a-<=LJ5a0K|iBoWh_J z-Vtm54=YC;A+J%c5P}b{a>kN8tb}}pu>I7$sZQw=xX$JLMcIRbCfL(`sO#Xp2x)`z z^$#`1Yyg4_LulbH?vT#^fvf zGfML9@<`%QCgdj1DkSKQGPI$bqb~+vM%j(hJXJCo8AeV`LSFM3Imui=fpmpP#Q-R{ zJhk_boAZzrv}2a$EK*4jQr3n14L*yvy)N!z&Wv`i1JF)++BTNEp%?EXd1tvibef2EwI+ z9R$xU3df2h1bgHi9)x&Sqm0LU5Jh~au8r|gVX8vz>w3%w$W4e9WG2^v+=B2TXDA`f z1M}NOz;MAsP)HGGn|bW53Ud|2Fos*C;9&gDl>stm@cIRu8GF_M+!J_Lf-&Bh7$gv3 z1JVV;os<{MZ(xB}YOr_WxqAFNkHFPIfc4>i8Gqa~G0%mWCZf;afCo0C&+m)BA0idsCjPBHBR%z z;Qa(^7uE?FiBU)wL|$k@inE7-jHYluVrWcs1JXeSjI7*`{vVHkN5CWC5x67-pmYhT z4bePE%aF`>moAY*MWK$#cE@qf+}tupF4uLOA8Dhf zpVWE#HIIiYBzV5|s5z=*ic&Y-t^MUpv}=mm)#M23;DCedXYekiQv% z9HKidE=lu3izC}-<&>%olim4q)gHX3Hy*2SIiLq$!+-V&j70$=Yu=K^wFL*P5-5%35EBYx^Sj+LFH#9Wop8$11AqRw=f~6O83M8+bfC$Ws=`mpohK@f0a2&^#Y%Zg(EG zC@E@0c%oQ`IC!5tl2BSf4x$91pp?Uti#*4WwJ4xalHt99H9Cr}K;e4nXO!nC1Gv^u z1!Ws#D{Tjl3l)@J^fO?w4>^Dr8R2A6MnHz5lEc2>Ji55 zRfXTf8-^Ttgp`K-;CKpcM-fGsbQD)8J~@t@aYe$kRJi~*$kUiyc#Jz?URhRYiOZfyLUoWI2CtDdp0<|^yU zo@0@*k{`Z=90JS-cr6np0l9@pbs_Jy@#fh>7BPPylbLS_X^D~^;}(o?c$gs_FcRTf z)tYZ0Pf=<^h%ygtk=#Ndj2t4wHAE`%isQCO`OCEy_A2M3WIHk%r8gl`8li0t^j+GYm;gnQNp|lHpoFDPe49HR1r3HgXRXv3dmMu zbH0p+zz))xwnFGUbEd7dh4~N<$^F6*5}kPk1v=NmmWQ#xPzMjU6%+Fn)~?8XM}IK< z!9WU6Lgq&5<{I;I!<~2JalOCN!hL`jVeqzdkm<;NB7Gp^cPp3dt;ypZ27zO_PwD>xk@DmY#oL0 zcY{J9-x)u_$)n)ZAIKr@Q;10JZOCfY%XsHgSSR8|$}>TTY{)P?e)s74;GX6_=UH7T zgGZj%p$JXn*srwChRnTBL@1%21L z*CzdQRP!3LAA?qiT`!Y2Sla`)y|;U7S#Kp0dmo?N3S^feTI;UmcUN5h)h% zP=9cGiED3?NPlE3*G5LuKKdWPm2u@Bp)KGUTxc(bt>80Da{~l8I50nAyacvu^!o|T zn|KIAyxy<5&v_&ZM`Z1cDeSS7-4}Y?dL@*C@>60n0t#o887Zb#^IpS_^c|5aC?y8W4V5OL!=vD_SC+3 z1Uv#B0gr$pY>4oM&}|k;`&Okxd6w~B##8uEbFI=Utd$OGnq>2=v**SuwDn0{>&5C# zx@NQHx(My%_PD&yE0trKu-TLph3XC*$901XIhofS*`fW9B0yyoS#kO}=aCGVU8OML z7#vsB9a4&o?b3}YO`ANUi5POQrB;#jTC`qJXl~_&rhYLJp$J|0$CXecIU07 zir93_o`12@KJk$S*L#=8RR6-Xu?=1;hh*nl)~&V8@3`G^?t71we(nnv7J1HcI3yeF z8|4wW00M7_2)ym9Kd^pTjz^52c!8rv_G^?Bgfu0LG~Uj^1;qr476{()Qfi<$K{1Ob z8Hy4VCKRms@u)%xz-AsLIHWVmYe--UVF$5}=QExaC`<56qLA-6Bc6OIQBDa!*wj2J zGEk6kHr^g6E&fYNe3TemgXK@E9HWRs8BQAq zD|iCu;4who1EB!<9uGW7WAH-(f`Ek>M!^_Rz=02B24n-uJQNxO=PDPeF-<;biX$|1tqWo44!VMW=nrk@-V0WOL6V@Tge<_DgZ8701pB}} z7F;d&0m@s*BMKfUGlUZguPWV(0i`V7Amju^K?Qcy8>;10v%)zw*4ytan{R(}$MRum zW|!`%b>yC z-CYlV-Fx27?yCLJFWprwUw^XJnq@y`GFqYnW+`o8jDRC;HeN5O5+(fukI}fu4?+WR z%y@ao>WN;|#>7V$rw1oo2H;Q%i76>ut^oY^LhKxKm>1ae>+$C-@vy|cFCuyI$$dmA ztsygJ2$s~pYy*DSS?JHhpVyn+K~8RJYYOVA4te?Tk-QXWXpdn5 z3D-}>oc)SktO4>gA+KqOG;F!R>-K{QjQY(s{|3L-H2;0R=GCTubam67GbqbvcRBdw zbG@|e;LmGmfq)AxyC7D$@bSW3lyVduCC&@4FIY2@lfMSspNP6|AMB>q-3$Z>i5wE z`Xa?&3PF}vi3tD6EaF%`OCAwK2q~B@6P^LQ zH*lRN25B$kfB!yxyVvFGuFJs?=2W|y{@rkX(~NI(MdE`NV{Y9la5i3qcHhYmWr%?d z+2#9=1weNo)S-HWP(hK$Po0;Bx8&uhRbI(qbVO+=+NZd6aCWKi8#(Ofjd=!A~VzIZaQ9 z1*31b>EoQ*WPXRHE~toW7=@p{tmOumUr=;6tmD`NkPRDY5iu%+c{9@!AG*YXOR$iS zfjN(JvaRg`Q)AM|c)B1`VSfXbf9cV&F4BlP$h=qXGimRE_2a`uYs{X<@DH&=b_>g& zbA`t(vs9lU`j$3ER(Ls2Xis~b2Q#TH#(vIs8P6lnUpM1gJTwK^G&vlN>0DOo)J327 z8;)lZg^g-`w#q8-%DwuWc@TsCBN;zyU*De?1q=vTZs}I?B!0%BBX+ZP|6ykJdn!-I zy-j*#)z=cCj5@<;=av$?RH7Pr7&L^n$t!WSDr=5C#)l8en zq8@pC7i&>=s5kv7Y*yw|WsQ?a7sg`eDZM!dUHWC*ew@IxAb@;Za!xmkSHVVcQE*g5 zuN8K;@XODlbIFunu%=DyF>tw>k#{>zbLoRhMe#8AwtxdVh05V2 zTjquWf)C*ur}(bL*X@5gCMVl4W0IPhrD5G)oHpfnqdWLp$8=XPG0Z@#j@yyO(b$n< z1-{qo{;%#`wFxB^#Yo4e|7%+R+Xr%?p%?A8P-d$%WllfLSYG^UqpecYfrK!kzR5Zn z+}V~0hf+QI5q$+i9AiNocdF{CRV19=nT>C*fUCY^d1~^wZMfS#0~CZ?~socHBB| zeaks8?8UC<>$kJE@mjskrf*vbo8^A1T=-lFPvIcK407{iEOtI>AuK3yZ@mrLT33nQ zNgk78J*@khBFHVW43*YC8zMd;J}4dh%8J+C-WqP>n_xU@GT5*!c`8!Zhhzvai(XL- zG^cc>bw?>iEpUrZuhpXn{Y_uTVluZdjOsN=wZtyK6ST>2`6;M<_m$vMmtnR++PnQ< zmO!wblt|o-@Ka-ZO1B$DR7F!$SBxnlFSPIpe)A)VupH-Q&!bb+spI|$NH^rt~K>XPuIiaxXx<&Ma5;!U{iWa-pc2{Lkb8@#{@cIeZVA~ za@4nLrnIw9&P_Dyo+ah~RkVTxs%+hrwrM;ZtpWRSxgM1{RCdO&hpO3Fn6xNG^3Gjf zT+a5a7nW62v6*ynk96u}mur$1>Is)NH};C}l{Do|ig7jSt-SyFI1y^My3qftWAg_| z{9ZR!028hL;ex| zY$4O-MT7NnkVuQ$o>^+T-}bJz@V}{5IH51?x!Jdd!jhJ*bV%%3+CN2p~-U#ItX*Z!2iw*PMye?Tr#1vtYqF zcHZHU7YDi?`o1>xP36%wbCJ8OL(IMN%l)s^We77b$0yS%P72vt`JQ|x=GounI>tVT zcXI;n2NT}zhufTPK)B%O=$9J1LlR9&Q&F1DlsO@ry@OiYKq;tnL4({Uak;+MOg9{N zy-I>y1LAtnKAyhK$V|l+zoojQ@^i;{{Z+6i+Tmti;agtDZu?(i*}5x#%&rYj(H?*3 zP0MCSCF*3FEpv#@XJcgMVwIRr8i#3jFR z`T6zlX{G>hMt{4__r2TM|0rAvQc6Rqr%;skIRgIlbne6(sU=Vg|9q@Q4DeIEiqr6t!|A?=U zO@=c>{8PFVR=y2mu5v;De182{z89eiM7zn{A{jLJbkMDI)gn`s*>ro`o8`{q8A)*y zPiyGkwJbg!@Vpm!>n>$p{CJC%aWE^@E7x#?H6ZmQD&o;FVbogZ^|#sarVFJtE=>Hp zcwbJ#6|T*}PJ?YCc(8Q&xxqTDqBi{GE!zN}VX8J%-~vE`;j8jOGX*IL5jxRgq@T}TwX|x~<6ykze zkXSg^KjpoDL_YHbVdWj!B2r0ivitLh)9XtddJzlvcRU)_S{}1qUVhBIyfE+%KnFZy zDIQme`>l$X*6(*TFT_(()YsAdo)K$Wquu>*UoGd;|Bvy^SRU7whD z=lj8bKmU;fxjM?Vbit@%&pn6`W4)BU%c)t7vUkFRq+M$<{bQDKU6z`Y0?JviRncy{ zDY@Js)t+P*X!x&B#06LNXvcq)m{sPNn<{4wA+GPqgLE{%{)k?0r8D04k4c{17(Mw{ z=5t7W)Hs_p&DVMsY)PmE*!J>wXczawi%%{#8nIu1{aHBKyjcUV)@h8plr{UuZGKGu{ceGH{a4@*zThp1xD8~44kX~G|6#;-+27yGS%RJmq$57wVfOf>L+2bGIG49OCEu<*e%seDUsJcNoXsMs zJl|#2T*0Wte!Kla3^-REXlo-#pjn4)Uq3DLY+$3J1LALM?Cuw?rMX*l?>Y^vLrI$bk6(RMMYbBst1KbxgeNDHf%Lexml9s(xir-2yo*mK zSD&sX$aV$4;w>jDw9b_Hn=j7zmK-g3JE}=>oTI>cj&&i5HeLBaXK;+t)Q<+}*bk9F!n(%I6m?8UlA zI;iNA-az4VxZZtxmQ?wO{wsjT>2>$B(&Tek!F5f>JCvl`I{X9Q52T1*jl&Y@4IYYEj%9}pVSOG6zDqzi1$*10#lpKXu9_07`$Y)7qo`^Q?& z1Z^))KaQeXBOzh4NKiq!66fmp_p_?9q?8e!MxusUz~_Cyj>Dm^))nGs$fu7}RVgVW zwc>S$BGF_HN%`Rb{=%Fg`7k;=^D@CKP8whfof9r|?08AWhU zD|>bnE}SdlE=Nf&dEN*3SjLD}9LG0)>npq-083Z1j{Or);T9nq4kp;MAk_p^)u&il zV|R)8z@mTrGKVtWVMvY3%1Gglt^EB5)^`{^e$xR&1p|QCSJMKC^AkUJ9Oy)q-<%-_ zw|68c96g$o+7$_pY)PeHFM)BgQz}HYQKiI8MSM^DFl^L)JgHdW(|t7WFTg#OTBOG( z;k}SbBehSRj04Th4IQMN)2!+T;hxBN;qo8`zts~bOWWR^=4#)B^~)Z@!b4@5o#8Z1 zD{%UfqZlEyUe_>dIk3sTK7sx*i9{t@|< z0~`qnP_e<5wyvPt(z3QS4);_eAb}zYD6_g0&v6Bf8Q~Y(EWgQtvQ$ws16Xs-neIMj zkG{tKJ9$l_nvMO%ti(IftVCHA^#|z`5mE1G&`kk^3`zbtmF4=q`sYv@kP1&Qx!wG=AUK*? z)+@&d7}lW^2ADFiv3Cy>@F)WMlsJcKJ|!w%)^7%(zun?BjozQ8{^h1tzeE74|MOuJ zHc({*vaNzUgY%>>c$X})l7E}wL&}SkHbSYh!Df~={yeDMH?7VaNl_`62)cYbX+za3 zrkIzsoyXg^tj++M_2e_sXAdkWfMVg(v=zCg_{LYdFHcKMg7r$30vazlO^om$dQ2X# zGBdSUGt^j_KFfiTX^}8XGhQpL{NuPQO82wDRJ=d3b6Oki?&dtk2=6uC-{;HU*~0c= zIQqzDM_R)1!F{YyW}YZt3F9}uv8mM7YXpy6+S5o{_QIbWg!lF`MI?SM@->g2z8?L? zhr?Xz)~nnDpff6_ZPIWsv&dK2KAwuR?eU%WlzT14N`J+R7T5o0f?A)@+|&QQX*bgM zh1m$-cqlzilVQI9_(MQ=A;&3Ft~fwhx0h^0b|nj@2KTWa1vlHIBR*>jijYtduAdBr_zDjeG9O!9p0Hyw`}dA{*P5ppKC1;kg>iR7XkNjJ7 zlEduTY1|sA-bhxsx^_mVr3XGne$Ex&73N{4`K!|jJKZ5h%ja$5-6CkEw+WAwGm@mT zPsM(}9lkf=7`e<6TpwJ%LrHLRPcV!_aiwnlF(y`I3h4rD2Chutc0)Iiu3VYk=sPoy zK{LW#kLLsGMomyD|J(_SHaA;V4Zr%K@haj`DHDU55qekDBCYi;qcEkhf;{^)bgE5b zNA!h0=`o?VkJfcmX$^Qsid(+Z*muvqc1b=S_vqby)3Xb^#59N&45GrRP^PU~X&uSw zycV!_sQBozDwm)yqBkrqkm`lH&wUnGaW~jV3NI> zOldSf+1OrtW0?LE=H=ZQ+`Mt!pL}8e-mX5vHy_k7Vlhs>Olq+rJl+!)g1fx`WLTcq z2;t_Yo>HVH<>EHSx(ks!9emvGYJD0o@Tf+=bj$QilT_>K5aMV z4`ka;dmWy^McZ_TQT3ruk}9W@bN|*i0I@~~ZeI2u(N45OuKx?=v<1Sp0mjFTuNP?3 zrmV#m8kK!QwEot*U%K05F4-6@KWXfK$Q(CSx4qq%y+U~X-S>;Njjqv~zN_B)IOXrs zaMesYkmR9fqZo5R#xr`+xcqY?`74gdn`ZN$6P;coN+Yh$ry0Uoup^%eC5I1&5oKk1 z8)kdeNM(;mdDE2tmdpSH@(LR6h5h5eHwD5qiciP?eDphh6>g~acwU_Eon|khyDW@-Ud4fJv?e?lBF*Ne4=>zCE zOS1CEnhZjtd@Mf+2BY>2%kb#MryMOCF7HNX#=WUT2VsUXME`^qJj6<3g0Kme8Fh6L zG*~)k{(&3dr0}zL`Cu5SRPTBu)0)48H8vWQ3a}{cW_t6KXDNV<1?|zfK9Ab2tIp-) z(L}m^I6L#=QD$S0%ZhM%Wl9>#hD`>Rw4dayk7FsB|HVf9jZdSeWjw zv5^PHLJnt25oI+w0FWnIjNwrFIluZVOXuPlwA9&hTD0OiCySlYD3XKVbMwnyBH`|`niAz1 z-FSPF`dmZUp8^#*`X?y#n?$6&;T`uAB>Z!O&OB`^Ar+M}rvJzBen+u4ZdL>I=hHcX=~_Um`8VOblV3bL^inXB{3jWz zR^2v~9(%Y}ESvhJaz7{BpR1ZTGhA@3Sm9Dd#=rvvbPMauMq&K5XOC70fcxWY65#ZzY9bFd8mOp(Iz|w3#|2b^ks4{ zv5*uOnpcl1W?Xj0exw|Zz??xLX)ozimm@YBA_>Q8yOVJ4)qRCWYkHdt5y4oX1l~9B zGDPUl)fxkI;-yv*dhxeN41(@zy()R#5fLc(DOapB5KpzgP!_GpH|(P6x4alYb%HYs z{1+b=kdG=SE8(+QEz2NuX}Kgq$CbQJn>FLpcS_QRp^Kyg%5e*W>PRqn#_+%g0jEIkoW!_$)Le^}8?DpUnQ7dZR}!aO??QJzm=RQB zLT`72EXmh-?7g3y;|XFGLOz}T?2IK0B(W+sY`#YVkfxe z32}=;c#+TVIfm2phUvFZ&%>Jm#uM7fk1GPno8f2F^KKI6g+iH5l|4YjPsr+9n6H!v ziUjo=fKu~!wz1n0kJAuaW_mznt&C|#J+!m?bPN1_#DO}g$>MRPFWBGgdk>jQf+!9` z@gxd;6W8zW`;|rg&9~48e*@EVwcT+5G}?s1-0A0u^kaN5#UKE>&CB<=YTH}5jfLk8v}~q%@lBh@3d>T-tPi_0fgZ!q|vkI-3Z&T!es9#+qk%sU?n|)zLd`R3Tr@ zqq_!!qfG7?7jy}peST-GOyF$~Of0&WZ*N4Pc(G5v=M~HYqS-E2={vg3TNBDFFz4NX zvhi9FEoqyN+Gwl=M-ZZ9YQLx}rpkQrTeo3*Frn~*{hVl#viXaOK_V#lSY2Pd(C_Vt z*RQKO95S{YE(kiUsbOj!vX86)|KxVGho^eZm1@As4emDO3z9C%T(xaZoTEuy(UItFBTPR-6UGM8r#M0(|2DiFDT~;=wluN`FVHVyD#EYPiJ1ACL_%=ku-X&nIN z4drUP^@|ki*+D`_;zHEmA6v63;D%qRbj2&YBa;9gGUoz2hhLLU`ZERN!_3*tSK9D_ zJ0WV>Hr!0c#$BUHKZAgpu>+1*aBOCP`~_%5{1F)vyELYMGcZLg?2;Hjb%2(SZ#qOJxG+| zZUa=$yA!o97E9pssVTqM$sZ9M|OH?#Kj1XwR08YGNl1vp#r(U_NpfQM)nP}EIY(?=P-2E`UZ zYa^!@ei>RIRAGxYRJVtL=k7c9u*X1rb3 zTBULM3&{FgYzWB!;J;Q~!2uAcw!t#y`;TnRIG6jKB`??y15-r^|A>i;E@lF; z(LI1SOR+#5s;X+>CCMy!$iZ*WmIm}Z8 z8}yIE=?bD=uf3^ogCLQW9_zf||W+33uJX zY_nizrpI9YGj=~FHD&=XaCoCGdx8BIHTRmQXEt$VvO7J3N+Qw@s%}Os zC-Ud1a%r_FvIb{W>?jwWRZrnmQF33lWe%1#s+-LO=i5L|()5T(b2`o^UHYw+(8XFnQ z&^d<99+0f?DI=ThzK`u-GuwP0qd}38UgN*obtQGWL+;H4dys~#;}0!GJN3>;Flq@*cjjsd!(!X;f?(+Z$>re zLeAwzbr8pEdH#_Jp}0qGUFQ}FA&nB-;Q50Z)5g!P7wtWX<9LP>RM%@Po21_o5A6eI zhVKs1d<<(dEphu_3#yPRSr}^85cO-&rEe2y_iMQg$dI+Vdj7H_G#;8AJ9W*yj5d3= zKdcd`-=?+~DR`9rb`kj2gB8j>Hd*?&bego-x;#@@nR-(1GJVS4HSa?iijwo&`JZk( z;jSIJdJcucj_KXu?xJPymiFV0$Ax4qGW`q1C*Q#xS1#cSpR`!~(J(2%#}q{+Z~y2| zDOXLn*UgA7f5-zPQGxprwCVj?y8*D zy$9jTPE}~3a41rA85A9iD!;6d39z`ufl|;bq>AB%sY1CKOsYFQR|BO03@l`6Vyd@K zWNpUT>No`nEggN|R;kw~oJldbEgp{MHt&)l`wW)0k_tUJK!QLC%`kzPL@Hbmcv`EE zYx6eBgwP>Cf;B7dbpMvV*WiN?9xm+bSwS8%e4<^#jrxTZf*1G_$W}=6pRevm)b-BL zf674%<tIN&tzOSh*7L>^Ve>fWLBC)G#3czGJ42ui8fCs_8l+{&o%U29K;zuj4VJwfU+q5E@{{Ws*AvyX;OXc678hv z!eDAxJ8`7@?Jzlsa3`az^SA@AavECFU|Mtj2!kR3aO_@&LuO_#V)_<^i#7pWP&SEC zt-v-OhSirHj1$Q}@gR{x$C<_q7LNE%&n{Yt#D$H-0<5N^!>HfLD_)4ReExy6k2gJ_ zgAZ!w#&^M!)rQ042cd{+a$^%O-3Wd0B5oV9VpD1Y=t+|Vwgib8%{ieMQVVQ6vFN8X zvZGT5_8|OzaPIw`Q>*|q4qyWb?-JQDB|wbvyk=Emb;%@3k$!Xf)#})JAL}9L$B=>l zwopTTExUV)Gm%bYiWj?B6;-Ep4>_vO-oW_bMHrfWYZ2yid7o?a3?#w{ua^r>SxI8x zQ_tziPmsdJY-B^QPLPYL06x7>M0qL{ax~xDo%t>s2k3&OuTD+gSe^@*o_q-JGsJ$m zVA`a&fYxrn7LP2I&#P^-;`RHoe+}=pfp=@)`6hQP16l(}Y!*E8IKS)fumrs_m>)F_ zKQR+|wb++_-r$PdLt@7)h1#Z)>x2>j@b5F^k#Wn^e@j!+FfM2h@O|jrTEvyVtp(($ zEWh+Pai_pQB5`gQy(AmJSTg_8#Fo^`-hsP~Tal#e?_voIRBY-AxRLQX$2aS(xyyhL zPIOQQeBAGR<7$A~kFhGzn`He+u_82QP7&ip^z?!VR2qCbJs375KMys2|0pt9Mx1a_ z*&=Elg`wpq_khl05s$3zS+O=8l*qPnATB~^e3EFBTAY1HStp}u=H{QpJM~0u{_4g@ zkUrmfiFUr33LPpV9FB@|vX~>tP4T6L&In+hhm5zvh6nRVj4T`*T8>dGboyRUjHzJ0Q6<4lpJ5AuD`AF8LAknC@@7T%A|DTH3^yss`MC zrT+Wg4NMtSeHrU_Qr3-dq8K)e_VlA*$H6GiEex(;a%fUSN$|&y`4XJXOQdPRuSY+ zZ@~ACRlm*GzP!gGavd~5){Iq;Dv$L&s-@#8=9p9?|Ih*_C?3?9R0e(f=7NFmQuM?9 z3QnwOVSei>*LXpn7!TtLJoKEbi6Q8z+4;%%fmiFnNnC{5FRIA)ujSM^YoeIt&;-+! z-$A1I4=<&(gRvFLO&&Bycj&Qjm;N57QqPM{$k?pNk6sf(n)~k)pC0g5MPI7^XdGAG zI70nqUjZA_ib)g92P|HB$)>b4y6vg=2gKg2Vq2NlX@s1lU0Li(`Y>MKF)S;yQMy%3 z)d|-qZ(AQ`GR&LQ-)G5NpyQ`#PD-m?V=c%CY#K@&NPB-xL!@QpZ$Fqld-wM$Q)+l< zI=9N)kGz`CIm>m+Uz$LnF@1?PF+IB5XoqDA;SYWYKQ~I^$MF5xd_B^lU?Yg|lhM2W z;{D;GVP<L$9GC0~sTDGI-ontE;ll&gMeYt1VCDx7PWj-45{*^m$`vl0>TTBv0QjIP(( zNOkrwqUE;&`g+ zJ2KH~(gc7?Cw}mlJ2;a|$Li2gmJ4w$O?)0{^wIhvVAqE=i8)+)OgMx#eAt(6GhvyV z!h=lPJjXq zP#`{} z-q^=*tkMYmRnH^T$=tU1k<_N8a?@AekF+U3e(5S|Fw$U8l@5=qO=cgzxmIO8O49|b z4@}ZZNp0BSpu=byd*khwJ91M085c`BH(r*cbM!-jA+kh=>P_7B;+}oypFsy{Y9JZ! z6o(wxlZ(vTw91EM8JsO0uu_eGnDO0Y+BOvXGlc&{n5HpX?t+#xx}b>*n?OM_(hLg5 zgByQFith!Wu(VOoR-zgQ_q|Z@tZwcFhz4+GX_^U?eKsN9S5XfP|kJ@_s!8 zZF6dYl;G88MS>9szcB@)dc=-En|7r*APe>(MKczlBCSNb?C9GL#cF5Y3n>kYFY8mp zJ_eIO?g>oK>#Hb#_o6o&M zpW&oV_f@o^Tn+Uy@A1^G*94@Bs*199{d~I+inUSf68=iHcka<&rd71)-Jc5Af3vcf zrgWsDs>2J_Ijx>bsMn1Z+D3fKXI`zzX<|wCc3W z!}stK#b1uY`^&;Rg;3cR=gNpyVBV-2OK{S3v;%rtgz{mV<)vhjV>CTxf+^xnIXIlkS-5o3T3 ztur;QDr8(H!jkJ}-{-D}u9<@fe(BBPXa5`z=(`KPaLZcxLYY{Z2q&`5fC~YhO_72= z$6Q0;siQGcU!M5G!`bu%bN(GuS*4MN`_yG)-`8?l`aY!uZ3wF~_Ml6;Fd!{ya89}> zY#Vt^wl|D3!{aXG$}`tNXTN#r19`67TGf-doEg2b|MpkPum(+|Tx;&Di9+Z|b)e8K z52?)#=%72EwvR`>*5i6t2j1aNVu#REnqs^|T|%`)&F;~AkYK)_p`fd zFtqw9v)Uo?9T2Y zzuyBev$eh+w5uw(|G281&@A+&l>4KT{mZ;@FkV@UL2i3i56eTyaoz^cOq(GcqI8A) zOnJOLWoCpSN)dl2inYwpn=3fSNtx*HHzZ@6TJo4mr+uWmq-y_bZlY}G7~Ji@Lb1s``K{50?e7wj$!o=XRl$GBRM0 zNNT;r;?6OP6K9W*1VwJG!URU&gF+^0zt=jraYsA%66gi6riwRtfe#NM`=W+W9cpT- z8vjb=)QIfpI^Xu568{8s@$c3Hj^39YwGpO#g3POrm#dI%w4X_K?$fQYMK4Pe!l)7| zfbroJCcdZS2pMpSlNC#H-ayBz0z{U;aKUl+&TSpyo23}n?_bDB07%s-%21t>^+UGW zUhhkd0HpJ2ggsV9^EMKozPGrKem~(jzFJw!$vTxOqlMNF@nA2PTEnNqMcV52qABiTEVmiJaSL)K zvmS5G_Rbv+oQ*jT!{SO)gK48rpc0V8Xk=Pp`F}X_iXl9H#3~fUP`HGBmAz+Ls0)eO zVc*h@9(90t0HKpxBL%*E^Zew$%?@XZ^CzcM~@KUY|9AwBjO6K4J@WmG% zxUy;Fy#CrEOrJ~z*pbrDG*P*udW408RUmw-+C2~(LC%}d^JU*lb{lcnb7}UD*|44> zX#ilN@5Mm{F_~IVc;TAH-@dt&;;{+{l)AdT2H< zMNS_adndyGMqH4WOJ;FrxBS#>GK1#WZZNRMJK>&6keN$G^SYv$@vDssyxHUdtV*o8 z6z*MivyT%nI>4qcXxBsCJ)w!bsjx)IPo5*7s-VjE$w{+rQrt1zEl3;PgQ7GbzfTbF zyE9JH;A?R-scO>4Y-}2Qm$b>X=uOY zBJ+98D$6_4Pe&)_>GkHHCyjUXExH<}A2#GMLTb?zDVAxW#1?_N=K`k@9 zrCDQ}Zk^$DvrOnp!iHSe5OpoirjO=nmmq$X8QGln+R^)QZ_2OSmO{L5X03hxhAz$L zo9?b<+*~|d<559LT+u!lO`sh27)wt-FqoVt_ zwY5?9tfBuDB4yp4w$8mB<1b+G9WASmzhjIk>pNTZ#JJ4H z$$DQyYPE77cWCox=Cs-`-U4TP!@OEH0o99{KR$EfyPNd83Puk%<&^9cBMVBi9;qEM zyKWWV76`TE>D?CyWwKEYD$9xDVxD9bOit&wij^-nRVC}|3+q!#3=1jMn<)(_N9|JE zA$5rkXT6g_yi`@({87@q<+oZM-BI^gE;-K&`{1M)4*DLq|9SUVpgI5GBgJXT@5cq6 z*)N0nHvi;%;{-*Ws1cT@58%D?&dWfM={L|T!dmZ7$<0kqe=kV4@a|yw}@Tu=MXl>0td(MX&~B9V_?`b2Pt) zrgp=H-F7tAQ*WPeh7mRo_?#t-SFWCf9t{O@<=)|46K+hGDxa~JmjD5!^R9A`R3K5WQo7}3fhV|B z7nwVG#|XgVJ6L-cCcqpo9xkU8@HHbH!i@?O_{H}3FOThc0ymWlnWnLbvA95Cp(RgL zyH=&`HJ~p{iy)73_mbk*+Fh?z`@MvE5d zHHU!m+rEGJAV){x_&^gG`7bQ&_m1&^>Mu(x zO_G}8yB^#J&ip*etlXW<%A6J9rsE&&#k_h7er-H-4U;uOO~tSRg*ml~rL!FxPq1%` zU>D9!$i9pDN!j^|-JUUTB|;Zj<4+<)!zclGkO(QvHvB`gLlchor)*!-=5S7ehB{Sa zDOQ`bqxbSlx^?hqpW~}e-hC+jl!OBf3k*2}a&h(a4p!~>f&8|-;A&p1?>X?l$$ik+ z;0NGIFN@{4m|kOWMGpi-A4-$a$(b4#s6TwAEL?<>D|%12kM6M+AjY8QDWvo#kyU^D z{~)c5oCD2Q3(%IwlY*3BWa)bUQyd>h%!clgXfx=5q2A6Wo*05c2THmL2^XQM9U z<2Hp(E7htzFubP$`-EVE=t}_r0i3~+E^R$eLQVD#kc>rV9+AD_q1tg#A0wna2C_>q zjE}Ct-I{lXW|NwJB}IJW3vJo3)uz}EkB?!_)pdZ`1Hy504(`L;FqS|foy*LO6xNm@v(~M(0kIsT8$yQ?SSeEUtggJQ1rX(0pNs;ew z38h1q-=Lvguu}w#Y8UfY#+9-sjoh{lahe^G4HL)4N}|$uq=E92V++At?meeo-w@v! zm4r%Z@2E#%wmegAwA#DETOu^9y_ii3j|i{E=t0j8T2?-p-nk-97`tIuVcuwFeB2N{ zo$50|+L~hJ13O82AQ1S82VW^cvvCfpqZ1rtMF1e}LGZke_AN zq_()(X^I2U>7vFubyfN~U?uh6=->EmhtthX7q`MJBf__7pJzsLuqvrd>|I*;NTuM> zlb-9Jy|3ILwA=ymy+c*SBh3jh5c%BThEpmp{LKo>FHN})xh@(Go+^}HUUYh?eArxi zsh=gx4BuaO&!V;6y&lGDp$|quu$79N(UtV85mjbkfJdGPNdrG$`82;d^m5cPRWg3N ze$yr-^VkRB(%2X1<*F!`jSxhe@z&!R9jw?nKFwbJAFAFtIb3g*bGujft*TvB=bXKZWC9Chh+DtM0>ll7 zm1RJ%NmM(uEpN6m zHxz)ji>*0+@>Qa9`0+b^Mg^x562IxU9cHnvTBsT#i^^you$tJ3mTs*cGt%iaF(F)D z7HZ?&To!KpR^B^{5Wc^R1_^ZNj@@~~Q?Vbn`q?HOZBFmvq(Pe*5Eq0>)~5?Fad-@T zdNhs59N8{6@lq$c;t$Huq&`pQ`68}t0GvfNnnw5fi8$c&pJgi?-iX4rz{bt>k1}B2 z;CjJQKPIi&{R`GQ8G%Q{cTgY?aTXD1-vjM)28`7OWZVSmBfsRK>SELU=(cwzU64Oq zGHDhsfj!Up!okiN{}+Mmf;JRSJ#R>w+FQ0dy|x?M5@e|Zt~QM#?m)*sfvxosw)=H5-rLDRO)k@>?Y9d!DYV%vQ%l?3S18%Fv%eK* zy91|(LEVP1!J%;=g~^x&QPi3Y@A5g!#Rm9tH}BB`pJ$X33Rj9*u#Y);@mhS75>9}f*@4ru5GW} z#fBvry<#)QQUOW{vSXY?#y}r}`WxJ*+we1j{IF|;>K#M#HOlqcYjbO)sp^s~Oms=_ zsSQE-1^z0{0-r zI{)s3G5_NlU*A~sbs+|uInV?Yd5}5~l_&^01RJ4{ZO&ax$N!SGd#LD9yd4CES>DKH z&wl3J*ifrvuBedj4@?m17g0RnYr??JUkE~79utt_LZx@{*c~|<)+byL9!6VWQO0A& zhGaLh=q6{&LFtU^T!&HM8z5MNc-gP_QH$AN113rkwf5nZ(y8&#ISW=XZ&H(87GBAn zIwm34!!1u`Q9vQp49x?OLxGK-IxHu>7BHe$8cq1~_F4Yfmr*~WYKev&zQJisgi^vK z9-6R|&isH;$|A`r<4IshKylx3*!ben#=T>55u-7R}TX1Fov}l zB%ftycW%dwpEayC@T{jhn%+DM6)_^pP``tk3QZXK0C_-eMDhY$kr==-kr?r)B`%4M z^WdDY9jz@JHbqJsTSxstv}o^)T7{imikHU`^Cjy1Xk`z#%s-7xlATIV+kU;swd_{(s%iA)p19_?l#$wtzJW}DN3Y1rIYZT&(zc&FVsgzi%cDI1RpCZS~bP5S6jU=-kk z&DP9stD*mOmoaNnYB?$qk|6oi6G zz_@$Nn?;OsuO?5a33tb1M$pIsXDWA^0Sz)K#h6UuYvW54q0 zE}VAm(H+GDvlx3K0Flx`l!mY}i1IT^PM_oedQ8RH^%m+qG>ZG{%&q2^p%n2lCsrbo z(J-juk1X^|VIx+MpQ}C=7SV}spFC$Am=e4R>W?O1g1p5QCg01%b9dG~8g5C9{qYKB zhR&r9_i7+U6oaz*ODD|ztyW@OkKMQMLwhAL{#%F=aQw)w_VR)2To&998;@TXJY2}f zLikbZzctT8HY*6uR6Gb@gr1S~>qE^qIYbYCFim&#f$sb|0GYtx#?QZHcEcLM0qwjH zYL34zTjVizMvNaDDxtz=I#CUHnUqy8AnVqc>ALmFdU)06HW22VU@^51zi{7KO21MV zkn36ye$=FXl3+K?`+E#B{7<0Y>2zBPDZy2ih)enbS$&ScUC7;oPN4yow4~*9+Y}wn z5=~}SOzh0lxZir`FKXEA5&);n`=qlMf(4T^En8tEd1S10wh9m@yZ!9g^c3e9i;u#-WE3iq3TJI+!3*TD z(;`M@aLBQ={gB3tYm4jLYj;lHeH^Fpr?fDa@Jq}rv@6sdy}LX|Y0n!Q_m>P9I}qEI zsq&=Vx$IB%$!IIAC9u;*g<#3r`HF&6U(xJ#x4Ma{-~K>t1GB}PAh7B1Es0oKMS{}B zs@c#6#@y!c>~<2LPl1sVd?}TH#dY_whQ(%l4cZHEP7AkMx$+1|lBikA*miRYt6NGA zXBU;(wp82M)eYnQOOHPXufw@I2~nnj{|<31z$20+6S9 z#rn3F334xx>;a@dq_{#p^CE@>3j*kbR7#!$f}+PYI!=&!I1ea@h+D77)po9jiKsNL z1yR3j#nezvaYY8t8h1j{>FH$)kLF^F&16GdKa0Tq9rq)qpqo|P-64cvi(og{ONA3k z4TK;{U3!2kT6!1Ikr58|$&(Vv<0{H>2j z5RmI>RnRzWOIq|z63!G+@EqAByfv@~dzbPy#WTVO96~gPWguJ-%1}JyiffwLFUqJv zEXCjhaEzJTrc#RHeB|J^L;5ck$@83()6KoB`6l3_ zu$&`Qv3(We8qU^JFlkoN1HT}GY%_&bMJ5Uf&`yd=2a#7Qz&NKkL2kW-1?RQ3h!U^9 z?=m41je`Zap|9>dcuGGh*eGbs=a&1^DqR5tiCp;oXuo zaUQf>U!Hbn5$_;^K1qx;}Elv^51-7EEU!XdM zBZ+(7rBaceI{=NNgQftQnb?D(TDi|UN!Nu{3xb8W#r`ibc$M0>r`BjDJDZE0M*dfS z*cMVWEQf=|Su^yx7Z@LWn4BFI@5DAq_~Lbbzm4&9>nQN!Il!Sr-yTl~A4?*iITJiX zK0LPedPo4{hKZ*=iVwJbQ0`?7K>j?RPMybFL|K?pJTvg-Pz?-kJWJebRT#uS1^kJ! z5UQG{OJ5v9V`_eVdnZVLefpF~_^Y3BXtUk*aCyWN)hs5a_vTYDt{W)|SqJhI?l<%r z(p|?40CxcI3g5Vy`nDfcVT2#?$Otj)cSE~gZ_Lc4md>3Os zQB{yb)E^$i8k|8AqUiydv}+YS!1O z`Z$`T@yEjI)*L^{9MvOjAgGV|W|ZF$ZmWWGs%y+e9-7 z!B>qgixQPp2Kxb1*_1O}`l6BFPh1Z^V@Mpi4U@Ldfk0x0gv({^5BNp}K&t)}bvib< z?(V2d{FqE;b_T@JC@PCwf|V)#x9MlJ(PyPRwv1TVIf|L(V=ORhDp)b~DuOSaXDVLk zt-4j4k1vJ!Kc?~c9Ss~gm!%w}_$WMm1^Hl38Sm4wbSea_Z3L{2?PlBPLbVxc8^H70 zes^lUtT-)lj+`s|(u5w01dQrRvU4b{`zWNBVB#P$#|p~JoFf&HmC&C^&bg&mg<6Pd zq{99YQaHOlU27cH)*;GpuFpz0Uq*K8Dy$G_pETeMQ|O%G%LL}uSfx_{y)Qq0F{HT} zp$rj6z@$bgA=z*@tiK;!``1RiqP;xYUN&+~>akJk^u9hBe7sly6LZfS75r})+b=KD zf=<$G#vm$O#(J4S@dz{Uxg7+!yF)sJGu1gyd~I1P5I_UWgTlYkSFMdJY{^4?_?&ujzA0fpcaol5Zf^F=`YU#j!ju0fM>}f zFiT{G*pkJJ^!R%J&y54ZXZB(IUv3ET^jlUTfJ}zR1Pgv|>9_ON? zJ|xI{5aU`-I12zEr+cpVV(~#x`W&RKmG;<(vE@0rr5_!~JqXX%_Ibv5@VWCbZ0R=1 z>I>pc_(z49yohpXnEc)%^wB2)Aiq5#QcJ6w5*K(_rWL;2sqL{ohc_H4^&Xu^-%nqL z-rx{YX;bZthD8U%d*b;c`tSPmnZ~KQqB2uvJgH_bntTwWID0g>#`-V#(XymFr0T zK2YXSeYM0ok;GDG`dZ-DxHid!)sKZl!3JdkG&44^kQDV@^o zg86d9Psn65^_vVx=6lsacN-bC{BhPIa@;aTMpKCLUtvX|m=W4};5))nipZId(huGp z6`q*Aeo#gdc6}9`N>#wyaRqUC6x)sR)~rT5CcO5=7ed}W4Ine(7hha@2*1hKir z4Rc_`lS10=9*swIA=-_yVHPE=j2vFE-6j7HF8f~xF&Mb8YeDa>-P)l9z3Gcjw@@QUEkT(IhFQ|#JypYge@7CK<++{%6l z!dEo zXNBbNyD##R-6)$pV4L!G5&_9bzyhV3B!nPd*z+k&Xa;o5p7h9_DU8<#N*58~(Qc>9 z@AX`6cB~btzGDi&gS-N6V2Flb_~tC4`cK0#JRE@R-j*Be(3J7oOrNWY!v2!&sAeJC z*ioC1W%vCY5`AOsQwQWP^Zgregggij)w)6#a-4d81c zCI^pNn;9Blm)an=3R0+-P2vmm5a-QDXVZFM5glb}PUHfCUSr35qo>a#$;(ZO0Bq0Y zsX=?K5;!Y*+`wyiCv^6$G^ylwP@V63`JLlV4b^F-XJCsvFu|awXny|`><;jy)JNVD zzDqMA`ghgwa4V#IT>x`%f2$=J(RZ5J=z1hH`$LVrm%Uxr<3OyUv>eOWGAeFt&nFir zzatzTII{Ct6T>#1vYu-WIH<0<=}ib^MBIc%in2yp^V)UE%|AyjBFG?Ew(vv_8&fquO%&HkRwS`oYL_|1vJmJwm61;5|>Ov*ZNRiV>{ z04$WpqC(E_b+cmChcO}}EaU^YC(8(C+N8{t5OJQ%Y2#e_5;B6E9|2Mw?&LMuo6ZRK ztl&Cux7d8CLQdfD@Q0kVM*~i?_w*e$+gF-gB9EmP)Rs7osBpME>05ef6zF0^Ob)-* zrr*sV@EJ(3coN^|9TR1-oOqzKKNG9D9!OQu?)!-P^*7k9>i}^FC55rSNznbM6xRlf z=Tk73i(mD`;+{>9@o;#T$F=KA8A7jEBoy2~fk5my@EMEv$TwcX9m;Hq&YB7_dF4g( za?i{dxE2GX=yYyq*dIAzZTA6htWiW#j*<~pr262|2+$5yZ#M_)Zb1_~`fS45yYZeg zS2D+n2k4~UZ=Pd)bo(P0#!MnE?s?nJTd67I^|)q>n$<^#F+V2^^fnh4%vD^*hRH?` zbdr^0KY0!?_-WkuLo$;!2L6xWDMfc{jSgQ zy06DDo)n~Wr2@RrrzS#PB-4|k?=h-P{#RqqB%540_QY>%R5@N|{9KMcO>)GBxum%- znVA)e$FrNA#y>lCmLaMz@2Tr5#Rs8Xj>ej&<)w#9Q*zjry<8f)dtCj^RqP)N&GuBO zIZm%Y<8@rHZf;+Ym)(lmC`K5=Z|gCRxpKnlL%OJ`Nc2A!2mYx&je`y&)j5N_ZwQvJ zV8L4MB5VS_X8H5BdnBO3`sV2kk{6cfC^yac8w7TAVcio}P7+f*QTg56SXOSNRW8!W zY)$~!(X0y4i(&|)T@hVz)4+3|kJ|$MJLZ^XE?=}=7Rs_bBI@8Or6lVdD!%>)%6*{1 z!>#l6Hp>;kO-7UEr?j^92-(oN>=1e}x3M2P{Y(n@#d-xPcGUuTWXwniFIJrd>+1Q= z6_vKzr=eF zzerpS(j-!?_SyT@Gw3n;Wuz))^I! z{mD~lf7jwh9rL=%WuZvLh3M{qBH1SI6GgY{>8Yvh_T>JPBSSSKH>`BD*mxnOsY^`jEn~%d=Iv5<@9M*saXDV(p<2{*f0btG z9Rr4VcvQdyo#)2v*Svz(%h~p-=bt8X)P@r#0mX8tkCnIZnBnV-R`sw3c^u9m9yIeF!W%wLEOkh7cq&Pqo;RO4DrP%9eh+F+HZ zG#LmY9Q(`&hlwBiX$Uz-s}>=LJ~X;fDK>yj($8-e5g}4X*Gua^UPwc5oicO>ziN$}SYUoMoLRnU-o%sPmta$d1x9uLx|`HL>w> zNn5BQrm`lTU|xM}Q#esD#|0d@^}~pm_j2K8c=iiKvYL)R(GVqZkRBWM794#TUlmg3LeS*FNl7^ zC>attAba@iqUIH7{xG6g9DOjRTTWBj=7NYpv$&Z5i|2DcZ{AhSFJ{KBx~`cq%a+Zh zVT@yP3{_)wEy~4P*i||e;l16$pb=40L8kelz7VJ`JCbO}K=4V^5dIHC158PjCVlaV z`cNSgUWi(ZYWa8rcj4nQ|4$Z6*puO==^7$zIkwA8(y$crJLg&TI}5lUvP)Of&OX)~61awI{! z{Bq75VS z4hWI9DI~X&kePzi;=-=dYX{8n49O@|$S_X7KUKdfpkJ)Zw%C}h(j3{;i*(NHoeiyI zQ(lqi5WA>rgw|c(Ee&Up^2Mn+pvFFj6J97{iyKs@SIK_LBAbd&fol5P$Z;+E%J}+r zE*4uB)$ezE=4LexVcZnm>+F%cEaC1?%~QSc!mi_{(_4_BFR?Bx)LHUXpIFqwsF;1* z34UTn>j0Jopv!*cjE0aE#rgcX0~xD2Qg(Dc(@~wlK_a& z>cH*v7oh2P6=8joUArd`&RcW(G_qu}{xxguRHy6et~;>2J5be(Buh!pQao6z$vZa& z_GoGenZ4|S*c5RAzI|TkEXa%*LYwcLMvK*X%l1h=DRsr?={n z;k`*y6M)`87o5D&gco{I^-$kBS;Hs-xcw5Bl}b;J6nh4s#~VGM`CGJx3?gBQ=(fq3 zwxgWePmF8@`r8(>Q=dnJ4&P+C6dP(u!Ee=V7f|oB7ALk3f#9624Rhw|3Ity0)wcv% z!S$6+!__O-47rUee7|Y8NajCJy&d^l?rw0ADa5B8Us z5O?qTFcO=ma@*5x`Pkn3lC}#HpD+EUp1fjDqa6wGN~ow8O0$+!5i+NISZb%TZG0^> zKULa1vtepl)S=P=7!=>vQ1VoNgfCdUr7(22bZCG$E2I{=x}S8a1k`s!SF&BNZDnp; z5X$cIPwkmL-1s82 z?ae!C>%3}udW2jYM0ebO(x6QzWmVs|PO&hR=}aLW>@7(H{hS0>Cs|jw=y^X-DBDmb zhFLsR$}i3e)HaJ09%=0iKKX&f<}Q;ys;QKeUfS7@kx9ctNN(7W+I#r70HTJjW2>{7 zUn^2iQ!$jZj@7D<;ac|dh&|``>uy^~LX}MK0n-ygLcx!t-SX78E2``?3XaswRpIC} zS3mhRNU{Q~GN9{C1cwfp2<+(irsLeF3b4IwN>M+*EDWLu?e6@^c7wZ)>K4BkHoeUf z2%fszo%Ef5cb*DzhZ7X%fi`2Wq5Ehzn!&BCXkL52@eEj(N=N-l$juCagmRs71)p!oG6+Zp@ebd+3=hf}VoGeW`mRj+TXxF=d}h z90WGNT->k9S!!5Xp`OoliAgo0d>ZYinudY>DW`26#qD9NG)TtJ1f8Ze+7zFqnBVWQ z>KeCXveJ_p>%D^FZ5(qntRjad91WRHXIYSXOg^k2Vn+VWl}U73 z5_W+_ab?;DlMpiK=@ zobE7|n;IJS(O?C9K|X?uL2qvY3+sL>tllR!UX$iJNlYcdXQZpZOy#QWG6$Vy!EJ;Blrm%i8TY9i}#4V;bY!qFcnoxC+W&)*KQ?WJ*&k3lLERP z2wbbc*WvX3>Fr7#*^AgHdpb83l4JU$sX%NA%e!ST z=d;7WQAWAT-iu8XjIT1o%g$N>>-_a-+h-NO!t&K1&MglTp$U&@vSlB;7Hb*658FKm(q4tDM3isfBB(JrTEy=jVL6mnX_9MZ}> zQodn-tQ{HQWjBusWvi~;+;@<*^6P2Q5D{#qjefl@;y_#f$y#=jv3c%Wwptde;{@2n z^wZ!v!+6t0P4cxxVuH7t2?z>BS>QYcQ9{{ZCo98 zlZ@U%Sk0qy?YFnk=01xXg)DkclsUOhfqQUbwNYx(()`HOW+948fO z4HwhpDCkNiixfH8%9;!EgVCc|S&5gW$c1X>QH4rL3$`k4RkPB>MKOQ4+(B6A71s*T z7>0m}p~1xQO5i}PO4FNIC3uM=`{Bz$%%^NOQt`g$a+6rwZiO+A%&|Sb1zZENAyb@J z(T$o4r<_?$Gxfxf&N5u)Kp_;4{e`bN};P% z>B<6l@t#!HAGc(ezSO!l5!Pol%%%uEK~;t~Ys(*_@t`Kq@`6lii~ue+ySVyJ`bTJR zz563ZklBCp1k}rxXQ5nX%o;Z`*fOs6;)qLhd0qYL^t&sZA05Ug)qbXG2d>^ai9iOJ zZ~B|K7BN)PH-mu=GKy$BB~NM~NVQXi`2V*;^FQwcg3us&8jR&hL9Ri-7KW9{1Au`L zaFv5l=d&ugUMm0Fb^d*cZh=VbYzIx&LQ|BOY6AO z6tu4InrU}jYKzRUVyTC?ewc^IN(O+pE-$C>+4>YlkI`n>?%!_OQ#HMXW^cTt$xcR2 z3()YSpgBS;VMbt^;2){eZg7G+Y4PrI>cvkefw>S?252|rY}eE5bzg~~h^*AxlB%vN%p=0U)XyTx7a#o7QAUQN zzgail1|gD&Q{bT-Kc3FgmqM?*m4lweA#oXXc6we{wSYb|?`Ik3cV5)Aw|M1#|mE>!|T7j3pm%vbi zMFf!MeO^vcKuc!7uVsONmz7+<%Bl?{@JZaxR1dLzg+0Rb)ND}tci);H1i6M*7*?;F zd`{T;jfhM`FwdB!m5qCe;@5g~f-mm`;FZvi&xlL>r7)%fFc!Kb!-8o1@@4bvu_q#i z^R6infz&yXkYSU)7AM{;IHfe? z2JAj=YDB7KoZ_841)r(~qZ%Oo2X8R5r3LK=p)mLQ#5leXWC9kyz|JKHfScQIbU_bA z*rM{XevjCW;+iIhfe^}jrl#26J!G|T^m!aWqga>4?FL*!Cs-VN={uni2zxAH3U6ui`Q( zu)$0IUrPZnKygkr8KyB%qjtNup?;9HZ##BrNYzNR|2}Ceg19T=GhHzh7e-ZtNP!OQ z`ANe6FjB6{W@d65f(TFfv&c1{w2}K&YV`((@q+-p06b(5z;g|GfPF@Mof^63>p$XZ zIW9M&5J~F5hk^oG$Ye`IU**w&|AC(o^UiT|@r-v6Cf7kQKJB|f31JGxEwWQlRT?a# zK$L)v?y>{qB#10pwZ#C>#jhFe-E4lnC<<$Q_Gp109~6unX#SX6D=(%bb+{r-R=rd5 zP;u)SdA(v$UpcNOA=SR&jI3*o>cOg$l2_KUEA$W`T&s7F)q>gc=IR7f|K14U$^SO? zproW`=EMSj-0|}lN_&%?*&i<&T^AxvS?;bcNvkiZ)HzgA{E@qoFHhAhL%o3@^B+$S zJ{L0r3gC8jm#8zH)shf<8WtuaDUUo>5#$h0nj}sDOo_ zC(kqKbZc-$*xftkJX_6fq~X#Tvs4UKnb zo$ga>atG!r1FaxRInS&7pnMUTc3z!{Bz-n^rKlf?jn4#EoL`omvI(^c`%32NF+i1G zpQJdGU-8=jx#D3bQWSY#;5Jq?6{>iRnfxbn^Z*5-E0K`I%+A&EzpOIpU|ZeaA%A#W zRJ=B|3PnqLO}V^Y{=z~eiSr~%u#&ae_LTuEN5JU}5U40t`w^G2Up$HlbNJ*68%WBN zf9qTqz2B_qt8kkeH?!#l47IefA4I?O-!xNyFflQ~MhZ5cTP7dxj~7q&y`Am^ZYH9W z+;ROj*0mysnM!TL3lB!9k=k%PN4(eh{QoVsFk`-?2>^ZE@Z-&;u0DFS+gaWr;qBXb zb5tkoq-pyFj%_WpS>wxd@?+hUgn+@(h_p~%Jax1g#!Sqw&ER&fSuM7MYVH;P3tf?+ z!Xh1bv7KGm{`U{|TcaO0O=we*(C7IJg%Ef7y`{-Ce?hLs4FyGZ4EkX++?(=q_K{ZB zYA}j->3&y~@WjjXx5i_`K_%^5gOTr}*N@;?prJx=uzF|USMSC=#+yGBRD zJnk}*^@u0yDd~x3UNuKCw-x3EAVG$ke_D>iPwBZg>V)W=Ko@$+f3*SjB@bg@RiV}UlM!Zvc`oP30h z-vd#d3%(8W4s2|`mYnRe! zzH9IURr-%+Z?@x=YZ~ngc~*rx$Fh6_!;Ll2TiOSDZ<}J0YnuzxgJ`Bu2zjfE$6x}; zbdfVV%WZNOa;yFo(JA`Chxc2$0E03O0TjR7z}?s?in=>U_Qn-=7>N@Gd?KV-nnAov*GZT-eQ8hUt zy3cwcH7Tc|l0%k~!g~C6TYpf&`b7XsbeJT$kvF$-+3_*IKhF;>uCFcFIW@IsGyb`C z_bF0V(;0+$#QGM2$N-ysmtb(*G@yA>Ti^i@T(NW9IMp~BD`UnR0sH{pKA7IV)RM=nFUfhmsAm9Sm zS71Up{#B*|IVB{omxSfz3{FLN}aFzf3b%SE3kzdTJXw{MDQgT{PEd8 zr_GJUsL%%j!-vaT|9@zvA=T-lbL~}#{HLN!e>e}3lH9Z@fii&-kj8atplJWM< zmuWvfcsu!<&=*=3j!%x`Kq~VR5X)AwT3WrK(*t zMCH@ds7V?bH0VW{?8I7ejMTHz{hZy^dm3lOLF7cz`dVANq^T^jy5Yf{a4X& zh-qS?Z;Y2;NeGc9Ap&!F?Z9=Z5Sm8jmBzWo8FiP%@jwq-h6bh$R-qe+ctvSP5?^UO ztHWccUJMf7FHXu6p0bL^m4pjuw0tiuR7bULU*8!N>(^4PBp{BNef! zZY2HVY+5rBH~E>#5*&VKdgkO7CJxGTci?F#^lUh_CycVUIPdPTYAA zdgy^`#;bbM(?RGA=L2Or))Cu{O3Gwe_LsfOK)ZLuBnm20J`^g0Oyfk|nRQXAdfqLf zY$fGMzmn>I`S<@6v-DhGgw2SnoFzua#&9JQO^$*%J{vk0>PPQY!2uJ+`tSsA^8ZEC zRs_rs5u>a&2hdtV#9vad!Ro3ZPwO&d4Y`a9+u%J*2Ap%Am#2w_9QtH=IgtLgs6ccutF zG~P*4Z4{>Vmjn4V7^{zSh2+lf8z&s6K%m5fA0G;HndL!vitaQEC(~IDJ7-YhBf`kd zl|IEo`H-PN_))I0aYOE6oDI6JS<#8A>YN=^Gc~Bkt)#qPJ*O5q!;q_tyg8-q()S~C zzvGD3)@;?hU{PbJ-8RA{E)gi!Sgsa2)@}mD?o#8z(u}m+BbFeJ~ zUEk8D$0=VUuE$C(Wz3meAyNh0ylVJ732+rrx5=09P6Vu{1LkS(k4wJ3p~WlX~%LBV;| zE^GdY_TH?meFx`f=3$}mXKmRx^_sFCPKxr<6MlcI=Q^|GVV7#C26C>VC%Ln zS1KIP)T_@tIYlEjG`dhGB0x6o)Za5V!*vp`Lf+lo)j-TEt0?Vo18v@6g?Foi@>|S7p)d44=76(5nL@8LBQwfdyGx_}s?hs5 zG)o$BL3cbO$E`0@5C6X~p#-bd=M#K9f_lAVBKpH7#2dRgv0egkXAI^O=}H6Y8wql9 z@b3c^k`R9;>KbZ55;W9cjwxtTZ^XP++H$eZ+9!?DTDgOxVq`ZJ z0IJQjJ}G;|6`L~soEjLBu#mBAAm5*cS7Vr|jgf=MlDM@?E&8d44OSU-?4 z-V$>A@*HHanp}5@JHR9T7q&3t+VK%w zt2MjDggx4hM~zbWHZ`+pHy~mR&-fB&Sh?)GA?$2#Ie%R0$onTt*nT}0)%XOhgbBH) zLlPTCDFnMyVI3uJfaI}vAq(+(5`Lx4U;ma|!^VWJQ$Mq9q8N*^Q5Uo13sCxhBZLLT zp|zsLS%LHYRwG%Z-Wbtb1;4 z`Wd*ee#qU^W61x0JJ?Uc#xVb@1bmFkRxd#Bar|yp)2^9NSYzKkE!5|SNnSS00l&?_ zSFejTg#5PX&nN!b@s!MeUzH49hI3xx;jNu|J1K6XY9yP!}^fFv0WgZz0wR%tFj}}p8(*8-ha)RKLaQ~K; z5r(a-O?d^ay}|6sWt-!YD7u1^3LPnc#G)$xX;(W;&~h@V_J`0Z!8I98~NYWZKlsKR1jY8Yc4NR2jnLk zHX|~ebLpjTXe>l5Kut}8z~re{*F`8;Q`AqG24Iqu_2)E|R8LYD2mf&6&H7bUO$D0$ z!`FQ+5FuguHgYK-(mP7Gx~ga>rz`s@lP?ROXh3*X2ch=`blpI=Vbw3|aGY{!ZCGPX zOi1-#)PEd5G+@y1={(n6?urWAb95*^4Ukm|B+@ZD&Z~`row=a(k_TdKh9vNKeGV6xJ(%NL8618FTTvt| z!hPnay7xA+ie*cDWb`LlbjD`YR-2A=ZVKzK)r^T)|Fu+8ol1JBYG{5OnQ+Io zihLm}o8~g%v!8uk?~+uzG!01=(R;;<=LZIzg?2J)ZZ*pCO>;_ekT4RsKB2RKs)2!BIKHUZgkC1)E~iy;}0a}jCTp;8V} ze*UGsU+`BCns2DrNbW>LO_mXBtfFrQ+nnCXo`5+k6dS zb~`xLo7{IABXF~Cc(kWe@=8id`uMop zO=r7mYHr!R7=~eFD^MFbbU;qr^qrAf2m9aO-x4GMbuy_f7wS>@abOR0!biAS!!ZQ zDpd6+!;E)EiV^*c%^3M8KgBQQms&v=QvSMxRDgDP9Iq}jTMx|Lts=2A@5$0AR zP8C^MR)(3}Kc9^W=E=r6#Lzh8jEI+T|KBWtv&a=Vj+T5>J^N5hEZjk-sPt|rpvKO# z%#Yiks%?F9=y`gs8LZTE4pQ^yDHNbO3P=>&PQ(lQ5)Kvg!mtOEF0tPCh6_R zQ+Zg#_xa9m*dyhRk>cW~j7YMSMq++D{>2(qf{2Lyb}K(gwG&%Xb>)uWf5lQ!b+IT+ zg2-(Ft!=R)w_`8ae$~h^+|JRJnX&dF#{y<|a@p9}iwjPt-uor2V_z)3WEy6e_ zsNk(gU$E{O>iM~!j=X%&Qt9OwP#Q({DoI_S{B=gxsd@-!|iXpp=!P*_zO3 zt(CgMLd9KUUA)F_ec=$r5)|g)^;n-e0t1k`Q>bWn-;7ACpJU{nYto zdI+fh;+$JDS4K9sf{=i=iE+2;L(G-gS4ERKt#c&?EMQR%h&GP<|E$smDWWy2g}b0A zz-XS1M&7aK9O%D#w`Nq9TBOL_EC6u>y+Y{S()`c$-SwH@I}RcCesKck<)*wdV5deZ zHz;&AGSq5o%xN7#DJz)&=Dd4PBtRh8m{P@I>+NY!hDt!Mct!PGK>nr})M8U^)Ztqp zT7oQs*^I(OUt@cCvNzAZSNn?bNk`T8>Ll~21iDNGA}1&LnRQ7Z=o7}dPNy-t$WIxk z4FsfbMS#&_+U#T>x-)GUd*%DiX4qv{PL?v7*<`hDVJ}TRI4c&IE;gj zFLs%Ikz?gWc*8; zs2;TN53~_gYSf08J&{miVPk|6==C=rV}b7%Qy+0ktpxSW!KComd@4H}7u+$#t{%_1 z?jRiZY3mqg9Fb4u% zw~D3GU{8ujGpGNq2fBY3F`0xApN=HFc=xem>Prypk~NYdax820&OT!1JImLow=aC0 z`!cs9m2p`U42}t!k}JzABBJ;-U~|X$pINnHT_o$%o(NS%Ao4BWxHm%9hgbp+Zo#@^ zZ|(R_Fp`f|PRunDXXwNl7|WHBlm(!L-}ZZ+JN=78WKdqF)~_Jbx!5M1cW5I_*^;iI zw;0ZHelkyMTWgTyDou8$TTl+ihg9>zHQ)*MX1^n3Xn)LT+Lt<5x~izZ@7h2-N=@C^ zR5taDk^h9dy83X$oH-EK!)Kn=v$R=-?NGmbm_waDia34UINka8dzHUH2dlbj@zUJw z>?g@XT7Ux{CX25h-Y#p$zT;*G+!K3VuV?b=yW_x9W}FQT_XRar@wvEuR!w*=-z_HXEMNmR9)j=KCGl0+S~@w7sZY%jn*6_w5E1$>=PPge7u%S zpY3cdvzb=qk(AA#`IC7lWCY02->JZhxcrBtiJnq5^Hm!lv)5oT&Dn+3rYIn4@P5$X z=j5TYTcxrm&y!Xk&eg~wA=$5o@vc z?o7|=MVYN`t`aG}2@OnjSFV)ZUb$nrDVeE$>`rI=uAhMcthX>GoJ>rD`H<1Lm~4Lq{kv0>@ERRG;qz+SUDpN!K6u(AR@$_WaP%bVVO&38HU5kX;#FEohjq zzn*-BzG%UhDH#rd5tbBg8VG%gC#>o~3XZJysZnlv5r9h>>A9JKut*gW;nV5^RprDC zrN8Cw(`1C*;?6f5N>)~Y93iRrK8h&}+6L*|d|U_)IDoEjb0R&?h@^1U4MUD*h@5V4)^xACU8*aVHDAd zmIT97u#BKvoRjpVE0q4#n)n^C9I*Z!j3bITtNU=2{>Vr=guroyk2rX>6`6zCGZfk- zHYTNq11MsO;d^dJ3?fZz_**9MB}r+B{9#q{B+J5y z*A@&}ubo^%j88-qnM07Cvr+Nyh5R>^%Mo_T4XjGf!x7 zu~i<>!i08#fg}tlTrPd2Ajn~GTxJ^#LE~}Ug8FOcrc}yhB1~fsa6frTc8}(Pg5}h@ z^m#g&u?nEuGMKl35B!kP`-Jvei#kic5{2XH)h`;NKkIBmq67JU#vb ztPzH3(jX@-<`BFO>XwEuQd_bvD4q=$Q+tN83E#PaHKOg2m}+JrK?%poaEw%#2)wepu5*$%XC!|n@2BC*Jy8N zvEIQ%lMxW*0Rj|eCLEnU$BuIKTNV6vywiI48qgHp`UUHU)vd$pzA(8z%Kk*RPHnEg zVznj4Ds1>?NgpAl) z@DCK;4n0db*9-n=b?Fk-J=zA8(XH$V7w`p-LqUnb#FP4dkF~sXd1>>bp-wl}%dIFY zLyMG@%(`NWa6+?UHTMcSBeWi?Q$I!56+`>M`h|Bb;5frB~2ngc&b}Czgu-sk8 z67B@_l}~}*XPB4+A%W<1Q)oHWAXc*!+d8Xe&YdjuK#4{k%gvHov6m7N^XoT3 z0SW4ta4Mjr6muL{W96p2zU-uF^>eXyJ6^|(hxNd&o_XFEkMT;~s#|Kk>C;Hs-1tMu z+;B)?O}m=R_tg$V+TIMI!2$8eA!(KxI5)L`){Vs-Rvd}C&hV!M zFsJI*`-6N@%I94lCX^J2!X5<*X<~3;R~6?$*xd(Pi08~iW{j0!A23uK3J?31zo7?#C-21?qT`(sP~8129-U&V6`H_bIJ-Ckv-7wCY3G|FGlLqCOWIe~j_6C%(&=*@}_U&f;+ z9W>g5>B#qW-7qwbXbz2}G5}@0I#;4rS$s=9#~WOfxlVhAlO=AX-ohtsD)pNC30E5? ztN)#gE(OGBH*2}$4@cO~)y*6MKnhVFtxqKekD%hmz(>XD-xal{hR~m4#0kMSq;nPY zurmR>QC+5*chdDc1h%Z7Opxb1o6&zeq>sBH*qa5(C*&1G=mlpRkdhm0a6awor)m?g zAl$pr!G2GWOi4klB-FJh$|w3*iK&}ic*nZboEZ?MA4%5n(hviyWOaJwMdY$^Z@C`G zlZGb=72@(&pDlw9rd&#`n=P{FPgqWE^W#;-B`zQuC~?aRYtD6}z4A2NCVqi%KfLei z=b>uG8_9V*wK}zykczWw4?;OzG3MD(0EijchMTtO6Ww_V+zG<8lJv!SNh>GTt3>4-~w`sWis z!IpKSH^+rC;?mo%MqShI|B(njB>X()O#(2Sbch2pJ7bI^$Y+wJ9q=HXMGFf5RrMbh zNc#}U+K$kc=o%sP>y@g@=fn<@`8B`ju^REnFGMp+ytm5KcKmok&RLL19e4yEK_n#q ztungZ2R9ZOE2982AvE0ZFh45QiU@D=1>&H>+0DZGqguhAvKl1;es1zUSK!@hcvH)Gy0S)VW=0AfuLuZh)3Y|CTxDi0k9a@=XJ=3d zjBp&;84mRcM)^jPs&6g3C*=yi9I8+M{?G|D5;m%S?66wrf&fn}Ex?d%tjPU)H_8&? z_t{wT{idpYMvAwG8b0t4V0FhWu(-#MocWnkPz!~E13uWC7Whk&G$uV2^M}5DENud@ zJ*w}wd-js!#i#$vsG&poi{30F?NWAK3h*Ns^N+i-D; z^aZcp^xFId*wa_9kuHeFYI}_DEA{GvJv#QcHw>M%C@?-)*z5HxTBoUmRp^Mpy4K;N zTE+>()dbGYUMK> zCfzNc)&V-#$uk4Sv=9V*V?wX4#3c_eFJbB&9J5*);9miNq2VEX9Yz~0IOL%p$e#&% zQ1R)Bz4=L~`x+_pY5he1LRbSq9x$6&5pwRf;u&Sf`$&HqEw_(LPf(ML$UTRGkSjCH z+xBwbbl`uKG$=j=`&blrbA0Sy-5iHB(E**qRGVI8D#HNt!nnCWI*#+tl`+>Ej?ea4`|edl%W zzurgvM3|6plt$en;@l5j73i}a+eobTv3rF$STG)?PFSviuI2y{-Q~Nx!E~R*)XRWH z+53QHZn|LRbx-Ws+uOhsbAp)Od%c-*YgRUy3(Mr%idOh7Ves*X~Z>0Q|75r#5s$fww0A zUvu(F1$1pU5FlD-x03m9#(>Hg$AXvU<`(->H6f7Zq&6sR_E(SvnSLlCj4hbB=ylMO zo9dJm&n!ld!iA`)69qU)Ka6E_u`I#44avAWzjf{)q!sFhk*KQ7g$I_R^ESRl8Nf$5a2zn z)x}c^mKz)?wk*WZ5*KcOD#4NgbFYxw zc*eRnO~XTXPQSpRtI^J7#K-@I`fj;Kz0{vb=Ld*~Z>}HDvx|10uEgc&lPIY6nx{Eg z>llyzJzoOo{D*CCMzW4ppO6S=4%_ix)A|(@b*5j%u!>W{-g5bvxW@0{G(=t~B|~sp zvlu<1K8X;Irenn$NsyiHeuu`lWE!G!ws-vYza*>x%8Z%R^pnOGc>GHD(6K12P^EA; z7VHIV&mPNJ^SFPn;w)%&1RoJf3T)O(Ws$IfoHlj6$);GSFETRp1D1Tct)&^f z%ZrsMmi-LmI^~NtVPDIWm`Zi+iG7Ivq+o3eeEe=XdM<=>twrm>L?W(pBajozqAv4Y zDK{Ga7WAyfO(N2eN1wZqL!U|4Y(9XyyP8Mo_Fnu#MRs^FL?;3H2KGUPn2cl-MT*k| z3<)59Dy^ax);lPf)m}Tj6Nb)y_GsYi6Q)yAQYicsX3?157YRXEv2(fo!ga9zrvh!}n}rL^P@8T`%doKILR-aT3s`K+^6o0trcCzRYpmKZ&7xH4a$b9qbj~ zFyy{)jAxZvU8p98g_n22IQ0x9KVzqJ=j1*-G*9FJ3kyk$Q7Z+tdc!XAhi}(Pcdo&J zQm}*A)tX!V$T!b1F)5+Xcc-m;a~Ni(7eJ|ZGDt|Mn2Z!(xBJuny(!rRVSY6c{?P?l zkj|xwn1t&Fgp?6ytkQi;GPBg$d4`FcHAv%XT{}5mxxINk1#}G@OV?QKwIm46UW0JH zgq|dG{pPurJ1%WndvNy#$Q{F>rMz$uQrTd_HkO`M`*Asja=Om z_mnly8yy+l;}b79b)?=IXxL(-o3>T8+t0o5p$#!39w6d8$VNWelJY`BiQ!sU z5lQQM53V*r4@fo$kd)Las_3?(?F>&>YtVk5U{@ra`QE00tiJ)DKxnj{sh#!svo7=b zYf98RZYbUhCD88|+S-#-Dwz-TATV7|Tlo9gV(oJaQ^~ny@%f+QMNNp`t$3*B1#6<_ zIc+a>1#nV-LHL35CMO<_fYmdbX+w+4^DS^SedJl4>RPaMju7?yaQbEj(-qTs&iSS? zCi)#hcEl^h$0!ln|Guo)iwYeqz<%Q)LhI=VJT7;n-2TKi6m47Z`TNeN@TsS}Vj;Zl z4tB@3ApZoO5)fWvp@#4^J$0qGQZO!?PCj}Z%e)HhBZ-Sx_Lj)OP_JeZ{Djw3G8X^Vy^t( zJdyCxXI6{@_=zkuPC$OuT3k2xi=FHt*(T5h4^4&}Wc^V)D0yOH6BcxhYh;Ytd$s-) zs_oDR^ydMV?=f{a(Icp++A_E&&ImVFQThx_YC`*0tBT&mj3+TFpBzU(QCaoX4F z!AguW4pR>8V0YFCNskYC3HwYWfcth)x66ZW-d)34k~+?GzdWP6zG*p3&4X zY!5h(2-?fFJt6#Kk3CN^{xW^NT72lm2E#gP_)3iZX^i?)6 z2AfRYT|3dK=ZghD1WX1pe!}dM3ix4>kVGUW0HBw}<_<0}P>6*f?lXo64nekn3ohP8 zXHZ$k?VQOMCBCfIMhu-;D)haU)6-+dxjqIt@{;w#Lkc_f9p~V7R}H;f)&f4nVP|>s zrJ$go{~#&4NWQ7_Z+A*}o43v}zml|N6K@dUl00BOE{@6bBb}**-gp`7W5RTM zFIJSdgCqYSxE00o{ph*29Q5YHdvklQ=l_ZY6M#6Veyr0_*pS_=jWljnuT8kH*yI6Q z9oG@Y?md&pAD-uBw*7b<8e}ZsNlx^R*&H&lCj97RBZ>?f*%x&29JLbkRH4b?@c=PF zXLN%Z^%ydM?!E*2{<(5G(k=A*lE);b`cX1#)=82N5&fGU^}HZ{&oQ5z6gNj7T?p^F zEXZD=y~Hc7W)hFxOrhM6T6q)75huQ+ueqo(*z-o)JgH;wI-Y z3`;Tz7_phT==-aagMbk5k=Q|-j?0)32YZ+{$kY~o?sFF@_cao_Z9#fNh=bZXj02{H zM@vB0{GlgZaIPOeOe+pQc21jrUr%RUpP)ZN_4|}QBz!$teYo2EQwDR?bD1r09qa4P zE(D8V-L|^`*}wqV;TiFVhBq@^Ad z?3*GB5T)N?&Uj$rbtRdO?_y(<(~=}YqnObX#G)ou*ZtDK;UT}G5?x2G3!u!})I+^} z7{=!MXf*}D&~Ad$*UtW+ZelDfIo|4c3KB0<7?Ml=iWEN0(EO8@?15n}_!C|@Z{e-< zGcK{jH!|VZT=UBpV5cpepMsddeZ-1k(%yQ+OeieR+bu1*E$`Z+4Rd?TjX57l?uLQK zFXz3czksEkR?K0;H5Niwq$yz%Ly2OC_-VxPdLlHz)R*o7wb2oLT$!E9Ay9)gO9Mt>&j%LF{byH<%OBhiXZgM$j8i1Eh9oS2eY8tg1bF7uiDJc!i8G zG(EQ^;^tdR3xl>1FxI@*+l#DB`=Yd5v2r{y&Mf^YC-2w2DhWabI8Sb!S!Cv5xlhCJ znvQAW057N8WMhx`{*GWUds3b^L@aLEU{@|2XafKM0CQu@exW()8Ln7;eMWzPBb{@# z2Wxx3NHifU;BgnP^UVR^DE2k`&BrrWJz<8J@3Ts>8(-}&2<(ZybNuXEh~abOQtJmWt3B{?{a zfAIOysSaK%OSh`Q({*v?D>-FJ>1AbI!Fr2HF4iD(L)}KU+ZpL)hHvGiU9vf9m>a<> z$e>k{>u&~*ap~yKtYf zSn+OabDZQH@%L)k`R=vC!#SwwMJ}Se==4A^$)zK}JK;E?ZQv?NN)J}-+e=eqy#K?Ti*DyS}E%dr(v#oty=y4Db;YjpOc;$GG z3uE!9z{N6Gd*^|`W+v7-ub_JA-`5oKwEP|2eM;O2d+oc>$qWT-R}S$D!me`BE0NJJ zluK^6a$=%Ubil4@&Z&X@q^>0V7-zI3{$Xa~6e|$=z4AO$z(xm*P1re+!RU^RtEC?i znE5=VHY6Ga2!?3TzCU;l9lygR9#m6>V)cyL_XuaMF+&9bk-Nzh7w~K2yR5{|)gi(dpstHaarMFcbrA#e zk04R+p;6gM^LejB{uR~0KQV<5dL1H@4*;v z3Hbh9gTP%JPz}W-RU#s{5j?tiYha-Hjy`ofe5rH8-0lk!fMrLxB=+No*IE=J8W2ws zba9)d7HMK8ua7YdPBoWojS33suR4?CM8U^QnEFK$K=us9ufA4q9+JOm!R3=hHBA~v zo1X?UuX&rsy~0XItc@X>Ra6$*_1HC?9~G2^2cUT={Vxzqa^DX85GG|`aqbvTgNl*@ zk~~h!{a9S?%CN$Mb(`sE=Vu5$e#&{`9KYRtu+)XO#sf!$6d`BF43AET^+j00%Zlxf zm{%E$K_M&BH(`XREQ&h^&8o=uc-yJWww&(v&4DTjP{06?%Nv;~h(+I~o-eeshK*x%P@ zOd_1!&?d5>5IxaEUJr`I+3hnS&2Ybw0XTYWFTeL274cHKA+ut!i|cxF-_f;8DmgJL zY&&4?7>V-09(^wi&RKK{$@*o-W1fab^<5AFcu!Ox!7{xP__5?!p!^wX-31Up-t5g@uD?75cCY3l_MQTC|(+ine}KSLY7bHo}}s;Y({M3 zGvodAV!%g=9QC)mn79QYs@-Qv$PaCv)sUWRu>LGulfQj6A-hqDNxJ&o{8436U=FGL zuM*0B=#_eub-={qYAnRW4Zi^6Sd=y1BTY4aE%wWcmWF!lu^hht<>ppCgJoZ^%yw=s zh!CpC_pCo}y5vGjidthU)QnZWZ2_%v30ZW9wzM7taE0@(noA0xfE;e{^|$SC!gaRc zKXipRK-1p>;LsUVW=5|%8kx;!rtw~-S;4<);8aFn`Ks8GWag`?1n-i4qZaVDc#P|G znI#VrNH)*5xCeSTqreyzOlS4DHV9|wS$Jg1sICj>KAw573MP9TiL)%MLIf`<^Px;(v9G$zKU`@k*Y zmIIbWW`u8E&VG+?WZdODNlgeEoCE;zu!u(@;&g$Jo;UokvT|)W?2v~%KLQDn=dQ1{ zfOk4)rO3bSCjV!;Fa7+nx`^F%56)@{T~I&2T=)A1mO!0y^uX^7#^LnfkjamG5Qcp# z2l!-1QkV{Pt+_bxS~MZU5d$SI3b<4jCkjZ2`L#q$*dXT;+Uig4F5!6!WQO;^yx1t2 zFYW1R0k9sEg%@U+E8lxeR1MSMu2|ltOuy0iZ|8qEYFWMGvWsh)z=U zQONlTBl-+(574YLAvci(l5+`9DIKB0IubYs1f$0)*-|=Lr!k6L37Iz!nha9=B=Bj9 zAvx44E06BzaD$_i93PEw?LJ6}hciO!bSb;T`cUr=pPjP99a8ORQz^{bYKmwuXvSXa z8%tLChF7z-u-R1b$(PCu5UQPQ>|WcHP_CW;y3Uy~gDG7sW9;Sxk*ixO5>PTxtUq7& zHwbI{uNpG%=SBnRCmFYY5G0izAB&W&58sXLyrPs^B(tPb`wD;zd-2pnq*^T?f4@pI zv@m%M;&9Da>?q!XJfGRV4RSg)i#VUF|J*6YGTWt8B`$;GqzbXZIW4t!roKHze2ZGm z6jI|?@FtAQsv)al4$e>9ta3WgFQz52_pw#L$ouV|XGY}6-H&tde!JN5^@8IoEAv*i z0ILac0OM)vcjMC4nPn^J{cmrcY{H_jkLLs`)SVU-I!jhwkF$-!$_T03lafz7~qIddnFtTfd;*;{?kjZ$OWdGO&;eV;1a&G=XVoN3a#WxOJkKC9g_o5 ztE?9z|5BTjg4u5k#NvlMqUU6VlnIZp&kUHw`hl_}xfgmza{eRkDRI6avkV=gFIJ|^ zI4+bs5Gj@xG`m1#1gDRV+RUT)c|($1>#pL5bh@03TrZ4E?6wkqR$@DE3*b+`EeiCI z2hG4y0cQ<1eIyIyhCc6<6sYlu`N){SjEFc7ATFWf)G1kr7^Sj~s|D%gUM!+j73-6P zqZe6uM^weSjihz46N_|!8_{QGk6GN@8ZtZ!9#1+zl!SvY4Yp!8erw5GASZ`%a9%cW z!Sfw?3e$K$M5}>x7*keQU&SzNfe9ALBcCuYU}qC;OV&IkRW&ItK2Kjj;pcgxNK>k* z(jP9AeCD(cCIP0dSzzF~HdEh7Kk4$VETW|-Mc@l{$sWj<6x(JQxBqbR_&HB*m+CQ5 zK!$p5R$?sQ{bk0aBqCk@^ZoULWJ;I9@Ft~x!y>JdKIz3navI9&*rPc?%y$7>ZYfzG zj#euf>Ox|&bAEFXM)g zuYoKq>eJrdg8?sj&fyK=+x%{&-C9@tzyupjZd0~H=PIXG8JRGW7M8EhB-$aJ%!})1 zZRRgqZ^<~nZO0MC&Q!qM8ovE!xKd^Ti~YVh`ZDezPY&C>G^#$H!+u@sL}AS zdpn_?=;FCMh6Jpg5G(~Xu?Ka?xzbt@a_&_9Xiih=C{^Yf9hi+duk$6_iHD zJL6>*tio69Asz#QU(JqIGs+upn65sB)oDCOU}9xH?j^?K2hZf` z$E`o|8N?6!a87IlHVeoDr?`ftR%e#@>g(sp8=n_CcKRSxh2c{?^$YS-$9>D3fVj(- zR_~7%4{hr@st~Yf+njahG#b)w`)6wFmF5sTE6Bd`=)+`b$ou^uRf0x>3;d`2gQZ^N z!C5xa4E_40*#7rP@movZc|WDAT8YSQ{Y(s}#2xJCvw$?<07>u+l+db1urkFf&uZF*vCTj1gb-R>iUlJ%GY z_Dt6zYE4Gg#boA{N&+EZ}d3woumm=Vzk8AMY32yAOWco=95Hdoa0@mZn%)L`6|P zAxd5qNvF9pw=+QN7b7;V+IySWac}@q?#&qqlMeE@j%t7soz=}5Zt?@twY1l9GGm~B z1(C|@chK|1MBj~#PX;%MBp$l_1QMZiCQdQe2VamtMl!{f%v|bmW3MOdi(lqE7i-Th z-sq&~nl`wTkpJ4!so215iPGDXC(6}H)$-L3Tbqx|L+@>!5`?4UXo&Im-a*$ z@A;Fn@XlC5CpXdI9Mbz{7U)7mxf??^4RQR3?!u(`Vf3jUl`{qPX~D9TO8GA8fAhsC zo;WU5Mct$orkv3Y0P1=hY8u|T9Ue^2(1sf$nM!_ z6rrwbA(?)MvS*$ls;D#2cdtx>>&~ybecRrYgmcOyirLEIOQ9h*U(sDVw`{146VD{* z1`xGlAP#xros5}qa((T=2EK58X5yB#9cBg8NJgg4Q*?+T5!!~{bt30;xp`#f9S{E) z9y4AWEG!W4U>$I1!kv$snstvQKBbEl;ThErjc%+tJ-IU^c_RZ((9<&k3LJl?3%b4P zG_}b`w7eviv-9Tv;7(h_I7V64tCd1NzS0DPgfN2%j=O_t=IU3vcBeV6iI~D*4V-dR zdGK*b&&v%{$y~ky)UKIh?>g)?xot*D@HsbOThs>$^RRE;kY2TxN)?Tj>#?qjxdmjQ zj~c?vAU~h^uHeLJId>S;)Rh(=eON1jj%lmPiT4MPoTix_Vbk%YTFIy_i+EPd>u$29 z@NdbVRDY7Ij*`r&Va0^LSc#xloDul+ScjSC+T3V0P>{M~M?>8Tdu8i_8~vC~-t>^X zvooQ8k+vf>AuN{LuM<@*kXStyxW#9_vCp>WR=*#3(&Rzg?1kQfScdLSns9bb2RZlpiIg>x+SODJpJcdgPRwU3 zmBOa1-!l}ikz|WhqP7I9B=hwID`k~8& z&olPSM9f^csncP?zdDcaY;tc8pZ>`9Er`ar2R)?jh{{HIlj)3llo_ldoIRzYt$B3RUSFP32DIUx^d5Uzks3bj?)*llU|a_ z+8x`q8j5%eqYav4jM*B*Mcc-PNeQBm$Fls)DQ*)lP08O2j_fL(Gp7VDG;^Cd9SP?F zV>C3PKbCi^({uWm9GA_vNs;e&^M}i%2IA0ze>%BR-aDjM^P)l_F*4pTJ5ePXly>(* z*=$V)Mc?!VNndon!O6JRs-v$skp1OJ$`m04S&L@?Xx{PnVIT0|=PfIiO!h**{cs%D zq0AL<_&MYG%dsHYn$D|WI#G(`lvMe|hWH1o2oPtQf!C>4V1{$Q7;$)$Ur}=439D(1 zN;9kmH^`(neQjh&V+on%F8HR~26egNwFK zcfUGwrNs*e9{82E&EJpv#mi3fH)>DBK=Ow zq;hpmNAL*RzRxv0(2VL!QzSh1V-?CX_3A0Jj?Zpr|>vm&sL3IR$tnJcK6YIDtLB1}n z3%v#Oyc8Rd^Z=2GfkhfKxD{I?M{h^K$*0>&ozNYZOYRq<*o>zG%CiV2k6-&8?=5?p{KT zEU`3iH506z|ANODQX57!MXSl^YOwO2Uj`0dH%O-rzERNg4>b<2$1))5ZRdtNWr@auCQKnceyLGZtH_7v#nre1b1I=Kthn z*UOqb<9WPb1K_gSJ?9rT{>KSe#t)H$WxXx3pf9`^B8+Fz#OP9q&7e5{Y9E4$RC?9` z*wfsXSh7T&nMB`9GQ5~AhdiP}MQFZXX4% zH{1TDF)EZXb{x$qW2>8(VH&8_tnn4>FkX(Q{lr zX(>62(;LvFq+H~7T#PT-)FZnMPmL0AsbM>|*a|b!MdJz1ob-s+!O*ACcE(8;ff5(`{dL51?z%LzkIC zdmgwTMy?QTEVI+?I%PUTN9H8+SK~$9jL2GwA8r|wWPg?91Y)3-kt++t6d52PZ8(da zRFZh#&N3rlqFIBy;|;$O9h-UxCpEVk_P5z-?!U&^6hWTgA1Z;##}Z|ya{wfgc)v$RgY0<{ixy^l2IVMTRc3_x z&WIr%+(2~^d4YXGCJWla9#)&-;Ze-wti~zA6Sf2jU!5+zAZqyo;%Ri5(JMl2r^2q3 zTkqkIs|vRodY50%D*4>kfMbZ%9D(SiPP~IQ4ZAt+6=tS_Y^P*}iX8+K=C!wf8sz7G zg2A=ra60jA1__%7excw*Kk5>`SAnBP>t7INUnQiOy6ycW#SucZ1{=_ZjuDNSx?tK- z3a4p~N#ysXZGNsx017#=a{&S>WMA0!4j#Y-B^O$Z!)162*Kl6U&{$(K%=ey*zBte#d50zdZEMT3E(y&@8te}U@ z>Ka((s#^c422KSBdyMHZ1+heJG;*kiTs=9R7oZg?)g()FI|N|ExM2K3?`(M z2lh+y7(06@>YYk8PaU$HPm8xmRni7D=nFP$%0KFUT)$)%)H}SfH_Xge^3H?EHEuBE zh1hQw$pXJ7ozV7kGn#XDUwum0+WL3J<*+G}1@Sjm1XvE3NSmY1sdCWX^38tjWZcme z36ec2y+3Ag{xdGq5GP#+vj;rkE!qbIb;4($+!OCLYnqQp=B6~rlHlsy zCIKtv%0FCBSWr;s^YI5EDD@gsaAUwaNDMKrUItCPiqj*yC|L2)o_H3P zrsYs75ZC8!dY5!gsuaKGpxup`ZUOm>v?)MypAPz{1Nr04ax;0^{hwJ@wO*fATzjEo#NZc-S=cZez52kIn&G^+Yv@5ObZoj zh45hw=WHkg4TFUN^~!$zhQH|?l06gi#ZU*pT!kw|UVx~9LUps>Y_66pX;JCStN9}WXNcJpc;$^i=ZG$`4wFauWyg}F} z7VM~=a;dLqb%;Bg+28VZ&-<;tU~`g6=X&tH$@e;b;Q)0>Xg-@Co{K-{r5{%Y*Ef)- zRw`cA+9ipSMmkGmw^U@%{D?O;w-huJg8J5WOtYr}tJzRYUP6Kvu`+KVe>3rIepGP= zfd;@cAV2{(5@(Pt;QL4%%;PIT>{Np;pk0oSKXSmZhnpZ!0d(3i-0F3@cyx6KBXS?$ z5_h<{^5z@=Zx(@e9}6sXEkr_M7y=j!{C} zB(of`P+jy3JeS@mu1k{glQU;E9e2@xRx^-0nmnu6H*)h@=m!QDjY+cv^a0$J1(>E& z>AxzlJ@k;i8r8Mja7{*-GUIYPmLU>OLE!)aXf$GPIi|5^ulShQ+Cjz0yKhv<|KsT_ zgW}MZEu0X71b6qr9fG?%1b26LcXxLiT!Xt4g1bv_87#O%us1p9-1nC%C~9Eu-MzZk zw~ijyhmrezW|Oozqf8E8q+}GsHF9Li2(_n&tqOeO&Ccb@7~`cM@S(6$ZZul*?_;+l zij`8_n!rRk^hhiS;QVnA&t*oFc!7X-+|m zd9g1E&r6NK_I&>I{RLI7q!k(JI`-7RCBn%w>Z7HwT{K&3D5#a%`F93+nt5yHJI%lq zWble;Dm}opza1+h{EyxJa9G=bk4o+fA>e^kfL>oeK?UZEA{n!~SJs#4gA zm*10HU}^2XICrn8f2iX*initoi9BBklloUC&PsFcA?(F*q0jx(X0v7j+w) zwxMV22Rpx$NHKvYP3IJ_fVD}YwaE=P`mXdF3SnW5DLO}ytg~7TxOm)7XC8lj)-mV3 zH>YF#CdE*@r9-y@2W(@&FAzSEo&^ds$n#A@8s>c&u}jR_1@GMW+txa1g}iV9GDVvJ zZGHM#2?3dlMN(c)lWG(sTtsz8lj>5sG4IV{)6;}mbDvPO@sr-~90Zn(jr82-H14ko zk94GeI#ystbOB_@=9m@QQ+peqe?1QLoE_~yy<9)h@cLeILiMBIf42^bJ~R=o!pmEC zhd!s95W}DL9mVTbQ-=20Uko0Ri5L!dv+1Hf64UexX@hmVFv$u9&wry8F1DnEx8i=V z1*O<(daPph}X=@!9B%vz*IyViMCJpD?YMaijn{#md$Kece=ZW(CSJh+@#}(hd9yv^ z3wVBhc9kv=F8BP7cf+x~J)-ayg!Gimy0p3m6z)qyDlGZ!s^jsTT`us>J9>m-g0T@k zJA#bSPpyKw6-mbULhdY#WN5@2Qg}ubxHy#aLxp@Ad178%5x;+%?1sY}b~BT58xQAV zWFRoQWi4@suFnzxMV~*B)M;`r18AnHnb@)9;F*Ns>|BABZIbj=Dbue>7wI zq5Vz>4;%1-Cjw`C3)Ba0g~m2dZoA)_-^%LDriaVa7eU&Hv`y{s7w?5Wh_&%8YXBD0 zSFRGC*=664i~={BA^@aYcH&kWtmWxv)RUAX&A6#jV=B!DD*9gKr!~ULg3imiR)PNY z8B3dfCbf$f$&3J?nnFdb0lBSH$D9-@l;frOJ3=VNgYmMlKUWcO{7R+}nG?T8RWw#x zT|b^xbrDkKjdg`i0Li<>)|h|Kbx7%t)=9+vmM_4e_o@4$v7bJoOyCx9W9}{Iyt=w} zRh0J*q0sgG*AcRlz4b~61%O6m$$w_Qkct$Tr1lvAdglw$5z2Uk2{+(eIxEzE8BA%D#x%5w91%zmv=DZGLXO z^b7oyQ=%SuNF-kT-Fpv|xayH0G8`_$(|^29cRXFdId9|C<0y{+MEXtFz`U4Bhg1e@>LyzjyL(&5UjL>t%%*@piFND#R%c_EI(^ zeNfT$>BPc<>9?A?-iGTqbriW>KRM|6YRoHEm)nc~9r&zS@dfgKa;91>DE z3aFJ_YuMls<8~k3Zp$85u>?j8z@1I-^xHOiUVjxfF5-6PMef(Y;J&K_&!wcN9Qdj~ zpn2`9eSe+_b8aZSadt_FH5<%ze+6p(s|B?#-!_!@`wI!F0EO!}tYL!@WMy*A%QHrQ zm`oh!PGTt{U~y35-GnOeWu?*O#BuN9ZLM>%44lR;%j4xG;qcxq8`;MWNBwqDb(0;A zdpX?ej>TRxK6LRms_?YVZ7_H7IT#gx^nrn%ITyTpId*YN9bfSZtux?PA#IJqm^3g-Nxa^RI08MNx!aEk-;B&8wJFU zcP^xENoqHeWywX!Z+GnKvbWFRRd9}|qw{D1j)%5WHX?G(?x@=$ zmHQrhoyqqsJeFqbgI|J2m|Y*E$;m?0n^Nr$UjpzoeLE)aJ7(j`+<(i}_iL>m>}h&C zW_5b$?tMlR8LC%WKR`2!;H9)}e@B{FZ+NRlda++|dM64?HzU}SqL8frg;sBOL87~;YTI;c%fie z_!^TzG)?PvRvP_LFyp19h4a6M?v2;)Y1l$uY_L6s)NO3Q&wCblkIs}>hkv`n+Zupd zzb8XlxBuPdyr#zc^td-NJ{m@~y2b6YB1?3@Ga*yk8yHwX@^Wdd&o&M>`xgIxZHmw5 znq-yLSz*xbiFz6Zbp$E4&5ev~w=m@$kjtRSGZuB3JI!y)-5URG_L)P#Gm)!E`>MO2 z@(Lbf&Qdn^4q)3Q;I`}r^58=rxBBk*)E0-Zu}$an7Js>(B8WsSygPOe(!nfgLC3+e zQdb$i?#w$>4$(oza5u@Sl0kQ1nVdp=KA~@zU>Y@TR~u;w0l(_Qg!?TJ02UAp`JR#q zP;`l$=T|J4C;-o#ZxOQG2;jz-{LnoyteVvUn5oe|oixQ0R)c)(G@xEO)`|UZYRU&8 zJfI~B=4yr4Cf|G{T{i61&#^a4`yizSC$+cb<%P#D#?sz73{6rJ`r`YNsEY16V;XVc zPUPW3pV#>5annCX+`e`zp+ysi!CB7##^95hy2)-o(Mmvd_RXUXf7JV*R>r;ew@&p- zfwu#cA_ic)>h>BSuCuLLfW(nk{VEaL?gU%d)|XL98MPge+vbYjIKrBN_#v`jhgo^+ zrkT&;unTX=Y!C1K`s0nZv9o~Gm9fWf5xn}suaiwetByNHNOMX#kHZ)Iw!nflp0KO- z_bVDVJNh44s+rEJ`yFLqBI?GsHm2 zJN@)t0+@kWJzv@}qDOP^a1l^{P5Tx95B{#On1{jmbDfpxPSTM^4}s({_unh&iG&!8 zNk1@b_D<;C7a()*H_vE%BId5l>s$GT_8al#GY@PPpn{0wBOh!H{vMNI%ItE0Oig=3 z%{oF}7`@~9dVL{bkBSCX&z`;}Jj$e%{JZIInT%rKTmQo2ZD#vsnk5V8bFfFm>h}9Oc;PDE?{J<1M%1PU%xH>^ z48Y$PZWzlo;c*uCPdB-})c7z>$Qy+2Q-p z1{S#!I+v}?Wwt)NU8&dZ$jacY0~~Sz@dAzyh{c`Bmld_(-=vYXeFO3q{y9eVv2tD3 z=gpk4D2uDQ#7^OC(`uE}ZDlTVYgHfudbwu6{Cvx~S}uDl6uW0;HdesvKwyH45zv}b zoA>;qXymr-a+;Co2N;r({GjQ?A04Ds^*$K)49D0NN0&6i59WWJGzUN4`gT^()b?{A zb{R{$SgsMSy83%<*@-j|nem^4eIOJH51q@kEFphfRh7(@{&~q@$%y0iu3^j|P(w|7 zMoFhlSYQeWaIjomMd|71O?>{?vk+3VPzc3{v&o6wtwzDWT<_mdP_eK~-zvar2i-}AqG*iw& zX$CH6j+`ys3r%H*tm7lS?KsDi_BlSiIt+!`ZAbGLY<|$-{uiYfC1^uNY*o1X;=|iv zPZW71pBzu$qV2+C%{Yd55Cqybk(WJ|M2e07vFr@x8~e;WhWKYlByV`2Z41B7ig`UfZ;x*Dzuu$TREMK1-x%NOSp@QU^E{|COQnLc z*8&*+S?3n4A|oC^yl`gmfWr<+>yCXYm@eq0_Q^Z3_uSCoA9L0Bc8vf%QM$M9THly{v1?Qw47G?A$5~slmxnb5c@0G)6 zg|BXX@OZaEG>N`s{0GN9Nh`f5bnI2SBA=s|A>l@x3P<>5*3Jc)0^H?u1l`0-Npi$C zk_P@%l@L6Z{Pd+~-vA@3EJOqs+89u~3c26tQOk)6s+(41WqOXlbUttd#LxG)jXd1D z+m08VWC5=5_A~W;6Wq^ru=t&$;;>KNDIrzmLH_T$?#3cQ3^dwnjsc(=bdrqT%fts9 zqeuOR-pFWOJ}TLpLjQqyq+}02r)P?!eHUC#r!N;94TRaz*Wb1lqqkm{kYc^GM?lu8 zB$ZW23pybtb1$o9e!-R20zJ25+KnY#oXzh1RjK3?2L006wTW3N1R!tXmC~28b?)ntfG_sui2mM-GWg3i$XB}aN zAxfRQz_%VMLaOc(H5iWyx4hYyV;?aXVKz>tAIInU6_rExK%RYej6p>w$qspiXTKa? z-BotAXoR!*i*4dit(GLe_vOxgW^+^pMH%w;_D+^)diw5?fD2LobxE;`W_tV2hRfbn zKGW8?`_8fR=3^V`3`blh!#{kc08->4n31B8a;`5oPKnv6mfXLLnyBHlZE4Es+**$v zS}9ILX?@X#hX2g?ys0jz^u*+Z5Q?J^8t5e<=WHq!p(cNkg6^KMb`3fEg4L4LaSsDw ztb+NqrKOOV0~*l{U%M)jf{FP1?()uLH(W45c*Ui)`0^9c&mD%Z?TULiCJ9OYTkvA? zCcb|v{_0vkoCF2uq(`==R>iM9*eEQf`BwZQ0@YK8I;+5vl9t=^#KnEFY$bkmjy*l< z`qSwE44P-(EMW*laMRUhVDT;6_(yhIV|Y?zwH-Yvei`LcA3F^>nImSJGhB;BzCJ`w zPc=Ks3#KtWMd4|R{1Vo)({@}Fb2IF_OPzsxqdovZj2bFa5}Yt^cm}W*=PqmPb&W&Tbz$vl)Aj}CJ9}$p_bH-$hK`K zl7IMb?iCOC%=l%w5xN8$nrpt}d5_D~GYG69Lyb;&_a<^BiV@I5{=CB^!Jle&#f|>G zd#i$8?3LI@oaCZ6{dN)$9PY+26^Erik=eehu{A*c1c=yQO*=LDfv7t7Dyz*TF4mx7 zCeyv|MLHgd&=L%J+ib-;;QU9{nU+Qp*Ie-`Ckr%CYpV&Z$jITGz>S%BD_=(MX=wEY zxAU2SO+m%NsFQxBT{Ev{ity|0?Jl96!2DiVy4rIuuW~4mgb3EB@>tU1umpe?eJMjb zi}Ftgm&OHQNPZPUqFP3qbQmP&BnrpO>)T^?Aj3e5Ab>U1JaH$xS3>wng;YKF1b{;S zfHO124R=vy`*4dt-IMsiit!$Iw=MRIN&PLq$bQ3u1YNH=mbwxI(Bpxo>;6D2hi8j# zFSE6wBl*h_1%Id4H|=E^%3|g&uY6#L%a>2IhvrGkGc+%(c`JdxZ*Mb1)wB7x*eXdeCwnRIh59z>f?aH|qotP(PMjCaJ;rWSkqkO?EnU)LH( zDCX_ zD(_v$~%+0X3Wc)&WgIP^(o)Gqwcr%zz08JckqChBCT zq;M_BwB^ao$+Vhf8W@0#Z%IwP(rL`&2FZc*8EDDLGa@RK z%Ov(g9$G{)i-_-C25lx&?5;Kxlt>i{=j%m;z8Jm?Uz0ZNv%WA5hYqbz)KAp>Y zziN~o>ma=%BX=Devps-Q|0{Q(W6{vnQAm#v=p3FDK$JJ*)wLTy?tVl zxBE6Bh2OIU6>qtkjVlit3c9~#z?o{sG#fe|UrgL9OR-|y>kBrO_q(q5ubqt8(gVzc za6gRjc!K^;B99-uhuw~s$UvXnE17Iwm)nfd3tcT0&cnBkuSId-`YI-%yfAb-Rp?r% zc0`&Z_X^D?ysE77;l~p>^G59DB`yoJgr@I2q!E^dqoxDhlO#&(Ay8n+FkDkH1qWxEQVCuH+t@@w+dXt%^)f$f3rb#WZtE4e!H{ z-H*#eRje8ozf?g3m)Xf87$BN6m}UYcu99hOru2uKCMTR3#AgKI;iFr|G;<9FdxQ@#wfSlBE+!&L zSyHVJ^}!M?od#kK#Y|;N1y4@_WzI`gvjWzS$MK-5J_-{;i>~uxvmNcEOmh81bNLJ? z0o>xK(v5b;vI0jFGWPE~W$TQwHDlFz_IrV2;ue_GuV>LHX9Z6jJwYfCT2&+NcKgmV zDukx>qzZ<17B~mAgfu8Ok{vPzC%2?g*@yamnFOAyrz4F@lXIa2I_{{WU#Y8A()}-n z1=y`HU;+3AXBQ&R=OS)uoZ&vSTM{9H50{>u!PLQCnZ4S-8P4Tc@{p(A-(HF2ZJp*; zLN7M!&zvrQEHM5lfDvVd5I!=y!jD2%q^d2sZV!dL#ba}&IK1{;RT1#7{L;&*DTR0H zQxwBcSm?P`i6M?bpy_sBR$kKT@J5D}o#Fon8*%Ph`?NXq(QkUF`Ef0;JH+4j&X&`o zC%XB51EVBN1K2}I;xo=NFQ;2sQHq)BvJ4LYkZMuLv)y$3wRDa#IN_^n>!i5mk$d#; zboFL?QqRtT_nbVnms$x281m(mNz#Hjkxb-Z4nq^6GtBdS#wPsLhMNs{W6`fWeUBf$ zon+8x;I3J(DOxl7=MSckg1|pz6bu=1albed=SQB}boK*_9<9V@adE9Vc$9p(coUDHsz+fm12OfK zwvvEM5xxH)DcP*fkXF|0+i7oqvlqhLxj$BQz8U|vg!4wT-Tzv2@FnX>N%P&Ip<~Pi z(4MR#{)B?p+{@bB)S#@ujeUknA|G0<7^_K8$E6hGTM?amg;<$q8+lAIeYx^2BZlyt zKk|nI?dUO7RB^D`TbsvPZpAI?RunH)SxIqWRXGfU_`Y9VZr+&4rURTr?e5}CCY#&TKei+UFpG}H=-Xdlc`~4~@R4)j^aIjF01lvE z7GhGcJiBh0USEFE^?4_&=tjLLNuH=Sk!;m@vGEY@<6up=r`to-_X2AL?4mmq)c|VU7_@3&9Q~vuMFHIz7F$g08j6m|ifGAgR;r5{@$uxFKoV$_qPEdR zD+t5lfj^mH~%G@!ahJ{CE z;filhVrA7)PiSz8oidv`5T=O_y^Twu>yW6SlIs*@k|>>i$jlT{FX#f%E)iwwpW|tB zfS6^}l&ha7(nR z(%3i3kWJn!{I@=@x)2r2xuueP-;>heNs>vJ=*l$0nA~iu2=AuV&%yeRFx#{-vbTD6 zt^xnq2aE>8=-H&v8YLu&+$~Ez)dKS!`@70pL@OO9dNP02qsgsU=>7wYYD3s-SNpEd=AyWbv@T6 zs9j4osoj*GkcZTOMBNg6=*OnB>j_En2F*IkYU6dyh^ej3%hXADmah{=oo)Hae|IIGmMF?C?&9)Dd4{dMo|#E-F_d z<1c)IKo5(m;It7ET}kkbQ-~T!7MuDwp63tC1$23N+vp91RzHs!^I{M!SzQgk9IPc| zS(?jxU6AK(vmX?;5nuKsfSDkg@+5#eq5JP5jryS^BpA`n;uZ#bY-9MfDQUX3#e9z* zQN-j+CKneo;=2JsY76v=;zg0sX;6JY3%OaA0vtvwq5|xpzF24tZI_{zU-VC{)VZdf z0;RG*E+F}X!FdYbUiDe-xQzCxlq9V$lC%3(s3Fv+M`$ZK`T4lt=1geZBmy|@TDWV$B{d#W`Q+09ugD~gXwjUHk}!sQ6f6z?#x zKxFY;aWY@v>N>jCIZr3_G|;hy0UPJroxj#*FTkG$@&-(?GRAA)86GpPPW0~<#C2um zDBuoCm#%hvZMWY@dDftvw#tb2Saf|-d*PW%VI%~1AOr=RE;MFC#2=52(0gG!ndQIq zKJX87@P&zCVQlbs!Iy_#>{WrV*5rjme;w@CYyB_W6xkF;5_K0% z=4&vO&Fd#u713odud<#WKF-A`|L%ioTWV8g!K;BqOyv;(b?!vcFRfw95l~dJ$Dc^> zIDRoJhAyT$VXC30)MYSSrs3#)bHOayls#sy|6qBZ4SR5R&c1Xkl1ur>X95A4#Fe*F zGwi4jTaPLq+fYZq6YaG9Tl}9YAOreiJ9``gGZU3DUxS)&NQet_Sf|3QQ9B}90r8N8 z_v~_0UY|Nv-w16@Xv+^kdlYu$?4b-MvmRSH)h2prKEyvO&Y(X zad7XMhjZiI{BNJLRfjAu)>mgO=Xy9TL}u!qiZa4^Bd z#nnG>c5gnr=WP>KDV4L1alP78{G$S$Ru4@lDAZ_XZ&iWGZhGV z@a$%OQM}3+Z6b~O2{OYw;r!!&|L(tG9-mJnIrG>9c?!cY&0&yq0VYg+JVn0PrKDaq z+rfR|NqCzt81$3I0&zW|k&$j4c6^4uW;Z%ahsf9% z-PW5^RV6B8tb7aEh0P+y}ZUK6AOl zZmN6&ppu6rj;+E(etC>%kHrIln#3^Y)94f$p@#Z)R$j92U=t5Z;L2*|CoL!>)#F0R zbmGAJJ}5BnH0OCA{$bRza&ZGtwVbH(%LoE0L>3krX60Ox&6Vt*yKvnSGRU|a=5yG@ zD|Cif(A6sSLvzYc-GG94V-QhZ-e!W8>)fg0=8?=m0Z^(;3rrP^``U!OkO@&zY_w`R|aYodo>5 zmEc&KVI#!h;eowe8y*!6P|%EDx4|mIW_d-Csyh_nS{qd7c(moDZD=@TGbrYNAAH>{ zGB#l;L>9oMfv2xLKV1*bxC{&vAD-%&%cA*rC6^8r0#$Wmc5$*GI&L<3rQ0=HQJ7K5 z71Oqn&(~L_NTpA1fAkEH&&WX)H(*MNnrW{Q2Ac#-5^&CuW*H^poAdwJjg@D9=!+1x ziWr8Kj5sz?eR|~o1GJH{K5&J($~u%#C>vM7tfdsEYzQ2PTd`1C8Aef7AA$<@q~e?a zhgtPO@WVF~iGzW(>y)ZT75a=q=FQz zbZ&kh!+~boqGg+xU~&}|#W)*V4@oHupV@lB{WA#ceCLJoU~DA!KmZF^%-LjYvR2b5 zG3YmKmY={VZb8A*mR9@K5R5_c0~{&rBe=Ue&L6yUzMu3$D1_Ck3&Dw-`*vkYWZ>WXSc$dgReyFrQktNg{6=%{C5MekZ97YX{hUZGRAx60{Ug9$ zkyqd5qng+H<0ki{7g<`*5Srf`0H=zG0|Uu#G;@W`j}j7B$yoaAp2+@tk;5?WE7d&oUsP53frm`x z$6>t;So=O-d`fCUY{17?G?zu6yn(~-W(f>KoLX7Zn@6XMMC|u7Ca?@2t+|PRq2M6o zJ|`Vs>gdu-fB8b(hv3vTD30Pvkbl;u;I{G|F_PghxpwsJ%NbcXYlsZ=1kDm+v-EYzT;l)<^$7arzyc%yX`@$ zXs}wLxqy}2zxB5wBzCqWw9N65o=UVjC|CMs~fg~8uM^|-kx zV*Aamfu__}=T?t&F+X?@}q-Z2W>MWeTapM`> zZiz(nB2u9EH9z1vgQXXVa(8Nfwa zbg&XV$yeMA4mu263l1m_k0|Q47gk!L-(&>wn=wD+pEl=-F8wa^@V{nNxD|p@dHuk~ z=$}SS<@MN^9;l+gngF)KSZcYZl|zs73hS0{~t*YJ?fE zVm!iwy=|F!4W<9Ok!nJK-TnJ`^er0XV3Uk*t)E=E}Z?2ZygWoq*X{n3%AlwF0N{$ zJ8TNEGW$L8p>QAm)eg;?y4aLBd1xNqbR`re^0;<|GOY39Y$1pX5v!~I9J;YPL#>Lu zK7(PD7=+dg-Z`@FRP3@s-TO#L_?6@a94ZG69m|tRe)f|+J7jOS=#Bn9Ic>RvNeab( zYrw!zaRVn7lXD{28i>+R4dWaxJKyZx%f9Dyon=XeGE9LG&^jMB5@NyYf;toTLdX*9 z`*^~}C|4&=RU;O-p*gM5AuqHi@X_r@6&3{6S50MNMn9ZyhU#j!PGoO_K)Lk za2Y#c!_^ai(N&a(mgtBPYeI%+H~@khI0u5Oun%=y<%Loc@P|iT^1I5{sDuK}n4Z5h zHB{AFczM1`H4P`HqzOfp1J6{Dy{_*KSkOV}P_W<54o_-~^e&w7FUU7BV-VT1UeRwK zk}}SVqI^@L1d(z^O(*)aOeC#HY)i3N`>mAaUZ&i33Lw_1~*)~T2a()1v3n{)MTOX97*XAyMpjlSra^{dhaQ#IKrCDF1j*ZG(Yb#*~yO@C`al(%U3YH1= z^%_75CdO47N91Z4RAq?*^P@3-CeHEW#5@rjSMi~?S{e9x|1l@{!%AgV9>9ZkNeKpo zzM%-ne{I<=X*^V1T#cK41$|zr#yr(i40^E9iN&vl&i-;^U_2B3Ki>g3l+CV@sV?Ew zV^(|6RFRcDKYh?{N&J7s8#)A`n-0eQenB+kUc^@Kx9r)wO(@9S-70ZiOcGj3skUFq zPL|JI(LOpe$j%=ti(dgH;KuQVkSSzeQXgR`>z<5h&QOwT)c6$nI$3QN6ZNxj%b>Om zg?d?Rlx`<$@c1l?m)7y=`H)7yi7~W9gy)yLkFo|uXHQ>+wYD2jc)~le(L$dm-V!W@ z!=TC&?vdCt6pAzsT@>M^&sB&XG8N_G?{Of&0-bP>*4bz}`l(uY`krN7lRp_%YB<_h1Neftfgo_c3s05L8 zSa2$jdGcW$hJM$Fv#a3d1$Q1UN#oQbW;4k;&mjSrhY_=VUSFBJMT6G+Go*he>ip!c z2HiC}{KoGpp+ET<^`k*3KL+Hy1T7HoZ)VQ!dZ-q-8e=BJm3ZL|R19u9Jtx8zRP*|y zI(laMsgNC<$wmqj!RL;8Nflv$V3;?SO_ysI~nt#Koh?wuz%UOdP zuBh10?IR|3md)q-q|kxvBH*h#4IRSLGV+o_hP#z~u`0tl=H7p3onijVe^sU^8U$h` z%~DFbaaS?6+p2?gBmrayT_ZLstq}&FC3<`0rqe+TAzE5OEUdOW94x)Gj^Y-sQc`Zx z+on}g;T_E5YG_a?fmPh`3zo1TdTV01PJM(teyfp)6vbR)SDogbF?2DHjxq@~Js8!~ z=OV`X02gVgSQM?anoy#AhpMDHa=0zY0r4i^$QY6`1lVE{^|VfD0SLAC3LC!y>efoH z1D<6WE@#@1&?aCR+RG`i9f(1fx1=CvpRNsLZo{7`#g)iq^X6H`I&ml>f(jLJ2o+%G zn<83trH6DZ9b}Uw+NgnZmz*}viBH9wPF{kpBj(azLe~yAtlkxioFA+7Ud5lJ+gPgm z{wNUj;MGFcl`vg)OIZ9>1*0-v`c~=3J}-TD=G;3&m!AZVEK@?IGT!h`J6EuZrqIt! z$LZVg`F&dndwa&~93p1#8LyJu>Fq*c`c%QpM^Nji#xEyT(M<)IXj1Ba#kdZ4# z+Jo%@?JltpTMN#A*12n7bci65-Zg^~QRmg1KXUb8^<)I#a(hVDh31Mjh2O9D02^f~ z9W$1D5O3LfOr}uOhr|J;HSG5rh#exAg>{#y^Yl~@%yELEB=iq__D(D`W73`iStbr^ zFiA;|bR6F$kqt$Z(ewm7V4k?zo9^AR_C>{Vg?~y*m1_nZtBVmsp(NaJQuiU2@qN}L zY#sf?H}E4xA6PT=6vv=rGG#G}m3t^Rbbf7=VpeP@VM(I4Z`M~+d4#>br)B!Lw5Wsu znRO}PKfVU;%;x<^wRfAS#0SezD9T-GFen*Z@{~`tPJ$C^7P?<@xxm615&;}AyIrb)S zHaq;lr~p!0h6BZT?0yLtDyffb{t`>+68?&3l>Ei=3BN4$91`}((Jv31h*^3IXTaKH zeBCDm4B9AfT;^YXxMXSA7!Tb`?-3XOId3+Fr6*iNpRU?}PhFUaeYp~uFg4VZ)fNJ_ zVc1DpDH0|pU|#EvIN_APA$@0KTsEs89w7AzGHFEiVv!9KpEQ>&jb%o9RB;H<*oR}L z$A+z-I}ADA<^XBskkpcMn()rp)2#*t0vLd$8yEGDh5<50=*^U(L&Ju*Hv8ndh`a z1D80Mg`HM+=vg1CK{>fQ`O!QYavLe6e>Hbs^1#4i7D*|JW0P~XM!fz*!OqDk?`gm$ zuMf5ee_s7D<}Nn}4|p_CB{7!dBMw9e@l%IPM{+6H+Sz~h_)d|qk@uo0jfE87uj zRe-}+VVFLLGmbZYZJk|^YZV!r&4t)_aLzkTx>s`zw;$&*|9Pmu8YXDpD^jf z?02h9WfA19)35ne`F|Et&;xnm0=~(9TxD2$g5Wl%Tq3BQt+uTz>V$@l&lN13Z~sg7 zzk)^c1jLj%4uSvc$BP%(@Zpjp-|CrjDr$uNfS*P^b%z+ZCABRY%yT3nST+6erCJqn zm~$}-lMp_KF|p|N=SkMWwu@iSSh?ffl_tb;Xp6E@7Tb2EqT~r{&7eN9I7*e!{k6D_ zqZC~uXp@95HgR-L^7Q5yu!~7I5UA4wa06`+l|dH^!zCB$x|~{mIbtzWf4th$Zr&U` z(=4z%Q_^ncreKzT&n?T4IWH|iVqW^-mUZYlg~dBZ_=I7rAe%#ogC5(u+GNvx`K2(j zAtPWE=~+jf3@H`q5BpC^4@**qu6O# z-M~*0=H{Oo_T#=L<$DAX+R*kaVG-xH9h zjf=N10xcv2ryLc<#vSwrznoIj4)HXB_*IqzNPv6&_J< z>cZSsp4IHKyMMD&q)3=f;qqJULxh4N;o)5vBHvatHicH5QOCU%PHO&b@Q|BfB9a>7 zpLXJa{ITBd=Q^nO>o}GbD;<6Wb4nKO-$P+UBdR%*-=Ak9lQ~sYV|_c_4PPt0Yfebi zVU8S=7L5BR!&Z=;L`J?UYU2&E4K#2mmG6>kVixY-a*jeNhRH)(5KUU5ktjaQRc5#i zqC{JfOp1QAB6%{9v`DsAhdHRrQoBBg>i+62+7Ia>Dkalw6OzIB(lUD@@6G9YnxzG?KnljziuXIrPDoM~ys38tfwNz}{X4 zpL1+sucX6yCjXQCh9*MeCax9=k8Kk_Ckdy-QA&ojG-tt-Iz*&0=VywC=LJXF${S7* z7M*T{n?;2(2_guzrYNFwUPGrio=j&=$?ZYL9b)|YLwRbSbR5tK{%_8oJ5)nqSxg%K z6FT1Etns)dy|l)`jp%C)b8LY?n$QZDKtRd;TYb={8l&z zBEe5&XNj_sQruKmHF2nPMy8exVJmAeJU(zmNYE}DmYw!oOVbLf;wj&M0Tj;f@J6av z-68n>+^&rPmQ!mhX!k24aHU}il-k0IJUHg8mxpL~*dzuEqJhGDOOZ!$OG9n6U9H%VP%Uw^oBbhxb< z&bG#u`9g2J_zktT4Ah^?zF$PnB$!sjQDbb8AvO|q;MIpHRu{^`7T2Xk8DDB<>FF#X z50u$?%0p7j`^`%0ximE`(PuI(kEtf6`_t}D>RXJ+FA$_9=#PU%@LEAAmz)G1HuG8{ zlD*FLu$0+aiW^IT`Z-ktkX!DRT0GxU5~C(nB$hgj!*f-Rodxq(rz~5O8KIQKC zfi*zG1&({e{V!*|U3=Tp^+M$Nhabc!{{w%4r~Od@R9m9dJ{MC2F>QNiYCLIn7<1)% z{M56N-D@pjWUE*<`tuYSkZHM}G9DgVFi?M@p#jpQ7^1VduW^9l*O?)W$de>m1PbyY zQirgp%)&m&vBqqqzmJ}*O{#_}W-whJ9xZxFW`@N^`NUatonD;qgax%FIknI5B?ZUJ zLfLm3{EA@Arwo3j7#JkybOT9?_%S-KBce399T*emRa|I)alG@->eqw`>S*kb8INZ+ zIIYI;s(au=mq+{+LYHqp_$L!bAxDB^OZ@vlpk|io&ho;2PPR~jc-V7@Rrc^ve!@Xs z_I||APU|=Mxh)6;v1*VY?yqy|vBh7dx{*JHE{ixqjc6ne=gjgj<@nA~{R!ueFM9qN zpK-$*9d*9P%h38GwWn`oV)$ldF&~J%(yBF>AuPBFeg3OCdTApRY^4`+;FXj)_gi55 zn&tkPB;OoMQTbT{$6(}x1kcShL#hjZ<5~syQZES!=n5|ekk7ewLOiS0{ur#$TUXVK z(%XUo?_g&~)W#J_cVJ?0Woir>PDi1lmzDk8uj|L$3;Zo(DPW}ymhFh|QLqUSnb^iS zbL302_nI}wcS-XhZ+CZVwf}cwf5Jqd8r^g>%d~}#*IFjcYu|rgaEFFaB zuIeP|3;8hg6GVj;WM`BE$#HXmdvfVm*(%mYP#oG;kC11_CJ&PQ3WvjhIa(Bq(3EHz zPA?@u(zj=cV<#uc3<4rA8j;z-SS76#Mj_HF4brry+>kq)8tv0F6Js*qwGd<7S%);m zWxf_;fXdH(|HlG|g@E*^nGxSKTqq(>|4laRz9QW_m@q#?9)S4KsQTiB-`XZ&UnAA0 z%aIpqe+9FoH+t-(_}0}dw-AV^y%LTwpRQIz1{Ufy8lT(pa-4)B3%ztZY3_@4ik*)V6b@h~t zph(i8Er&N&HxXN9g7|B?fbj5$b1lzx^=EpDN-=Y!P#d#e5D)U3e-)B^{^9yn z#~eJ%qlOTss-2i(>R)-}{G!}_go`_LQ&xE;gJ_@gy{B~Bn+Ds;bE#lgEZpQNUw16+lIs5-$|!JvCo-Oq z8>jBCeV0_z+cGgRVr=+bQB#aX8a?#=L%imjLN%%|b_P(b7+o9JiiLs?pO~bnq9$Hy z)2Jt(O@!H)+~jW9Z4to}HmsGuKmE%KI_fu@-q7{N#LMW$nQM(H7NE7->I8Q;Z9+!J z7}dKdHD7ncZv_||k_y#CZUP*FUQ7rlABod0vYW9hyj@4UOLv_8-h{cnL_2s_!hVNg zW@A3akf!qD{fy*&u}GTVT8dA2y9(zhyz=CNqKhvmCRh*3_hLajTc_!R)uWTPx;o^d zk$EZgK+#(Uz~M%4VA>}$?BnX6FsZNCI!S8xoWW#E;q%lZSLWBcCurd1q#7A&2v(E? zLGjvQ&;A`_A^~&{|e){fsjzt z0`10V;t)dX-3{>cub(`2Cb+sh?(tRU_&C2QhgX?k5o$D|KB%RTL`GR?QX~pd{yX0S zGn(K7Vfh(j;}w_|BeBTET$bilpgEwnAtv}54{blP=YLoEYvK6|BSsI@gt-i)`XPBF zc+e#G`Q_8dbrj?U2UM`W&{hP!7>1Y0-#? zUmay}*I*3{{_hQ|$-2OscMy@y#$T8aoAfu5I2qkQus_!Yd~TFvazq@xp&4lUKc2oZ zx~^zzJGL4%w$UVwZQFLzB#mv`Y;4=MZQFKoV)Z+{_rBlQ|IZj_uf5iq^O+A66=`38 zesAWowt+Z&Y2s3vM9B_(+G?zppYDLqYYOcA3jINkg?H6wa3ScV#&4_xO47V-e+N5x z)Pa3Q29CBFoXIaEL5uAT+s@5J@_J6*D|$~MT`NTg^d`KNTxu$d@c{Pn1K> zj-Gp-Y+fgXf5TMxn3Z^ult3T~7r4`H%dX6D=Og=5i{2z<$}k@hXhiF;Et1=_6AR3a zz-mnKYg{DLpj(f1jEvj9-v@?z{tRjd`|qj%hcd$`Sf*z4c?XoU?2LXLE>_}Pjsq6q zJ`l}S`ms}%%UJK$O=6KDpX4T)(JpmATkTc3!ukj#%PN=*?_@in?l4dbZgK1xk2Dom2n!;eFoH##e))Y*R+ zHhiSt)UV*U*?1Dc3gFxt3psc-p2k9?noj?1xW%r!8p)D|Ce_h{Qq9h%?&?1y>A7IP zxUsWpGG>XXapr})#cg07$z|VneIGP_P9G>rTU+tVeFgTeOovbHQ)YWrlZEhhyZrf%J!?kB~oIAUKECa``=$Llm?rO~u- zxqW+|Q4W8@*CNjuv^#-K98gbCl2_`Y`Q4{d4yp`y&Ca%y50R9)X#YE&2VDKrArMBv zlyGd|hctz0rSO>UUu61|z+o4k!kYAv!-mNv?eLI5U+biDR4?KYLmY52)J=DtZ9^EF zp9So#_F11EC~>Mr#Yg0E2jv@B@HM1*$te;iHyo1Z?gUWwa&0!}SA zV^vV+G%fl1Ee#^-BGpB|5_nL7J2S)&f=};)lO=naUa&k@URBM3W5RQYY+<-?chCFX z?s*ztahaql2KbspP-+#AGOe7Aj1398f$FWj8A4Bxzn|aV?}JHUK0O2O)uiG&m^uf(rko0JlcskmFRVV27sw+nU-I;b)M~D>|7laS;v-2w2rDWE^UYGg*ygB zKidv7Vd#*a`e;gR)^lN<2*EX%tn-5)(9$`t=S~&rooV`G;jl>3phi?`58SR5o+`6A zeKy-_)IzfbQ*`$i3X0H-k#we35-0!FM%k{uG0Yj0KC}m}4=iEq*xt1=);ZTS(x@9v zDSQ8fS9gAOj2Y#x7v}nyjI!V#%=nHuM0Ku!aDTO8b0Qqj{zbk0S`x?gZ zUuVH9bJDn9wq4E;Gx(h0BWzVeq`2y&i8mFR1mAUHf=vBm*_8hCiZ28OHm@ltUc{vr ziE??36&q+9N&Ytjnt%lz`=k@5`*S&oaJgO&X|Y{ofz1cZmSvB{1I9M76L{W&n#eWq zMHPJ(I(M-`h-m)K`o&~vysbV4o0ZuT%IO78RxHlMwd&{3hC2U|^fABnJzLmsFc(Ex z2*RV@wLd|CBt3boc~GXBA&q~^I?dg(X{jiM4@`M^OXXI&j!Ow~jh*va+}vxhG(VI( z_U^?}T>EI{8{S#KRvM`79v?>n_TiyS=++-xH4O&uTFlB|L&%3$NNwN7>QxBgTdrSq zZDn3QFxQjxK(ry?t5}_hZ=L%O?Rp5%;o!(#({C^dt{_(riOjq4nz?G_UbHuB5bqJM zb8L)idWQM6AlQ$<*x*RVboC^#6<5-p3l9%JTZdQlLx5G|5-?P=IC&t~Cr6xxv1vC=INIr&4v@bv9Ot+}0Si&>MZ! z`@6Y2r=aP%MY`h%(}`2qtCBK@FGW`T07O1j0h$C1MRE0h{hJWFbqDwQ1?foa6tSQH z-eg_sK;|s>H#h{tpr^ZkZ6GHBQo=3r;$3S)?^xzg{;xl%@}^A-%KgSaEyoU-*buc( z`FT>vcSQmulT8gzqBR^xw9(u@iu$s;zze{?Gp&waY-atN=$7^&Pu%;>i7{Y;-K!WHk7MBihTA!Rux%0_M`u^Oh>6WEEd*wF^F2I00y!5% z?6mMlfAObU~(nYV6HHroHCyxf&lK$B}uIBt_zy7r&KsFy94$vS)UqG!G9!QLyTT&1O znX7e8@{0-S%5?@FN&hXEiczr4DCZ>CkKK6*tO{kHjS^;y7b1dQAPQj8KvxzOB$`kt z5Ns(JhKwetsxT}T>+bQ2{4mv6CvSMw6GRq;COIIj^2Hv#}CTiQiDOp z1Un+xwkj+j_1(@m5EKxvE;y%nj?LUYqV8YNif~om&Tyle90Is=`cNhbWhZRt)k%m> z0IfZtZctGR{wP&0zgrzhGM>7`DvZ2lg5!O5db#|_{!F zLyU~PF>N}desu#1iG)rqy@se@tX1+Os;Wxif)Qk7w1q_t`A;egv(E$*Bf0L~3m)fR zQ<%XJa7p}e@>N@2O+bQH9i^}DnV>`m*(tHfyP4b2F;S&DseYK%9$R%l#6yU!m)!R; zqePnIXJ+85{KcU#Ywu0AxCteh5-kx(O67AEEoXgo%GDrG!DA3&dvmlw;CB!cUTpS>_l}&l z-G~x-VxOO$@*9A0&ehpn^#7C#t`LWKlarGoE-uZmZ3_lK%!97N^Ma-Y%E+(S@d>4wcr^Qu8Hh9D6yvE^}Wz z61ODe53AqEY>_-)>a@Ai3W%vycQN07d~8AMb3C2q4BFo;gX(;`|{20&WX} zb&~du>%zfSk<*vAu&@xs%^BG!YfiT3 z&shV`yd!#LAP#vZXJ*6=4TOR`kIgn@;RYmr})jZ1MXp# z!L?#K8T?z!ZCj|d;=^QX&L>Du39mb2ByBur6qkb9VvU+=(uM|b9iCyi{_Y=(>uta zOZ+mErXzu{lKyd+P~qi)jsLrE)c|+%gBNv>`SV!=hwwM|v_ZkBJ9&wE*a(7wt7(5$ zg9S~ACtXvRGce2XX6W;j@2~I!d1ut&rLF|EJ3CLBjImIL$AH3!Mq*=o%D2{azyKUn z@AX5g-Q6J*#K_rXQgXx!7E6Z6{<<{AqYi`<2MCSK@8{>|f~F=6vsXD%h{l0E#);8; z^^NPJ^|MrxS}&Bi=bK^*)G;SgX=8=Qr>hM`B_$zK{eFy-NRFhj-CE5%paNn_VPQMm z*Yki#ucB=D^+^wpVPkr-puNpvZA*4Eqfgb9lN>4q;OkFV&1(=*C%G zE9rmfq#~?f-SI?U2PJ0l=gvm&$2wDg@kBkPVNa>nv9&P%{|WE|A|T4drjTow$GT12 z!gWOm!`9iFzS&x%XIE`gEfNE%oTN}$f-9~t3|~V_3hwHa$5GXFu2}NZ<2~4mFC0iJ zC!%Bq2Q@Ow!KBucnYcw+Iy|fFS+{0bubxNL7c!WSV@k+Sx3WB_f}Nb{h~dneSPj1+ zI%5S!c}He?qOv8-s)_%+l3Umv-xiqfpWw;lVJ|t_NB?Q(lpD_07pS6r5>F;gj$FGa z(I>roHfk**fIcT|T>`}5ix6UoiSv(M>L`n`N!GK*-lGQwF8Bkf-1NgxWD109{a1br<0SL9nJS^w; z+{qH?8%=p*CPSpamd9JFI(#896WBV>-t&ZQx2sFod?kRQRK#gLfk>XOlCx$YdB^*U zen-5+TT`5WT1$(;u%Woh_G(Jg`vI%VodoJNZ_*t4;6HZMY;&!9C7J1m&)yZEp zBB$M0%pN2l_M3+7C$FHGHLK7h^wn2y;;Cjwwi*BpkT8?)V3X_I61N~!tg5P-_8c)k zuVhT0nUMj{Zry8!HhZr_2gvcAjsBddw!5EA^hLGRp@;ov! zGJX>8%5m*b-nRHK;OjHgh4e=rZeZRoj{UAv0)qaYZZDJy2j;pi7Q!LyvQ?GL$VwA- zRJ4AR2M&5Gqwpw*d00Lwi>>U~Ib*J%KUO>_4gpHsL(tTzqA?c7nLryT5C$IoyC z{jpJd`iNd5QiJ<@s8ER)d@;`>c3Hjc#d>cE22Yr9Y?Z9}Zf=RkqM8a+Qr@Q9MD)ul z-{@C%-P^d)Z;-!8mdb#OVF(r_NP^a<_^{Xwp>Kx4)21D#)INg}CIrlwf~XGh8xnie z+Ou)HZ!v7FArQtdvB=sK9y*sqD%m|@GCyKEIlQA7Of)@DO>p0k^Vxvlkx59p#k^$e zLreJ1`xLo)${p9sbM>W%8fKI?N#ak4RPGx6mgXMYq=O5p$U#|DBcikpcN}uf=#K<0 z8;USThXqeY#P@SxdoqN)HA8n@qwyq(5WjH|#az z`AH!b`R_KupX&U51|`I;+OhdHdmvD+Ynq=3$RU!2n7o64pyw>CP5l+Vbmtftu>3IK zH8ijsN)RE_zOTHEU;a9MKQGOI4Zjy3|4o=iT}8!Usa!QgCnHt_m%PvWdXp0kL|a8A zXmC)(7>#e|L)}V;@*hdYud`>o4b?!;rLMfjjIq5H8UYc}kX+d%oW2FZOs6WWeeDdP zFXLwvy*v48ToZ;W{T7GTO2AF0cfw!;e8xE-?#g*DUKZiaf!rw78)*igD_ZzF&fH+; z{5;{^?Jyi5qf-9AJ>gtP@8BSr7b$3z0wjG~PS==BaPezS!7p~*PdNS&*E^}FRa@gpV8zXS-rT!>Nqc7hfjeqS0o~< zL!+9bjwl>r_;v({S1v(d;+M*>rVbn!a084ga+oVOw|fPPSu=c9&*O-o+$6Tn>;D0rf2pn#IX(Fo0X;#Fj>OJ;i0qc8BBbW2^vP| zGj#oxdXxX-;hEXov=(-(Z3CgsoY)>nmuSaa(Vu zvSG!ZhV34HH9zbage%fdo!I_Abs?OiGj|*S;c7E)L>a+ZPtbq{4d+k^k8YZyt zbGW#<@A}~xQlAOO0%;>Gh!2#?`ePuHr}gB3J|xoLK*#3v`a#aox!kf0Yn^p<@!PR; zyKT0(9QAY}gJKLw4(~gb{}}J2|KV6#jTyFqN(FdTUR(|_$f*>xxq`42gX5a+j_zr- z??D7`tgHvubh$tnljRX^4f5P%lNmqaYnJ%@mn?$_^Wp^xw1$A>Q(mo&e%eU?%Zc+e ze#8Z2`(nf6|A$@i4{FPY0}cP+_c&+y!NzVJ6+Z(s$e0hOe-_nRqbCEU=PX-Tn@gOL z%P)t&vFEMg{$TuFpOgJ^B6Nxenml zkFsoJNL$!6nbL4p&{)@1pU7U%2dwaakrIuo$WQ+gfGWoU@3t|Pq%U9WTQlLo40?!f8@!yL2?w2Eo(0NA>h zfud#95L%$hdoXQu5pro1H`oo4rNzhuJYj}V2@&VY$F|2E3bL}%ioVxd9p5dNE8B|i z9iKcNJ4jG2U?j31qjA~y(F3cGyyCjSh)afucg5(=c_+`j)J2(k1(lSY5#Q`~&XqFF z&4(InvqYZbW;l>VZp*Ju`+BZ?p?QT#-TuZrQc21CFXdDFki49h0Kr>2KCnPxx&*;`(xPTaixK%lHIx;pGlkWv( zT8$U!4Q}0*B(+E~>#o?w`kA{X+!Pln{ES))V% zAycq{S`ddnKE>VaFzIZ|glU z*EUu}&aikvCp?DN1O101>1uL-0%`$)`=W_DhO?5OAWUtWF*M$k#NNk7^?7Ae&#mDI zPe<69g?*ln6Wb3g;V--Mno%yTH>kTS_Sk~F4np~@ChUy~I2BlYQ1O3IyusWzn5a;! zTeg5sJYO%8XgD7Za+a?#M({g()m<}7ebinS03eTm&Vx*J#0$Cat6cy7RlbsG$ z^YTMpkA-$cVOD%%P0mE66MD^(-yGe(a#GfiP*FT~-i8~!^%BI+-84~5cUAit4j@=U z>IxW%(tSSK0Zvm}nIQJf7pxtN(|8UY<0poN+O9Z$bl$5|Z9Jz?5WYrR6Sp5X&xLTi z0v1C2npYEdr?qO%Pas!C51edI%U_EW=k(j&B#oc9ov%++wvb6|Cq~Ld7!pDgXw9Pg zsVz)z0%D&Yeiou{4d|>MDQZk;*CfQvVq?S#s$B3ozn8sL`%`*COFkEB>$g=C1DymR z0bif*@tK)cl>tWpk5MDydA_@F`Q443FE%J>XfrCNU?)pU%K^t3UNfssVBBf)CeV5B zC`dp_33a(zcS5IxltSJ^OABYYM&DN&Wwb3V$zZ^Bo6CQ$_a*ZJ2Mc>hy2NsJo!xK8 z$AEIX%YG>na3=Df9_X43jP#3$rh;p2&LtsDib7A?810gGXq&AI|jjK{t^>GG_sQ1%WPio&l)?|Bpe@EQt zxL^}WHQiM}k$re`ChH{I^h^0zkrb3Z2$S9G;OdZxA9mZa)Q!NLCull7$2iG7=Y&>h zy)pX((}Uq!Jb*FlAQQpD7?hv$n{HqBJe%W3gI-{jI8Zh;*ud#AZ6>IJz6RSkhv^zPxai>1 z(h%aH-&yCeo;W}06J7z#@NVJ{AZ^1c8$zC-Xrv>B7-Zu%`j(lPXnff$S_NcOT)QUV zYE^aw+hyw>W=Sm0GIYO?Y`dS}8;{Vp*#m)_xfnmZjf5VZ0E$5j0X$F}wIPO)&dQR4 z0^<LbaCDcrZQYjFLd!Bhw}MSrh<4y)<_WrF*Gc1UgK%fs_^+Im zYKuMTKN%9K`B~?GMo>DC{$p`Da3?+xP_cb0AF@8#B2=prCV*m5h;NTAnCr&S`?--?H;(&%bZHIy*p z5pss*unas=yIYpG@-KAZ?|)$>+Xw?l54Kc(7m-YRBo^MX3ZS4GEv`TTO%Mu`w{0cN z+r)Kj(=xHxIkPE;M;Lo`%DTS)$4tMy`l9uj0JY0CA60}H_Gw!LL#b=Mo>cn29-lj0 zKSEtiFQSKxHoPJ8rR!zLJ~DCDK9YW-+3HbDk%XMB?paR&1p*W`__Qm<)(3ZUwvQYn zfJ`7@m!LAz@rJWwdfP0A8IY6Kr3jh<1v#Yk=nmxbNi1CFl{GYJ0d&?|Tu%$H&!_hN zx@lRc5wn9%&HZWna5QBYB^zUHZ#sV0@{}wpE)~#KZ^8#arO4ziFZw zWyQ_?>UqKan8v{>-tesa61T9VQLa_ve{uttcnzQjG3p@Ls zC%n)A^^*+?4#dFoq)>juc3VE{?^oYmdx$!fUyS|vDv7|~Vh3*uOe;nx%pkI3-e| zPsWo_;Us3`VFYkKG-^Lj3Z9SiX29L@fZpuoV<#1p+d=PbwiT&b z7VW5gFYqzoaS-sHU7Tnux(BzQ@e$<8fR_PU20>9;tpLWk8XMtDmKrE9QzrDs{E13S=G?^yxm)E3QFj7mxk1bq`+1Gv8PC48`{l41U zQ*_wqw#VmH{2$0RElzIVT3A)3jhEcpYEOEc{%)vA>b|_AX4$8_y!x8xI5NMWq9&4K zyMFq@y*Q_yN81NY-$Zn}I%i+cK0!*G2qoianxx*a7ete4nzi8gvZ!IC^XW#2d zvW)-CKBs@2SFl%$c=pj>*S?SqJ57yJMa5x%3<<$8gE9zTDNA-HP`$h!`jEgBMtNJ>(YgrQ*`V`xJ| z134|NbUm9^x13y>-6V7W z4tE&nt!-+%EqRBm2tHWK2rCPdBCu5~LUx~i;fyVw3f}&-yuvo79m+oD3EeDFD%R!$Kuv`qhwW&ZQ8Zd1*d3yR8hPFY?ge3rP853ger@xGeB{_dAYm= z@Qa%J4HxYcmd zu@DgA7qdHf@HgQpLLAJfyywpADt{=KPMS-hkC?*c(u#-&naLO-Y9y0NjStf7y&H_~6uc z?SSijmUC(SE~@(oQpwh&yb+NB@d^ z7MCgs3CTkp+XFYjvxt{hC#-tx-S8vIh7bhwi{)U$7m;ZRPM|ddCq3>{b>hc805uJd zg7{7mPRKF|S9T`5B4!v9ngR-{v`;UEujit$yjaPn!!-K(V(Uk2b*uV5$(9SGv1Ox< zWpxU8lXF$QcM2EAsn$c9Jg4!<#nFlRkt@i9x&88=3yxh5y9_&l0q>-43R!YDzMRDC z@V$8qF8#ifjzZE)g0rWQKpP$^J=?@Rr!Bk$aI39hE0?{d5R=3XH+Z{TaTc6l{l@A! z*m?XonyfwmS+0E`*qo=x`b6R0rmrOfg}KJHnQ9voTaW}nS-rl)Cr)pQWGKSai!yvVHNMFA;7i{nPU>wL6Esuy030hITK}{0=dlhKj zK?z?eahUr|t2+SXwykbI=^FYoq7jJYzNY03e_O!76SwEe-u9)4lDE&Cv*tqC$`(c)@;m;P4<1nx zqh6Xi*xW8cuC|X)H*AS4*4|hmCGLy`k08)~I!YbMK%)fQNjgphw^o4h{T>4H6G>2@5Ivd6mgtA{xjzhEg7#M z`#euceTnF%oc%#0ivgKIo8Dd#DX3dec(1$-6z}x%GMVc!0156sG1KCXJy%S92&l{h zon7f?O&R&mUAK!0z5d&!;u%l@$CvYm0Fs%DgdpiDER^+-ZLj%VJC>`M6N%$LPZSf9 z%q=^Sle~El7|;|7ztppKsXIS?Rta24{vkFYZg503jF4LXlmJc1rB#Q~P(d15a%v3` z6_0?l&H^rpR>7q6GekQ~n*9fdfuA{KfpH04!g+om4(78i_-(zorNHQvBH6d@wxX{9 z&>Y-}ScxbPJ~X7XuDmAugMgbBWuOP(5H1&+wSPdYSz)(Z3+y8S|EJ;>uE)GAp7LW< zCH*Y#9ER7md0Zla8MLPlMSE&Wu?nKP>GfPc{$&NHNH?BVuG! zFfullO?}qXplay1F)a1z~rC<=p)A~RVu2S$zJO=#0A3T*K1Oi&Z<98AS zRQvOE9ar(e7@c?J{c6+1zq%yTA-6>OS)BIGp5cXuNyh&qw2=Os`mr$Ms`td6>$qAw z1|0Y+iQv3wflGE&!t41t3C*eUxC-skf~Uj#`WMS^uY*|KclheDbOumM63Urxa2S@Ro8L0#hhvHzAR%{>L*_rB++rlN z7B0xWAWhF?Eh;OVR`Gh#xP+c>kQSXwip?_?^;SuBv!({7`R8FbGRg~^%}bnGmT+!4 zN%Vx@xNhAG*Ld9A{btXx2@`*6lj50JVm%c6;ttCuXt^8Pm2p(x1?gS;BXD?qZBtfL z!=Bf2=K|#g)!41XH)069_J%=^J0HJd!^4^8Kw8^E#(&+`x5%*+aq0C5&v3R>uH?`n-BHOK9y+A2x_%TMoh-3tMXxh~Hx+uiB8w zDXgn;BwF?V7N1_8i#};a65coD|C1vGc&x6%EV}~wJZhdX*X0sjG{lN8O7eN!Jh9 z>_uFccdc;;cN;$F)e>%wA4*^cq=!1rWmX)ux-`ElY<&iF;PIfV6Tw;A{{pfF-Tbsap~Sz+GrqmaHSVE+hr`G zTPf8t$pnx(@sp}bZT()%^IU!0-1qd|&sthyUG9WXiAQG|bHjHnQ#ZHXb74&=?Z{gw zs8SqQyvVLKoEt-}Ay~5^+@3S^H8WcY+Z65sw7!>*!@)%-n?HJ7{jN~6eRrPMDtYJM zRydD8PWPhbe+^-34wD*h%WKlNHm!87bidr<)Ei?Ul0dCjgV8%R71w9+oQqxQZug1i zgKkv^$$^K1LpTk2E>R|k<=YvNLgYz-0$0CkdU|pJAPswBolJ675N~ku>xFyznS%g} zSBLXOYEGYz7ers=E)8Ak8lP6RbV?T~bEC$W3h1hW3C>=P*9t{EPg($M>L~--gDoAZ zrx0`U)|`L6X4m@4rJFui$MDVjxCS2hN^$%Vh$nwa@{+WsdjXwh98LVUK2mLeT5A!` z-6uZ+MVIz|ynl-VjfCP*UH6hweo4P{EvJyX&xE*AzUx<@oeU0B2h)3*GdM!SUl|t} zFt|dJ<9JAVh~dtmG;9#SGI<98EO<+Tz0beF%`8qh`~K}CoN3mBKUZLPuNHy1IxIr; zBHF39!rt~?L3w#~do@~!J|eXa>3M+6|NC+LAu#v?X(6FqT2wR?<^MUw$=!ULZomJm zV7lfpc5%9VARzv-g*w}*P2ti-zICNKuXNW9A;z$9E64t;W-A8s(TA@`8ua$p81j8C3n zd(VXI9pg_qb$K=nOPCj4Ptn3DZ3~bq$*XrXE@RRVY335WELL1o%+$!~MsjmEZlyRB zR0GtHux9vmjqs_VcNhAM+kRSl6bFSs=&yWqXeiPg}3a!b7MTm~CO?9%U zd<>xUV^O6gJR@s`&fjh5_lHEvuRa2wvcy? z9Xn87H094o*FK!RTPVK#BYmvv@+n1W?&|3o+8wF$nsqH@8h!i-TW%mf+AaJ8&s{#f z;+~wuB4&N3{ZzhS3~;CAqOF~gl$4bE3^+VA0dJ39>edoi9QQVE${MivZi%?v)a`(9 zA6=s(*@I2F)2LS5^z+E>{83g}Pwz)4GGQ__fm^OTTa!iVcelSvH8WcgqfnD{3xYCwJA20Gy!G2*4Hr5u zn`>iwX~1?j=q1z}q(n(bgw4%4V~QIu!mOje&!;mvn{3Op^(bIBpnQRDQe#G#h(v#S4taQSFM^mYiKy~r|apcnmw+O zJY8?F;c&5?np5_VTu3_Kq0`ehj(Yb{%aj>TUmX)w^)I$XYa;UsGTZ)^^N#;%+y8eg z0r!A6(u0JnS+r9VI(7b1@m3mDmf!-bmE~&HPSZv!G^Jc-9I1GW#IIk)lR5nQB&44D zgP?S=1iUn6)0y$|zHbYk98OP{nw)O8xsn|QePEIj5{#a#hGhuEo8Tr|255Oc^M#W2 zCS$1JPT4%}N-rAZRA-XRKqYn>oAtVBKx-AAJm~uR>Z&=&;aY=P#?PzAz8DM)42Qjb zNMqTo+2j%eKT|jn^>gp7JY$~r zepw(lV;yIFv7u5iVBmn|l6ofP3oglD+sk++dz6>YZ!%-xwiG&7Uy zN((5z?z$@6wsknT$fC}6wO=yeegtg};`xOjS-6Jw%7ty+YOArFR3xt7#zlfRM1s@FF!T;dk#eqxBZA?9bb(=4Qd;>B;VW z=B~UtO|}~jBCI1Fc#y5tMUwZ}%p_G;x}P_w-B;MOJyY*;8kIP4EYhl9(5N^WMcel6 zqCuo1{uR3*&+5R*raS)${^rW;GfhN0u_Wxb7)lyXz;qh(Nys9n)*lvDR@i!^4{;Yk zOj90?UnVA4nj_<0OG`_@))SL;(DFJ_)?M$QD&_-T!lR1F$-<(nQnzCnWOloIJ{g(+ zL*eOqi#Cx`o-J=J-k4Kf_ol04m|;Ep4nbf58Q~r)c5_pQG*TbArt70?{@Et_spZ!R z^nCeGd8;U}*_7>t>ZF_B)!+yt!D3^)X8_X#oc1oeOYbsk%S+mT3y==@#9~Q7-Ly+; zKErK)A@G2yw#rl$$jS~`kItA#T$;n#A!emD9kc8l<$P3A#d#;#J7gtcmZiE=!rX_t z2ry0^mtTVdr>VOVn#o+!T3mOdH}R0+%0~hg)s4=s5+PzRQgE?yL*H|1=9m^x(;^#D_0sXlBnkDd?VnD7+wFq%e2-B$W2mJkyH^$ z4UvIQd>f{C-5uz1I6TB={%R)dI#}YStGNz!@?>mN(Zh%;ic}Fby{u(X zMvE$Nt0$akXsS%$QtB^UhR2s}cKqFO(nszjCoxc59-9e|E0Yo-0<%DavDoNHtnN-MF%d`x&40#9D87VbZi} zXM-(Kd?F9AvIdWSjcZighao=JXN|M}*|G?>U5HKnWUJQsL~G;hK3W&)wmQbE@4XR{ zXXLjfXfBh<5SBCJ{+E$#?LCrZJcDN2@VG*u>1T^cHarb9D5|#msNTV-^S#|xvSBvi zo8AF^XyamVlxRn%$k$W`JFdKuNzUr3hB1hr%nOh;HZ~lME30=8qC|=|$1s1F*IA|A zG7e4XMTCfm=q;KU;zAs$*kaq|4a?vyNPz?of05X8>3naDhcx^5v>;Y0B%qf0*JNB_ zAsMOohSQS-Z*ood$6-1?+Jth3Z6zUgMq?w(g3mF&cjq^i4Fv}4^G`f1Y#KDEFgq4h z>)lU^A7n}d(|SKhN$r$O))lGASWy*Hkx`JmrS%5g1NLl)t3&Hd7tH;_OmP*Fa{ekCqn>Ikq6x$})#u&BO;w~oS_6cd(VJz0Q zg`I!plr(#x@5_JKb6xvc!h0l_h=WE3XFdIJY3kEihg3C`cEsMGkyGI1svpp#q*^t% zPZF@W+xWMOYyGj~z9t)d8vO0t{n+6jNV7SOH+#Eb&B~3AMa86Z3YS*1J!6c9*P0c2 zruCMcm1Pa#E}-?Klg{V;mOBI@vTmmV^Q2)%`T#^Am%K*9gpNL+ci6KBpSN7~dl=W; zn83*nvLYD~^F(^zq_&y4E2ZkbjFm!-JG6Xs`u9J%>{_a`7BL-ty(gAxb7g#VuxK&t0SmMhLK z27|>p1wI@&PaCMoH%97|dIf{t@940uoA%;4DXb@o9(3BxlYS%3Km3edymos*6GKs~ zh5b8jL29ul+WbD{rZ>!>! zcC5#r=H=|fY9OtMs~7EI8NCz)306EKEgMMPP_4K8t2maLS-G8(=WwagEg~K=7Qo4w zkcwit3@gxGe^0Yk_Z(JTvh~yVi6t$WQ*(t3p=O%&cA>exNR;@2g{8;2uA;S+o-(*sabl7rUR%PFWmu zYqNX>WBw`~HnU7Ub7jAdi`IUYYQ-^`8Xr^Ytx}H39!=4C1S?LijABWvIvE%cg0Nj^ zS+V^ScB_zBqXi8bYh5B zPJxA)?8Lm8Zmqk^((l#Zol3AnKv!7W=dHF?H&W&0wS3ey*VCZsJhw-3tL6hZo%WGZKgO@UM-bw8>1lN_r_-vR1;aAc_%3;!bWYscr|aw#RpZ~Z*|JWne>Pb=Yc#k|HaDVAtRZcS>_(|5(B+%2I?Zb=UM;SFFx?(n^jkl@yD;#Qp;803N@^Pl+FNv2lCq|&^J#bmdH4%ID?Kkn=BvvGI$CQw z&$#3$DmjwGyZy;|>;t=alD28bGpiGe{2}!{hkG^tpm)jM@7WqeO@MWofv@G~9H9v_ zLdxByXtj2`QyHCp_vWbD1EQT1Fh;4~^Vuei-8MTu&*uw}gZoM%Y)b0pCj;6V85OJ( zmiZ?w&0<@*GBa=!f+(ibd4~5H%UJu6+=Sr}&rkP!L;%wr`4wV7lL6EQH7)X^tgP%8 zUL5?6_p`41(I53FMb>Y1bX6L4{Yg`{0v7kiw~=@Vb8G^}CY04xI&Dgr^jao|!~Vm` zUWLL059EhtCh~7#Uazyl!1piYRPB!d9gn+5eNPgC@bI0JKohww<QU(!1{9M)GA)m&R!o0E*{=bf z1$Er)n1=|MEFC?C#c3wr3*R*?)%B`#Ev^KMx=BHtA8T>A8Ew^k-52u>F+IYrq7IlW9 zGWG|d$96ttWjg9_clWT6eWa{Pik>S6^=!T==w+0y4sM;z&S}YL6eG0LW7`RdN^44H z!)%&eMZRXkzyBXmXC2ja*#7^G(T#v~2vXABAt?eAgIXm0uzV7RKz22Y8FI!1`FNTvzaIV)wpg{#?=bv0_ z?v)}=^Qb`$fBWlp1xL~`gBwbz(b;L9xd3W<)MlB2f<}t}4}gYU4rYG{c27)n%lp@) z%vyyeHWSo8XLJ&I%py;Y;-I+1vI zgx}cB(Xaki^{^3}TU%;hH{>s+`XieWvdy>TiFJP=OV|1Ouu*|Dd~R{_d3_<)x1cIR zJVFvG9By**+3MGDcESj*f{^jZ63)lWkQWXP4yQayPm03TE6Q%(p`nLRzK;Q}UCzeE1Rgl#M{yNkl z0SH0G^Q_LL7iR1Asv})z?rgxz%G?Xy9QmC#=yIO~J>5cfEk_-=4I4-CC~dPWbMequ zT$Y4;3$~wzQ9X16`&sG^@Vc$XjI?ta5%#HSL=Dj4Q0wL@xG8z;;(Wg!)dX#n@PdbA zZc;Y57Hpokb`d$uo*9Fl#ZP?&6cW8mOtd2Tv&GBB&vJu>_039YdgXm;q=_I(boe{?=ViO?O6;c~mlmqv9)*v2O!47(ey(_z@bFY!{?tWMur2u_INJXFuM!7I69SrN8V74q3O zD?0`-TiaT%nIHP5dfU1YD=e~RsqPs5o9HdLw9s@h#OwW@@o1L(S0G-Kvm%3-wzLPg zl(p-{Cu`~pzEtG2=Qy-TH^&XHVc*Ny8u&fC4CS@xvEldJAUyh)Zc-D{BVBUZQ=(9O zzt#A49M=}?JrjL!tC4()7q)-pxsj83M6@#(_Qh0G5K9F#Qp9O~c6QR&3i+A;2kCE_ zb%x`pkq_rh{aRjaUK=ItZN)e-*qW~B$7W)Zy|j{uc84apd-SX08n}r#t5Ap|qE=Nk z&St2*(ktbp!Ctgy5UHMrS1w~kyQx~yIk1=OZm8gUG<2@;a`F0iLluu>jSne#_N7Je zSA#P~xkrkZQ*Jv&OT3xn*?-r?83sc-wl)?>WT{jYxrYeVCs6V&2_G)2+*p0>hpx!h zo0qDDdn8O;L)&+QC!*EMw&nJw-&7!1HJEd6$D3IaKq}twK+na>R`e+O@7@y*`-;tc z$cshTwFkS(BVUTA@0|_FmrvYkTR9c4#uAC2^-H{1y1zRo@g{sm2rOi~H?^~h3ryZm zIs&Be>@qA}L<4P;-w1u^p`~NZT6H{i$tN{?PPR_?}4*`rT*mGyoUI{9DllVA8$}u(Jqk zbs$YIxPn1KBIdJ8qjxhAg}`tXkn*I8%yXc#xboB{$!+gMb91rIqTnAlWsf-@VAxXM z!V|^$`%|`c6z_Fm zI5O?HDG>G|KR>8e6F2JykBHgCCZu%Dn1;^#^7nRNj^5nk<*O4Z@e2d8jd!K5a58l@ z+YY`V%vwLXtoD{JaacF+65;A8SX=l8H%E7Prz2U5kCk)h{5 zmc-wY!2Oksv!dw&Hb@pm|YJ;Bh+fl|vuJfZW;%>tC+bDIT{!~01?-E3$t zzSoN%@P3trs$O@y=?+U2iI! zH!IWzQayoBhu-vt@+n@=j1U(N{TU6_XrjSux%oTUy?ceg^Ab!TnKw_}pZ$B=_T0RFs>HR;d}+T5umv4rEII%&LWk{s{RD*cUlvkWp7-xBYX(r7Wg2k0z7KQh5!8g?r0=P{{#Wpy zzsWaF)*(IMK6DikDXExk23NVHIDq>ya|?RZ2Mu(lT_>HXQl|rN`3h)v8fd}KXjK7v z3O-A6@vP*Y8xof)CuZAawjw6Gt>1iRZrVcL?AQ`5KU+*LJV-DGNhGM6nLLT`MqBQ? zvXVDH^J^yb&b4TU4FvDCCth9+L+#mhh^OxEkepj>BNXF? zzWSSy2Xo~JJ=_!Bzc6W1;s{V5juopftyz!71N&zmbsn+YV*ZCU$;r--(WL54F{Df) z6uCmq(5vmTc#ANKT#{`M2YR`zWDqbS_F2>lFYb5vZ?LM}-pwyT*ce#mSnlcYBc_{f zTDnVuQR*3fMa89t7%`J>YkM}iCXZ0Q`gU}BZC1chj?BzGU4+A$C~-_8qx5Tgn~#uA zvNg$ZlEx-UG(Dp8a5fXrl}r@Nhf9{Eg2x8RaBb~_hTd(4dafB?yt^SVC8H%<*3R;> zUx|w&4m#|_i$6M1T95zbI~dQvkl&{9M9h`qnwM8K2xK~LCzpEf`m4TfwM)2s;>)P? zn{6LdT9iTU5`pg!gfxFB*7mEV(m^$YI(;cD zt}}Q8XtBt~cB-k2Z23Zn*5y_x6q0&iX~|Z!qaWWZVKt+b>eS~}CNc2vv?98_c{a1m zu`1mt^)JSnhtIhefmUfM?-jG}!Wk=gViC=>=^Q7Cure=}#<*5CpQOe+derPCFT+)I)WhG`5dQ-M<}yJWnw$9ns}5MKgHE5l-x3Qo10Y@h z34m~wI{uq<2;7;HMXpwV!nhKu)$V)eByr+DRC;73Zy7SAZji#>TRZyJ*(xtZPKoSOezr?3F?)XzN zPHelDsljmqAAXDi{K5daEebfi&fTrVK$sPGby6Ue1>4VRM`2c}>!)O@d*!nw=ZgJ< zUv*AUoP&nl$jT~{?BQ23jJ4Yz{~E07=YEXFk7NHi-)VU1cNYXwa(hZI^JfX}%{bG0 z`SGCmIwLr~X0Bmkt@D~m4{|h5x6yRY@~HCY?XN#!b=iB2Tye-!i!-nZsIkMyc7ua<;p^gj9H0t9sIyUJS|TX~#ul-Ag^ z(j21mg^JXazcEs!$}ij+mBh=m6I2u9KQ{;Rx2i%e*EXp8!-~1$Gx?bRv~44TYR-Nn zFvm5|x+8PjYM+Qd{{E0{r%N&Px(sSI<3_voM?YC0gG57Z9k7irKa-us-7U#vX~ss$JC1mK z^2didv$9}C)t+3N4e4bOTJma^y5hrqYLzz>56Y^dq**lrrq=Pq=ldlOkCML3wdz&h-r6f*?UWaRWgzoIA3 z!hg{I#*(2--5%Pk+pz2bS};obahgK68DEOdtsWm(QnJ`V;v8;|Yho}n9bei#(o0w?BLBR!4L^9#tuuiHNe5dZNJ}8T-#8J)KgS@?Iv00eCO|Hkg!^v4~$8mP~V_OLr`AItvyULw}f*LWvlhb!r^Ypq+k|^p$*} z1yx8|KObQp+&h)iQHk-5e0yLxW&tsK!w&sRpMQZFfScs6c8L2HR_iPUR=#B%XP?Rb z`hY>yC%OVT&z*Vnj?o)%LT$G8;J^4Crn@V^sN}8oS)&ivujAt!kpinK>*VWL9aG^h zNOFwd-caOg8>z^@v#+8+u)jKSQ*uQW1ZC=GCMw)~Pxph{&f;A8QT$tV**`=O zj3N#~k8k*D{JUkY_$p;SP-&r}Q^FxbK>T!VlxVc7DBgT+!OO!EgOIJe_N~7@bOQyJP+yDJ-*;6blzRzyM8KYnAlFWP=+)>eF$iNhArf8rNU6( z{-rp1r9;)y&8+xHp~-fvmBTjyT0j6L9vz@f>D7``)ZZaf*QC#fCWjBDPC{lL9EaZq z;IS1?P6lmg(Mn?w`*MO5XO0W(4@0eDd7q8p$3I zEDGPhJsyf^;)jFb=#k9N5v>(Sx1WG6k=ho!WKT;9o?nEfEK(nKb8Z?hNrVR5hRDyQ z0ep7ZC6EBTty#by?LQtHTEROS=hD|ft1bta&qlNb)KgUoccIl`k@Xo*7xRWNGk7l$XQCE2EE(Rzwf&Is1)un~O^pMdPWD&`KG$r8nW)2?o8@j*26sP0&Ysv-<`OU_Zv| zQ)k|Y=oE_V6rrebCi3`Y$}p2aCh{0PWqmtkO*cbZ>j6K-&>Ya>70k*7U+l6vEEZ zmQ{PreZC_C2*~!*Q-RbUq+8Tpi#lofnC4=&r{B_dFpsRzcQJT3X^|KMD=PiG);~-K zQ`SICj^5M@o*0Un4CHE>8R(h2_BTWw3eXlZ4{?wf)qOpLPww1}WPk-Z8qW73i9zIn zLB!Y|*Y2~O31CyZXB%3R@43EN;)T@ySo(Je%k*8&J8gbakX}jQ@KK%nokVc>fMK(! zT|6jL>x*4=RE_+S^+#=3h!{z`mMYt znzp$|j&Rk%x>{PI4gh7)hmUVNf){tq)L<|>o&Iwu*`x(UmhV7@KtkrZI5~g-~-e5?|OV!iO8e*o#*68VZHNyto;qKI(C1fCO$-7q zB!+j`_CB_Yom(|!lG4B5zU#5>=xL40j`%y!iq{aVWd|su(%thQfi8*!vR23W>hB`I zK;`6Vor#&+TQ6nbxg~CxSPqX@&xv;Z9Y6#tIygO}QBK(l&y~B)C42iUP1cGFkF85Z zocPdQy5^b{^!Jy##+ke|U3Q))FTAL7)6Gy$M!i(%oKuZ4{ z(~A7=SPAI*M%kEmn<#A1IRKXWX~8K1S{8rQt~F`8+!rgi%9E(km?_lK`GbBW@Ve%u zzLDETg=F5j>@i(e4(ye2OZJaJijUIE4v7>Xk0EVc;e2Fu3+&+&aDt?mJ_lg69GU#D zjf-jKe~Z|9uKKNO71MoR(bc^iX1^QpNq=SQZ# z4-K&hRjljVrF-av7otd5qDPc|W>SmRb9DiVvn_DEI>-;K(Ct6Hb0B`Z zrN-?;{7o3!2Es9`q@ZCJ0b-%rlLYuVzD#+{(}6Gq+I4xEv#~v3JxTkldipM(%d23~ zQ~9MYQ+;dhg`ZiHlujcxQ?l`?c0LuoU6&KGK_Y8(UWYzECuuQqtcQ{=(XPKhD67HrJ1MCT+$xEM@$1KJ>$tdY`q-2 z8qK1B{)LP{zfzXSd3Bx+ugCx;PjnGgkv5S9JEA)#?5%{JWX53wQ53<-CU?xJPnzv@ zL`swo;LL^Srr}*?SU+>TZzO}0pg7e-tB-lyY+Jk`-8oDggqvS>V(w3pzcRUov^un) zFpPK}2o3?WW%3+45D4)JI9^8-r`Mp&>q?KUTONtdG0{sDwKF99|gL|#o z0nr(Niw{&;aWBc9%W*Qr6XX4v0QXE1pN>}>jP4dbSq_dfd16}*ync}sYfC8}e??E9kvW zX`psgqd4S`tAuU8cl(iUc*Gmv8+l{<>+;^IW{1D zbZ!qi1pR&61%16_hj2(ZN6Br{O?19JiX9DPL1eA)A15_;X8?5N{F;DgGXn5S!}8VX zB1GhGu(ITqRX=TyO$i{BE^&0!>eGqM=}J6lTTXHQ=!$D($80a-YBz+YXx2Vr5tEJS%RA5%YI? z@j`#`B>KIV7fNi|&#TYKF`4~e%@roqA@hz=|K-a`l!!SckP$WeZJ^H;FieZs*w~;C zvOD_wtA~i5F4h;k!RA$E-ZTtvqUxas93h@cD04m@6z>od;erd+(J_w$X8Hp9IBBVt zFjjqOh`mXT?S{_fAJu>Bu9s`dmT|Kj+Is8qL^+8Kc{2)hAxqBcy#dX!FM;YQ{)4>|#Lh z?!5^Ft4oV8zFaLts7xIu7W(5QfcrDg)a^&XzPdCs`m+|bu)nW;mwra6_P5f#C+-;V zNTa6^Ja^&~UTM^QnV;%*V}1fYq%ILG6a>KH%J!65H3|!5*pSx#4qr zCi46Nbw`s5AFY-o_?jeZuMhs&Q_^^(O#D;((N8!yq%om8#YRq}){Q^pVq`^)rIyuQ ztKuTvV*c-L(xI~oz>>{aF#duoq~@*u6pjU4p;u1YU|ejIsP+(363Eyw4O7AY0Ultx zF$ZKEhj39bU@0nbcgemZRL=ie_RvO%kWlauAG-y6RVr@n>wY{7+@uW|GbCTJxLfu<1$v7?*9iVzLkw#)!x}3c|yHpJm$+0VwM)-;Gfy^dPub; z_>EB6oNy9CY(wUaGfJWt^W{tZ)UK4c^dkHhW|p3vp6bvcC1>ZfbNcRIT8$siAFK;v zo8R&bm5UI7s%73n%fs^Cig3lqiG*qIg|TFc2PHO3pg_TN`Ir~nLX%Z)x=9V&t;BFU z)W9B(%XovSJfVL)ZejOv`fUN_>Z*V_Sql~U$31QjNvp5~^=MF>nt^g|v|2tF9(5e( z;yp2$sFG=O_}2|HN98JBaFsg#cFU&6dn^bXN-2(D%l|o1$C-2dy;P_yoT`ESrKFf) z=l<1|mCcu$wx=yrNtJHSlbgwN?46X0`M$);8?XGh@+X>W|h|M zoNQ6THGa#&e)YT`w66ehBgVSM8=R4n#J4%({3@;K#e<6zM4M|_((&hRdU{PqZ-=vG zeu2TO{_?sgiruk&l;Rle^T9FLY-@>=^DDUKPI3NofPbr1<*l1B+5Iyp?k~5jU_|jz z#jk;Hn(|I7{19TA&oPw+OW3QIx(qA3V6He#{KSPHiR;U)4ojXr;muvlM}PtUD;SQ_ z`yx=`y*8892Kj1?eplCwv|}savFG%+$L1sqv^2z|;aCy&FGx|MnK3gZ)Q|ch#B3FA z3}W$~WQCih&Y#`;NhbZpWPR(hLjkjcx3Xd(Qf_RxqTq2#lJI6M?}S?w$Kah$t=*7K z#odF2mUv*aI9P0#C}SAs^!r~ZAGgw+{Fth@?S6L$Zg+n0{v5(=#&H+)!+6{nW2Gd3 zZlyb}6?~{VIO?)8lUS1KlL~}hDJLjdDG4aUR2-E{pTfF8G}72)7R0ZBFU-ikC(MUg z;1+5z(c)2#=Ri`gX%jbmUEy(q&*V#rLe}{4gQzbF;2|qrQKc4MvipJkY!;Ftv?7nz z51--=_VIl$&7D+Xop&1j*hvHKxg*>l3`lA{eYkVmEd;nuJC#QNu_55i0RBuFTIVid~ zNWZDnPhYv78-0~vFv`#olGrF{eh$HuTjHpO$z&6#mCtGS;>hXFqt>#u$R@LcAOG%d zWOxDl@`6r8u4}#{!$@Zn7P|6_$G(catazMT$_Y0jrRA{m7t$ABV>pgdb$>V9U*Fq@ zzY>)8Vf4nV?{th?+pZ}eQbYN5I0UnIa#HSV|7p$glz8V~zSHgd`CBpJk$)tHca5~du$L=$WqZ0m8uXpa7p7vq4MB)lkGmhndosUuoz zMe-J#DzC78`b!yi)VE6IhS{@Hao2Gij&g27n%)t@COpyJ`7+C(B@cyReTk42Ie-0( zOuUmQu|~!Lyz!)%lZN3=@lD=5a&ZrQa%wd*H*IL?cdckZ3L{VDFh$PDm^! zvS(lUO=KNDv*}0OlT?XUhk0&??~m8YJQm+H1}z*-ufs!+6$tq6C6C+&QVUIuOouK) zACN1oevo}>-KgP%ycdjpfowftzMO`y`!!B!-Id<6SBsZ%@Hl;o{8d7db>LWl{TP@9 z+Oxeg|H}+OgyBDWkH>b-Vrw%KNUfJ826k-R4F=c@1=XzH_ImYqt7O*^$ZH4J_=ftB zCO*{yECAfW+*YIH&3pBPThNo2ZB>|FDj;%iiRcjbdmp|1fxic(Z3{3VAp0S7C@u|%jlK@@)!{9Le{i_H4_rbp60I)0Hv4> zw$n@WzFdV;Utu@LtoRiBII?9MrF0&{A6RH-*&IzK_&fDv{ob7I!Db(}?)R7DfsH#K z5ci=aS9=eCTU?6AK5z9cd_9kes086)k{Ed|*L}yUkD)zeFt%8Xj8bO>sB1&_^P;zP ztzq#$=M_~Mu}-k#8f;b_aN?6G&!9_-_lu5?YL$#@{z{&wrWYhJfzMe9sq2WDhsbS? zCEG7`FoVx__YS9;N1&qw<=|=7gPGx=*sJE!%JJn zFnL6=AH-c6f>xQ-Sg7(aW?q%M<>cQi2eB#okKx*L3{MCQKO%Z+Eb)CuQaUACW}9aJcpA@oMbrgw_voSnugyEZanuSuL3(8qq~6*vK`)(y)pMvgi_uvE zCJK6)AytX(i<}dIaihSgqm#2w<5$lwe;*(n7RvL`O2a;vt?MkAUkV2b^w;d#SKDt_ zkPf_-eEui>_cL|N)(oIf~e`;(VbA2-1VQO@6Y69lHhoIktf z+XAn0A7M)z!yC~E2b5zEQ&N-4jQ*k3z((+w zjOzM@G@W~XMZktePRpy=Pn@-;AA`f!s%!5<8Y+F)m+VRRcx4`0LpR$wq(!Xm_6~Fh zA|^7I)*f1#pXv5N~x>LeMkgq6Nz;>tMH`VN_sb`98MEZ|QVFE?G*`aN!?Zh%f0&?rl=XyGqfVTqk$ zm029~kIcbx0t*|jGE@}2th{8od(E4?tZItkhvPQ>q-n?;ug@rz_&l$+u&TfM7KcY1 zX;0dYe`2b_{eM*8M;{%2(z#Xy$|Y@I~i8X|JCL@*hGw~&u$EwL*^*z z4O}=|1p{ARY%FNaHVH1a?A@+<6nI@7T|o-p&{oxMDBL^GY&R)n1@ab%cMYtD^`C@y zwjl)v=0auYRskqRCuZ0ruh@AR)*Xd!Vqie*;NRf!go#LW1sPh zhm5sB>rLydlq8(Rvtnn@+M(-eH9_%ym;1a#q@4BH{!{F=Dag!ctLq&5-=G}TUUS=( zP3?;fi#~l?KM-WCRZ^>g5~VQPa6jkppv&yJgqIa#NoLc_Tulb)BPySm?{QvGt{9i!f6B|z}tNfX0f(|M&=dP#MSQRF7oc%K` z{*}N2p7$b{bcO$YL>vvNU&Yh@gax(n*+mXRQM;+FZT8}|qw(IOMKV04#B+*(Ti^eb z9RJFW|1H{HMy0O|bab2}u$!kd-|n*xG%W>Z=we08&$F2Ow7eNH0`d?>K>pvdiF*U6 zoj0m9&Tfr6M=g1EDTVIYv4|EmwI&PKe43fA%z~!Q%t`ZmxO*=6@I!rr{`H*xyL^tH zaI_b`GOiv>k`|?8Qt^CBDh0TD$S8)i|95gXF8~D;aGZ(NMH-3O7U7HHBu7I-rDANP z$5j_5ZB?Gm$tUAh#9V!yER8SLJjo2uPh9VIp5E+l3m_$bocp3n+*tPh?hZZ?Q zzUujQUK*$1k=iSVVk^R`}$JEB7vwVLb&M?V;;W-|FZebM?tYGI0(Cf*gVnauR; zPedT!B2n?|f;pjiVCW2*De*$?u~^-P7fzu)<_@eJdLrjW4pETQ!>9q(`JW4519JLn1+~EFa)&} z<`B53U#63Tfug5k`Cf!NuPQ-lxyd#S3=GyN3od@zWO`K|#{u0+r@So6k4*-Adtzh| z=DH-tF?p1XF{!XGKAzu>H<(9n6J*6A#jn}x&wb#YzlwEh+iD8sw|2HgAi|WhSFd7k z3%(IuW_<>Mku}Z~j1D%}LFlm^>M=a=Ldpi#wS7 zvcAW)--k(r@+~Yb)@npONlPsoSiduxdB89idfj$3e||J6iTN#+jrl>}{3~8Lq~L(T zJMDjjN4TiqHohnU0fD3L=Zg#QMB>MfY%9LKOo1nJNaqq6&F?jsh>8BanR=beJ*{tE zOA72iucyPv12MhT_!c$s+v=K{3=D@6`~Lm0NA$J4x)l5iZ10b-bjZaQpnG;Z(L=r@ zI#EZ92@5qFIoQp2{f9zlZP)J>-g+XDNG*(gX7AweTM44H^j0EnU554;lLnqG<}xlG zf8>0=kC8qNOii8TG#^mr8SObAUkC8B0Cq!utGT#%q*n2idlqcHk)i+;xcS{50v06Y zMA+OJ&koOaAE3B5hcllq&K!$)ZM>}4xS4{>O1`t>udYU~)O5Fu1b(S96)|}(XiV=9 z{aYfnpx$9!;kLse#I3~EUF%^bA>PzK)AhPu?dfafz4)DsN{rj`QfhR5o)3?dF{!0R z#;h%*8F*n8v`!tgQ(ktp&*ILj2)$}Z_QeQlZivYJ%VZe|F|j_3b2oU`#cNze`-Q}$ z;=O!sP{F5o(Eg8*Pd5iGeF;pmaW8PP{iO2OBULrd;f%73m#LR$op>yLsGDIFEghIU zUs6J%zsB;1_NA`k?TLpSD+4txr;~+<6@|7gsL!7wF}I)PQeU|I2iWbdH4)s) z&#yr{TjUqOD@W2RB|9p{W^5Q3ZtY6{)UDvec$?E$s(+y>g=)mL6Z$>6A{u zH;2E)KiuEn$E8yAS$pe3=7ENB>gKy+Lv}Vr^%l-B!MtsoE?f?#T7GSFXH4?JllZO} zftWL1UYB{!Gxb8|KIl@&wBVUaD@HDBn^BlKPFvA&x|TGBdF6#PZ6lUEW*9wM58(|Z z_4dc!4YzFZUJRE*K}$D{xkc!Io7bz3SCQTu1jY+qZI`8KnVEBsLChWu?uzaV*`f0^ zgT1AtrEQIK5+o95ji^T5l5=!4+Jg>s6xq2y+5JT7)&6XiBPS!+ zea`*Io>$IdMmGg|Q-%~xud@>Wg@;{l&mbDD=9s;LRxf35Hzz)^ zAQoEuMjmTLqjFbU@;AGs`woq3lY7xgZii#C*1;1ZPS4JhyZScHr!`*5Hl8`pmYmK7 zYl5a2P+>9~z2FHOK8I_6_03ecnK>teswrSC6fX0dABucjLVr1K%u zA%G(O6{yX)VH529L6D0ii{bRA_l>0Jpv$eH-L^z#yg`fCai?~@nRYn|ZyfxqCnY6< zdJK9^dTRsoZnj__{o^DrgL_lo+&SN5mpPIK9dwTECi4afkpd9R@b%Cmr+e zpG3iEt zSx~y)mhUviNU81z9x%kQ5jYG%Wc9NSVK9t#HUxQsg0`VxI5;@!!?@J7qECffC`&ceI+=9{yfp7JwCJB7~*BMvwEWx^xNs!J1@}-(x;mj z@og%@tCmF$JWBiL7{bcd+9VKXGN#y20VGGVqO!o(TG@RqeGK5I=5@E{`|JRz#mj$U zt$LlrEm#4o+L2r<2k|w4~6J(cK?QBep zPV-ex(Dbv<3~4UK;1YnLKsVIDOqoYkd83yTd`R2dRD zy_Mz+aZTdw`~Gan%hVV%Sewpbb#*`H)UVXNZO&K~Cu#sanJvOEu`i;Kt==4pnGHpP zsy*`R?)APh%9;thrzv?p++gKY{bsy%u7Uw08#dB=BF+BQCto{&e7@}L?CY^y`AOx5 zW%Qlc-Z8!?#Bl;eFbs7L^Xe#N`*SegJh2vZgJC&kW#>$n@?`^u0dUiUyA_tZB8tVs zFQteW7MSTnK`VDaNz)Q+4u8+o*swtGS5HsB{Q3MUWcEh=OW8iY!9RTY@NkhPpyPGF z0}C&rNgo@)X>fnCJ>hV=Gu88;xv|k$g167g%!~&E!j&N~XHWI4kbel{l<@L2rX{}q zGnO*kXW6&lI+xGl`)J7hVK-US&7OxpMouEQcDCewB!rc*vAM}W1m5K*5M*a#BffVT zz&vdwo&v!97jwBC-@eL2^fjCu2YP z`}ovpCN>G)%+)!(U*WM+R@nHZLMA_n)ZM^aebF2@hJma3rKP2#Ll3iq!cG-WkMdw0 zisP>Y3ywEO7{am@k_x142|6!Nk=Bw}-$_=Z%PNQ5CT7?-fqB_@gEVr;6<`l>1cm?d zo5W-sXenw?2Yggta65QHhGSr6W>#>H#e8{^qvv{(*gAoj-un@VlDO&8f=kCE0Y&>+ zfrkw2L_P1XBd%yQ&eV$KW>sb&XM4hSnhL`tYJ&`NCE3@5np6~n);;ks{Si!xfOfmP zH0=kWwn50afLYzodG<9fmRmxSeG+!n5C72f{1CLJt(6Z;5{*OeV7BOx-1FzpMSM5^ zsiggJidV&oDb}|;COx$9H=m%~Yu_`!Y&abV8*>|H@eT@7JdtmC+UEYD zOgytW5zR0ETCPUQJd}R;MGuG9bN4k?d}il#NDp|(!L3>NJV2HmCoH*#|adBvE)12n_QC7NF=4k=yS zT_rlS4ziU+Vbhx(KZ(t** zS*@UHU!e-IH*|Rc3j82ny1kSS>eZ3`EXgHnS&$Coq=29PdgmJZj@aarPd%TRP<>!X z#Yy&eXnVtxFOepW!!N}*2uSHaYaYzw`xfeSGx`=V%KP`k;rR0S`qWWTXKr6GCwCEX zJfLLaA**>M5EqJp>)jXppS@V8>7N`FGxP6dBM`T>`Zah7hg(H)`|@!?-ZgW5r=hc! z^D<0XU5~{(*vXk-pJy_eCudx4wgMMvo0`)~PM7h^l70BLJO(l8$BB1VdKqChA?i>H zL!8^ZafzzBwfV7lO&PBtaxOmdWcn?Jo!_~lE<;W8<4#(B1rNjBo+nSY+AUGwg#1|Z zwA2t8H@sS`Ha#b0lI1T$4{L_FH<)E2EP1$}OgB*a^H-;D9C<)Iv4~M&aXTD6+PoEeRFbqQUpJIyBg07Hz?O9i89$FNriR% zSb!MN52@!z<}6+3ozU*GsZ0c}37r7@mT1Qe$WTXp#A)x{wetE%$x;0!8k{A%4IpXT z;aq9;;nQh{_WI}=>-I%NFQ|u25uGr6EgLzn%Mv81tE($1l6!8+pvT1A?Pkx)_G9Lm zVc)Oa>1t7*)AxdH&T;t6&=dXRVNyX>dmnppdulYKbzmhX-PW1Q=b;DVyQFg1Niho@ z6xSybm6gnL2^?{_PziMrT>_>gpKnbN2_kIlJxgsH`ra5Ds~cY~p0{z` z-=XdmUQYD<5{FOqmM%VWd*PM&(aPGwz@O}kjRlc>`E-!w+e_gRmf+@3$SI$m{X<>t{S?bSAh z%NoH#4*8``%me6H20m2iNS`TVx_NvP!GOXhYpEyVgyh!`3NM&keC>^{u*%)sQocC_ z^vAyQy}Ch(8yuGE;=j+p6V0s13@qi@y{(6=kN%R|U&sFa_IZl#+B`pMeK+`1e!sQF zVI-s8$YDbtc6NFG7#A<0z3<8+0yJ4#nNzg{u3fvrS=*#r-kNm6OY{KcLMHI5; zeMtv$q*P#3x+6>4(l@2ch$-sQbHWVS?Kh?MYG&K2)g6$iZ{ zRd?>MknM|`ze$wd!q4V?N3s?4&IMsd?-j_C&=gj{57g&$`uS!vKY2idB_71!9pON?~0lhNR)Lt7yz)0H*N|!Bn6ZZAU!}>aks>x0|7N zW#tiWX-}^K#&jmw!D$qOWV6b7X}5y+Xu(pl28Lg_avIMx%9UZ*P<*u^jKic=tZGY& zzP?_%Z~NM@d99?{YO$)4J)(Gz0Ez*t@z}3!BUo-a;y4QBI-V3%|gvOFl7EvCNw|u!@P2a# zEHuBr9mu@j*HEdXD)&5^(0PVRjCRqXrt zG))5GI?v-5L2e5%21}!l%f97Xx1QB78o{BiJ@(}_)tLqD-_SNcVz#btf^%v2ve;fO zjt~#a2w>KhN`0^n2;Ua_uz1s2R7qd^Wuyx#2jwB`!SGq|bEgj11E&4~#j=rT zsK)8Mnul}ZaJo7sBuCznluzF~NBX=#rc7&q_bT6(9m#Gou7{z8ZR4LAe*rI6xv1Nk zSMH9QG1Xfm8r1v!^WKf>?LtWNl9*b0k{)8E!94akvrBRDUCK4?o zVueR+aT3X%d>^SzB3obcL6F(iZ8s@D*&G zIFBolQ}#4LPVFM%(!?x%OPxvw>Z0l&Ya*Sesi)&i{P%(Qy7^Rs?PD#A;J|ue{H|Tu z`}rVC$wXIcs;mi2hdQPrnP_BNVkB5WZ*D;vb6yz)E?Ne0t+b{EYHOV>9OjSw z5JYvh{eXDNtZL-;v`Jrn?ccIr!p`xhH zJ}m?Tdkky)s0OiGc|@S|Y}|^7dK+z@a*0Yl)-xPz_2|cnsHH|7&Z12g)EUC-81_Q( z@J>xYZBq%_=hi3Rw~g2YHL%^Q^m+6ZBMR(BhaksZH7C{_PN+Mqw2;k4|Tl3Wm|hg{31pgWaA`fS1^xRLGhmAl(t54aU9T5(AV6%S5y_jN$|+^ z^QVrZ?inoySM3k((t)G!d5!+#^*jWV!PrY#J^#!+x9v4c!~p!)p5eY0e5PSNRrEl3 zA79`5f(3OKK3~xMePnlLqqF-e+wA=JCrs3=_5lB(|_SVV2?~mWZyK~0-;XWw$getCX?-`FYE=>M1aRlPhP1NYc$pV zY7#ufApQRUBthH081q&gYqwI}sw3yx?Kt2#aLaK3K?K2+BUaGXve$M5>uhVGZ9z7! z@g2bq;qVXDyBVMbfeoP(K@|_u!N>|f2vi8tk38~-0cZgD5H!6&GG&k+fzOv{D<3>- zxo(A2jsS_|1GGXw102G-8Zd6@(xnEJLh$80pcv;r``OPLARVOv02+cOz$yI)$VL!H zV52;KBjn+^O5XwIu7zkLd}9p$`Jew;5LD?4f;i{7j=%~imZ5+aeV`2hTHpA_Hw-`q z_>XXjFp5<@;3e&WkEFwU6Cf08b3j!DT+X8`z-tsjSVn-gUal5|qyL}$_(q6__wd4(;1$3iN(+<=l#B8O5R_}Q$(Klv@&zFrA)fvKpkgJ@dFnzz zfr5cPa*t3ZAI62bfg%Uv3-|}H%>G;So3a5=ndj6^8&Keo2E_z<@qk4+L^_lp%mWlo zcuv3@6dx#622)zt{$MP?{6*=-+=rLU34l)OBy=AAq8})MP}VTk^oOw~J@baKwBE>+ zO&?IKFxLTiX(w&E|Ni^UeMg_IH#he@_Y&n`Pyydi{GcRazR)N7{F~qW#>_X`1pv#O zq&#>;yZ-5){>d1O@S6|HC3s4C+!uV9^BBx<-*S)RRpGmK0qoK?6gAI2`>g3J{J>nF&7!DFMjchMu7uJOd1qklnJjP?k&==7Emq*7o??a+|R6u zd}n>5Ox8QVaQX(nQM6rYd`(5S_&+>tXHL(M`Li?Z5$AqB8GkSt5E9NT@ zv--}(0{9uIye6FV()lxGxq9(&_fAPmmiHBKVV=dS=W%sSo$*k#4xXOm4rI@tt5NPtjM3899L0b?-ky;s?m)veyU^grK`uX&PX zTe4-<8GoMN)4TW1ocYb%`{vB=l)Qps&AS>U7wb8GdcQR^HJXsJkU8WeE!S%n&6^=( zR91-FH{V+)tJF&o83{OtOyjh@%ND2?fRZSXCp$DuEqzN@k5s%2mtDP3y;BqQOgANo zYCh>VulBc;? zA&$dBgN$cvQ&Wo}Cy{G^<}tbIvU$dX(o2mgLV^Q~*LHzkldN)}JRegsYqWYe<1r3t zjqFPo&sCx`*(lOr10i0lWEis2M_Voo0!>OdXCBFwB@1WCMKgG|w8=p=Xb4dfGC=Q- zi0}~e8-kZTp^M*Ku||pGV)@TIZZsbE7!9=3-lQ* zN>uJpZ~rX0{>lZ$V>&~}{_&^(EiWlyc;~XE5){}T_2R1DjXWd%{`{LdAN*wQtcf}wL~}5E$g1_*j0ZB& z95Ci7(QwvRESjU<$jRyv-(ENMvj)NQ{oikXpvMx;bIaSyQ{H}mgS_|Q770)zvv{3f zdoz#96phw^;f+}zM}^CSfBKh1sv(4H;kg~D^HP7x%ymI37x|$}RZgN!F!%VK!vJXQ zCL%Yq8=mgof5b$xxJjRl{wMlQy*$I2vlv)-dUgzP>e{aTY!Mj92srCt*6>b2??D$s z2V$)XS;%@2Pg~dZAL~KZxU41bzWZ*Y7qCve@4owtQSFz%{AF_=qpRSN3%SVpoNKri zLKK|>q7}ny)}E|s@4x?kqkAC_a$v~G8W#Nwf*7*ZF;Y%RNillZx4!i)qeE~FdN%9- zt_sf;>R>I8F(^6!bwFsd7Umhii?#e+cim-l8+31UKy**e1vB)JuYUEbMxO!~*5>Fg zte4TxoPs`q4vF4_K8oJU_27j5gU$z`i!ROn1oeY8WGqIu=m=pWAcL{y@`=<(=F$N@HdhrEn4F^;OfRYL&g%$8&BWXwiXkfdQ3#3 zJfjAIcqG>}H5l(`Je>EHWGkutQCM`>b_t*Dgto@B`4;uMCUo`B*Zke&Wp+K6&k=qh z65!=4zge$_gH^@i4*f*%Iy>Y@QvX!_h}hEoe0V z-1DyKBjoj+6R(g+ZST=7>b~afQ9pa|M!0z93dD{z>$w@A<OxL79jF)iI~U|(j$myLVawkRY0bIx&v`LV2~kf0 zN7?~Dh(%;%bAh_Rl0I{s4GK1UD9k-J1i*`V!OJOF#xq_N#(?W-7XqB|q|g@kDszUy zrU@G@6vmDX1^xz0#*l0I8@!O6`<3hIBbz16Tk2+xI2#=)x^aP_0h?FgtR9iH=G zMZdU?WAq1Ek&Oc8^Z{9zYj`m5w}X*0#*XYm@g1ThSV>GsR7z-YppHX><`q4p1ZX>% z9~2m%UUi{L%6Kaomt(x*;+0Hx!p}m+xt}0GA#EUPk0_xsYuW@8x|YzigeIn~imGbG z*T;}*crCNhGbv-FysBQ!kUW&=sBjaS7`Y%k2&c{Sjr^sA`{p>rY?XRs(?{lB)Mp1)2K+LKBji`Y_huJVM%U*G<#bw)V9^@ASsjayA6O|A>ZE$GF8Qdh41J z9ujQE$xFwT=m?ovIcBWtm7v9|nULIsQl})uMMy6FYe!$7+7ITqs?xz$L*gSW1Zn6=Nkq+VFS>vQ= z`_6M41Dm3fQZ?$)P^K#1$fS59Bj39go`F1T7R;Gy=CtcE``IEem=NH;a4qOEkh82` zSyQq;M~6gj!qLU(7j@GnI6D}l2V<;?5i7ExAJ7kU7xZx2L09D(uBU!< zbD}SGg(3PRIuvyv2ZizA8Yco4dK6?fedm1IWganC_)Nc$lR_Je-JN&dY32)f@S;EH zKF&UXIe8fUH#>g3_<6MJ)2`hn>~)M1uAmp}<8Q)jk5)4Iv8}JjvC8A-)Tg$;ZnOsS zJO}AH-b(yJ@H%&Si>{wC%W`F_hOj0qc6(8OYMtw>t-1=2%{h}sq{&Yg-DOVaH#9`+ z-g*46)F{dAn%lX(J{?;f;6N;wm z^?artryvW;9C=nd7gzF_HX#5}SW&J~2=Qt~=|UOC%M-;5g^dFFh3u}y={N74`|6yB zVuzAUJ&=YFb!-Aayc3eysoyytC6#<&5PT?(SX?8cQ@{h+Xwy|Or9a@y_w)tD7+hS> z;aZ4k&T*7d`c61*+JoGMEJUeA*~S{34GgEC7(4QE9{q5|-Z_tV2NT{YDB2Ldw8wZd zf3P^mBi6Mz=evH#XRbkJ=AP3h<^nIqiN1p!;hXu6!dP;?GhfV?C;r2_9qV~EX^;i^ zkcTm5jM$9nszA)*4UQ22=b^;2QNj70+hL=IjTy!Z`N5yf0p59W9NFm){RUe$nVh-A zeS}=R2(RZ9!tJ>lB{7UbYHfE^1b8)THb#8GTW6V6oK!Wxte9wL4%!$tR z(WWzJJ0I)(w<8;4%Y8zfe8+PR0-pKD1{5Cb5bI#X7%}!>K!5Otzzc(Z@Hh7o^*D2q zHhB(Fms2>8b~+a@!n>XJz?Ey*Gy`YkIn#5qb6tb=Z>7*Wh|Z=-r4q$XQJ59$_P;8W zEJLC5Rf4yxLYVD~rcadB8lJXH!@-s*F+*PIR!=l1g}DYeUW@=AC**+wQ=@(FEd-c{cPzWg zgmA~Bm3hE?CZ}$g5|SNDo7sqD4nSN}crM}z55edZj1)M>6dgduH@~n*R<7A>#+Wf> zjPYV+W7U?vLjbauXGE9f#4XF1LIiuM_Z5X_7^wru&0Ja8{@mob#J%kA*S@jN?-S$h z7`=3^pL^Zpc-Fk&g!5)Yo97)-5*$x#9@FjK*zH&IeC&?#3FI};FucY(Z^l7RLz-iF z6sATKJbOBq_UmiSbGGv_`?p14FeAWy*?Db?p)fCJeeU#+btvm>42+$1J-P)3jL!Pk z^>^L5&Uxn;YfER%P1}wh!8xoA(7%ZWgf}dObun_V4n?ownZR{tYKQAs6QLKdhNmvB zqtGwv>|C7sSP!$#b}cC>DJQW&4)hxIC3JYAdZFVZ69%8?(o2^vH98_jr07}bDG<=; zG3aAv4W!RC3Vn0-3?Pu%0{~0va|*a~og+80I`TT_@Q(h&c%XAa98=Ic!J8NLVDQVe zPW|-9(FM%~F8^^pr%xNEtX%GFAy3Be}zoV~=LFa;ej0t4Bqn|J)$OD-@ z97{}Kc$e!OJdQs#=}H3$-pG*4uOHIf*;!kqK}pq6-#|l3M{D?Q$X7~iP=w@FwHt@< zHzaduZIzO?U8lX^oUY2DvK-@K>>T4a{?e3ba`%iI z|44^2U+RDSu1ENXs@xMLF?fU_%e}O3aY0cg#JF?b>E1EIZPI(Wv$lf~B){)#b9O6P z9w?V+nC>}B?B^&Uy|?J7k+(|2cT>DHd^sM}wpav)8Un>iJU{%W^}62>DvRc)>N?$9 zpVdRHm&2<+9d}WNSKn=KEdmw+i$I?v;8@(E;NiXLkSi!PC{Yk|5NVE8up=PK8;Tgj z7=_Jt6jziS3W}4XESoR%Ka@1ir)~&O6iO5&ilhAT?pm-SPEAd9{i?%flvI>Xycdxb zB^YuQB9XdLN>SJ#jva*;IZ=R7-nt5(?I?Y&4LM34N+e#7oP(kY;fu%h=RWs2)0XQs zYyxmDeMa%as@=7qtg@*qu43jW6UXtUJB#L7&0b|x8rRL9vC{{2~T0( zg9n9kyHhw1c`#~l<{S5dGj`NTNO|Tno*DFy5Oi!{Fo(dNO&sPl@=yoY@|kOy7hRRk z^NPP2C+5dBP?k z8$Vpnm=N-gXAswd9pk~dP65--{d0~v?_A5=q8>t@JL3g0j!^^c;;}H?3Iyp0_1x^L zM5xzYwtCayE$kH6k%rhM$0%DccyNL-BrP7)5Zw?U!Ajl`1{R{0xeA%gyE9(AJD;64 zcqb?F&V8B2n{<}u$_-OO2d}_Q+uAo9UC-_K8RkX}M|7hnGz70|PZLJmAzR4f3JJr! z3=UE+3U5!DtKpV?)Z@EV$s4@3ee_s(r%<=e%`N760vSozWEI-vXeFQT^hp_-fAz$M z;t7Dr#fq0JCPFwvj!(!KWkQ%UN9z@bTE(A` z-^gXlg+%}Y@TR2;&E_J;65BMFFYnC3_R>DUjHeE(@rHGtcr9J8AZwgo)zS4h8SmVF|V9w2cK*8 zxpn5EMupmLm=es8+4Uz3Ys@uTAqGPjNbqcCM$-Szg}r@bv&AAX)DYkvf(%5Lb=H6A z47^-*UhZ3-$9%tV;lh)Vn;dryh(|F#=}1s2}p$*+W53qJ5`+KBK47PUpgUu(JU;^|8O=j3I`rM~@yg`xwk2 z>H^=+7&>+1$?b&TcJzMqGxWOQTFC9}r=Go?%hB?Dd4BIo^EIPm$YdpwD;v}pO8=e~ zGg{_q&gXyZdRs~7Vl&_HrVjTFGS@i&wP+}Aq9paC5Y8JC+O;sp=O;}TU-iuHs(9*r zryq41)|=l`ytkV*r*&~{#mUnl(I<@ ze1&79?dFDNdFRN+lSKGcX>;WE372aKa&>gbZ#R<1E4;p?<9^;5o3jTk0)qg7#zysi zK3t+H!0P1VH&2pjlW^v-2E2n{=*}qOsT#={Pqzmx0u}*_fJLAK0&Q$obzC%dINs?G z6gfOs@sK5_GfE1?FvKjRE(A&EkWDO*$)g0BgOY=yh-EI`nGkI#XAo8pI1Ukq@&s8& zdz6%v6muQz;0cKmOP(XhG1r13hGN?Jg|g{bF*`~lvQiI3Byym*LL^cLeQ=ac+C+BF z=}tk(gBYV8LKN@XwaX|W^pVX2F34YD9I@1c#wkwXb6{g9r_H~K{%y26<`Mca%Kn+I%yfEP+Q zZE+2CbuP?V#*^!52Q1m7aQeV?5X7|0d|?xZ{!%CXXI}6(ZG$tLMp)M~>hcs5!n z+!Ne0^owh0k83%f_Sl4AJh=D3gE@s~E^~waBO918mw0i#vys)gU#I( zN)UHdVxuDrc}v*oBF#ZeDCi@{^346@+%)bmTx>dlhnHzZ(s=3_!z=YdaFlX zjS{PP>$jQkuz2X5tuP+d)irw0H|n_S@0q?z^?8Ys*`VKbo@<@!<2=Y5NS7@;_nEw? zgznz7eXnfXwnt^u9FR($LoSk=no!~7P0P}p(R_!zt_1?>kcK2De{YkL(S+88yy7#t zoGHjmc;4cY3KM=eP>DwJsTLHKn6TmGW`)!rnH=X*HQQH82_#Dt&-w=p|RwutNBp<|cdttkY_d3rLo}U;( z1T(>qb0Lm$oG)17&y?{&*uNdWU?Q$ryAV2efIoDkrd*jvv<){N*QtUp-;vL3{f zm$f|4bElxcq@<*rTu-wGMF!T{tc}mMR^)!9Ui2l}V{MGSh%UpLnZnu|y^?i1Yg^hx z?_q6A{SdvZtsyvByHXeOvR3U}sFN2u4fU~pMqczLzT@TSY^v4=j(dUsJEZDEWh!fp`^)$K??ScWi3OXBm0`!4)UwrXJqx;Z* zjMTx0>yZmR6djAb5Z31u+CVQyPWq2dN#D@r!I-va1Nr#O-UE6NeS+L)tkJI+Z=zRm zeOKeh`P2zcU_f28i+m1t)P*iXTh2ZPZ9Dn{V?aG%fV`ZGeur*_p2c~L1-eg9^cMrf z8UE|7`{N8vbU4J=p6f%M(AEvjr+VdAs>kjj4Qb7bFxp3ykS2__L%u?!5>DDnN!=PH zvB|d#`AnE<@-aIx3r$ncj-~6u9 zqBQGoTY%=Lo}W0`cs3I%{9|J;(NNb!4s#@FZ;0PO!i3j0*Gr@77a=~Z`P5L%!_(xr z?n0cif2B_ttt{pJfo_2Q~k7PYbyB^}On-jQ0HDi$GaL zojmu_E=f}F=Z{@CLHEmiSo+#B$Pwtc>oUkYW5u@!SOhEreS-kMe!Mg^V^e{?ow~?A zlN@-mqRgQD;6;nl!V6^wh0HZO_%8X+vfwG%*S#3ag`x zLwF)HvZ1VT{A>k9lX_7gA+@;1$pMATD6c5&Y!vjgP#20d7Te30FE@13#kj{L|(osgJd zf}G&O9APei6^ibe3fEk6$t5NP95OT4kP#d>hJsBW*^EH(MxjO_VdKH+muvm>opFLp zW~>>L6rEdaLa=cFJ`2^GfO*-uu<7#Vn{S%A%RHog<}v+bqk?;aO$6o_a^hXhoT87^ z%Q?u$F}!Knv|%126LoM7@6K4`waz$j9erf18B30HjEx!OVIv1CcgSF_?~Ov6OO`A# zp3Q`Fqi#rK!r9R$3il;rN$%|V^XHrUmhZuxdB|qUO7(W&o`ala4l!OmnM<6D_W*Mh ze3|>;$7T@T%H8QNctOH*4ReS&-@ZA4v|`4dx**aSGv+88GTf(*7dh=PN2rHq6ZaE_ z0u=CIP7*?&4J-=dk)rd;wHTX?BrbSsxIagA(h8#&S}VTLhZ6S1X)B*SIA@JC@rrt;dFEJ zkDL6d5I|#X}?Gqm_J5)11=W3wT9lDpou99#V30 zj?A1qPL6BN=ZzXhoC2{qQZYK6$2l_L8H-`S`YpQ+;Z&gnC^@(})}4FOk(c*Y^~h+} zjjy)0zV&Eq({@{Qt{H~VRiFX>nK384a^S(rfXZ4E;orXKZ>(Ythf zLxZQtj45NyoMC=K^5q>b(wx!pGD+uXSKjJAS_e7E3B!I!$N0EjL+E-+KtQ5tdw@f9 zTJhYy{KeD9@$5hCX}xm5-P2nNDXTkYsSYZkk7mJ z9a3YIW^><-9+j*{3F`Hy&!x-e&yo+bkv(p7s*KRQ(}mhMy+fU@c6cY_`N>TKv!)Q3!*gARxej7K$l9V=Crp+2r< zEWm){6vlAp&Yk9Z*6jR^UI)fRfnt2f>&)NiskDv02hQ{bos=|E^pW;iw}L}w43P^$ z9C=CkgMQ-79nR;)++qI$jFH8;2Avh%9(lNquY?mhkffyZt>Z2=fAaerq;*1gZ&oj2@-cTV z$7{=EZ*i8)(sm)A{nbkvVtHNe9-UVooeklcb+}mO+nz$Ny>>=)Gvj&5?qq|0aQB$4SJc zi&fD%^MY`o_s7a-5wHjhVg%G1uCa29G*#`=Jy+eZ(ylZG&XYEej`@|&)I$_G6cQ8^ zlnN9w2pyDA6hIVDzJq8+xxx~eYanSHA_e6NCCE`?IS)mU^B`JMR0%`jL|Ne+6eI{m z6fLZFQP?2hxCTWKB@pGtQE;fg^NV^Qd1wnI(5VMy7Y}8G^P}Wc&7b+QvJQV8Z z9NMN16g7w@`VS^3tn|^T8%5Aj-1rU|AahZc&!r$cb)wutY9beX=Y>MbJER$MgFkq2 zzH6KGmHx3|LO&ov9paOBaApi4N~wcQ2}pXUU&swcuH#I7j5A6!pB=)L%@LG)USLPv zvjFB_8PQ8o=bB4Ou zNI;nfALb+DNnt!Yj}dc|ejpbcFwB2KwNn=xEBsBNFJQr(MPX+iLejE`Vg@{i}OFOw~!8u^f_1rJCNgJJIH-hPZwB_7O+*`~6UW^O4 zuz5k>xUVn{K_))45rG_pAcyz|3vi_l?gz$_P~r65DPTpNj0^431{m`k;9BM|$C(e{ z2lmK!FjMTU#_gUiKPksxOg1wS4#X>;CTx1WUG(ta!H>ujJ}FLwGEjH^bz3 zh8!jra-x!*G3xp3glBCJ{j52Y)#DF*@cx@VY3#`y#oB#Sm}hZH!ay2BR8t@`AX|AS z?=fD(vo%z8Vtllzzo}V`v~;5*LE9u3H1(6G6z|g!QQ?NzhK#9J?@|cZIWs0ox{@P= zyQL3!Uvmwf!^o^3^?BH6#y?4O_eLq%f;{A7C3iDc{bSNnWXkw-L+TKg8;?|XcMpN+ zh2$ojc0#-wIq6(-EzBu$H9N#J9@ggLVIWPVatvwD@(lpa(5 zNApZ4%og|4gcIc4-YShXMN*!JR;s9{Bd zKIb%dxW4KeG!2J(yyHDj$YnfPFKE+J}Nxa zc&y?{&HT7{mi{KFCvJBL%-$NO-l_*r*ZRsr4UeyejmZ~FKt#_smwPIUo8GStRryku z^`T;>UdXz!doG20nq16#4jeVl8jNT5XKDnSoP1fVB(txs`|5Okl|C{_#^`;>*l~Z8 z)0+_eCQ^ZJRFBb+{1eo$0Pkq?`>Jz?=Vqjy<2(yp<)eJmyPfp5ceQrWXN9%JW zBsf3`>~fPsewxnX@fzBn=O)n{=nMUIo;$2BtF*100{uNzqMCVmu7wTqlB`V{-o#rX z)2~nx`cw;nR?X{KnZH%)%CaOZagq22C!K3^_PPOvK>3;vrE14ERfU@+^6Kj(aQuW* zBxIciG0J+ZtHOMQ>|>42x}Ei2cMEF~NJ|VOS);PHV-3#w^M)I4FrK}%kN$O!odORz>~T}>P4S73q;;=2AYzhzoCvQRhr8}H~}7}Fvz{pC0^ zVCc*Go`_-KLjUMLSYc4hTAZ~#?QxZ125-#`pkHuyU;c| z9oI94U=1Fe&sZ~#j2T!lZmxy?f)Da=J-P^bA^kvR>SA1-`q1V04jml%(fiTQ_)MR{ z9~_Vm{f6_=RhSQ4gZ%UfV`thVlzC4J^8<`%5A5k@=R$kn&pBY*xm2qW*OpvOomQM9 zBO?=JO2QcN_B^#7=eoLvI$5)4qqqlYnj}pd;yN#!b58e8ZsIvg2yZJmX!0iWTj;0W z%%$~}>iL{^y335$bYV@2A%|JV?<>g?ys4Yjpp|ggkgk-5mL}st-K558l&lJk$fkXP z_&r>nXS}D)ffN4`%9tob0HxmSv6E98UhZSvmHtx~FCz-iC5RI3N}VU@L^xly9@ zTKe>klFGY~N7q9U`W<~Ksx3Fi@IuC0yFv-$)j7K)U-MRb>DmL5J6FSr;~~APNFya_ z|H6HPC9_<0L2Z{bLcVHjSE2c(_bHKGukGRC%y*E=2Q)WzqxLsay}NY~WPRQqLxLl7 z2syJ8bSS69Hw=ce-CzslD2O$D^6-Bc0;3f(3Q5`*b zf{cnt7_hy_E2xs+KekSPZ=2-D|8KGW9$Wrzzz$Xq1#{B}aL1=@4=d=4(<|4nm95ua zA<;K3ll1$3CT?f=vFrnMj<#iJ0Sl%ssU^2#gb9fg?<2U`XQ0)%E~1AxsMlySnpVS$gg0hOF7U zU!+n==7zKW-&hnVvvV?cBIvwQDIS=K7aT=^3T z2#t}%Nmrj!#cXxts8P;KZ@jODH_>v{Wpm}t6>H@EwOizKcibR&3=fx5uU`54HIzIa z`555@1)iHRCwSo*xBvY|#4kKe(ii_kyaP@RbKYEEAvv2Lm(s)UOX}PoNl5&>;R5xL z@8^+U{ao_?`joUPDVXu_V-kJyZKw1ZV)nV`o-^d)DF^in?oGT%i4?_J9@4P81tOG) zJVf2W6PXvrCd0YlY5c?!PZ)yrJKy{-z>0$LfMCQa#4u_s%`#*LCc-uv_3 z7tq!?pb*$}Y>)ilwLeH~aHM>B{%vyk=-C5GK1Z0U>IPZ9;*i|`;3`Q^i9Ls5a_J!fj+>W3`xmm2iE8i&XCYpN%L;Y;6Z?}a)g7!dY*B_(jNqBq(O8zW0q0m6ul@BLWbO^;O5EK|Oau zlE(KL`HWEBf$CuwJxLlW4oRJQVEY9}pHbGX4&u2*=wc{%3bD367hg&z+B=w+un(Y?m$|0+7y5`X9(M zSFhhQHH&8}-md6%gw7_HF~+BzJzmdL$51PJLlCfR}J^V6cpeO_n_choz-XQ?_|^f8LN&nGn>sjbAKy zDR0VCT}C~MZRtG(2)!K>5UyU;hvogOZ9OBORSZIJHb2)u1VF&8rBZxL9@A%gt_f}I6*N&i)eG9q?ZVq`x@lw$zmR0{ z4NXzvr%Y-q@-#$ad#JgyjSk_1Q#Iddgoa(UWvC<2CXIC!Ql()El>stj9doBIig(}$ z3DZ!_9zMZRt?ffn+A=f|V2)u3c=J%+g@PK&xM@r22n=~(Y z;Ca<&FCLx>*GVjZAyLr(y5~vY%>PstyaLqj_gsdi4 zv8RTZ_R^5jw)8FnVA`V2FKaa1H(|hgQra}_Pot)1YAUXhw3uXBFlkm#F1P1e1TF{y zL_{eltd@6HW@@^udbw_ChFpEcx#dv5AOhM-dj$deYtSn&+Da?}7J*@ffNt;x#C=@q zj=!Nf)^>;!5U7?XLx*s0$eC6>}VlOoDBB?DoC{@Kr#6L7a+%@0V*$O#7C)&{4 zXM<@-9B*lAG~{oK=H(5F(|n7bU58V=P@Tm))jx8qlplFl=Vz?=1e|+l`U@qat@r#8 zARm2AX|@`{6sp02<~LO@`@Sqha_|jJ(ufeVq^UYvS~SO@$2sOWhwK`!Ih>s`XQvwrfpUPu?Dn}khuRdW2$8i|>7hlXb5cgby7U}!tp`v~CCi5E2ag~`JVISR4d z-9ql(1q&8*_w*rs9t0(?GZjLW52V7__ayZCK$87zvOqkOvzphr6w-zf?TdX2a#i;w z`HZ|XGbYRExD?rzy^FUmKy^){Y}%S5TX*D1d~C4XcGILjm%%o8;SuQG zFUW|)l<&9@+~DRjw)Hvear1Z)L|>CMZh9p z5g0ZIxV6-XclA5!1zoC}E&k#ceXV#0P0~EfK7EFPo5nN_h)6fNqTAX$q_MWtkl}qM zfNf}CBcLP@VUeqjXR2pvq#7&CRxiY$K9!lAufcKCl@!z{6xlnv?WNzRLfXd81A#UT zyIn5T#Ye>_I8j2>AjbV1$=~x3LPLHhN#y8jG}OCDW#K{VZGS#s_QJI-O?6U|rJ?tm znqVuXA~Mxe2|^ z{n6K{!hF4bp64Dt-yV7Tg&Hqt->^|qQE*V28fwqCN_+hfL7=IzMoM$GYv^rniA=vr z{K7|>wMbug&o3lVBGRtZ`MFPuk8JGg=52cy5CNVyC0U!K&COe)#$2u5&l7cy^?mR( zyr2W4#!6`N0x8zKl9h#rE?}>1)n`JWrLj(m^ggfDypyq$Zqt-RM18W}&u7BS9y>b% zc#7h=8xj&?JU$`bY_SLoX9Vi&>m@5IOGq6yabo}ELG{#p;d91KlVuB*ihrZK)D~8# zA((Zv7_9TMO^H3o=SCgJgs^B?ICY)``JX+q)?n?gm3#0YP*v3+Z@hg#_8lye3FD&W zqM1qJt$F8d8F~m<0($6;sqK+Pz#`B`2;dDDn*M$DhSt2Gtu^9R^|ttyJ}w^h8gj^b zLif=q*k&vO76FSuZy?YnZq3D-_xTa=DSKMIpUahO@s*(DyTvOYL-*MGW6A8Mo`(tQ z1wBvds!F8bz)B^dt9zrfwvIuJKvP4F6zp56-l>%mGx7HJT+D_D>ibT4`G+e3ex-z^ zUTO&OqQmR^UI^P_uOmQC?@G<_+tk=-#uiU^chCOwM)33vmY~=v;^`YM$1_(;RS_h# zHTLZFfj>iq&6?A*SPc^JLJx|bq|csPH9|<=8zh`TM0;qEB0zZj;NW0MPfwS+x;ojm zZJRV}zH(bE0>crVJ4EpS06+jqL_t&m?tw#x4wdIX5}kY89O+jr%Q zyRHS6EKHZki0-H1IDO6b$UsEEY7GN%RIE%E0gJ$(LBP{LRU*beB&B=r)%FiazQAy|$H7KfY3E zr5@03?tPnE*L4isH57M9{7gxn`!&hg{Fofw^qk4J8$EiyczPk1^*(dOYp8!N$=V|2 z`FqvzIYd(De_Khp$^A+UstdLnNyJIg>@O-QHBpYOd(aS<;Ym{^G?ElIPuf!Wba*e5$qAS_C>EKz`epm>8Kmcdo2ny;|OX z|9$cI_m>$nW(awrZLtUpbp*)yoSB&^d3kx_diPygscwpD zPaB2^G&Z)#zh2xax%pM{*-uWFc^4(=Z+?Fm{SQO%`=QI0fbIv-ZIc!Oi@;Dqz)ieD zbfb05eNuJgL20PiENxBshQ#n_I4I5jGnI(GNm{%Thf=3)Un~L^0fT^>@xfBx*>+zn zb;xo)+hFtq<72ev~tRWAZrM|jY z%5ru{edTcpPrE{*2)iAYq8^Ce{oZXiHxHegVG=&-GDA2Q?0m+AN^Wg#kl+#OVd3Sg z^V3t0v7ykv8>#&c)_@bBP1u}-WNfOhlA_GDQcu2l@si}(|D|EK``f_bBfxkL`UWRS z{M1kDT=JCCBP%3l*P9YEdVwLK-PIeH?+Mv$%lRO1VovbvX=;?Ryj>>jIl1OzCfp)n zDGU19=;eI+X0IK{2tYzdM{8)_ZhsSU0~tdr z+u%U}&p+~=^2*K4m7JU$Y1F7A@Jb902$CDrYnBk)&#Zh!sxk|t zzN|_DVk5-U&s%?cb!}&Lkp3R3lkK;uw{u%-oAFq$EiBg*fvqxq)C5^Dex{_wr&!PD zlStVQ{f$6wex($Z)X3xsF*0>RtoZp_gR}k~lMC6tC7>@Py0&hMfJIkVh|bfn-0Bfsw?o=AT(>P20gJ%kMWC+LODbA@q^Yfaa~q<$ zNxlCXT0PBojcp#%(B>)i`rf23O1gKcqTT0u>XMK*l`^2h6 z|03~^xk-FNrt44gvLogc#?W4_FstH zp*&6ct8e66ttrixnz9_TLCJiCd>%3O2JsC^(7nF?-TNk z!oor+E-seva83E3$M6od#Ue1+5y0cQq@={W_{`iNH*TDa968d2-S+exI64Se2L~z% zJ^uDeWHm{Ncc}J9pE|gY35All#)|7PpZxA`y=R|MF5BXeIQYnpP8qG@$_hNj* zsM{S!(|1u2v`Ix0$n3Ou2AowiQ=IIhPQgnK}0uJ?Ud%4Oli~buC`bNECPcYfuhDx z*-#%Rdm5soN{MI+v~p{ko4&iB_$-bsm$FZ4fVCskh4cU6B>*+!hWrTB^&6l;q6O`KctQdi~ha{|Mkkh{v!#@YMJwLN}0OBx2NJ@d=7EuYpYBdIdyE zu;zvK367W2!|&>|aF?_waavJyto=EtQ3Gu0IRa)*sIik9&mNCJ@d=NUu%vksk}%6e z;26krqUXJ}R}54Hn1?|@LE`7yYv)U19EsS_>G1JAqcM6JYdYQlO{-Pj7TTAUao#2w7JH}DU)ScwW6Nu86np1iHlN<9}>ersrTKzp>2a^pckpCQoBq$jbxFKq|16=2iNi+VeKRqEkk zJ$$70brz*8{v@YDPWR&tBgi)Apzjq68uvHhJQw#&#Fj?|e@aUon~Y@A-Vt$!a3hgF z-Z9k~30705vF&$oSI>e?0{ih3rVA#{2&FTU_>AL}%pA+wh&7L|!)cdD`(KOCt6+=# z;i15&h%2`jLED^PY_jN`&meuOggkPF#U#a|tYpT2{ z5Duz|B9SjF{p_<6xL*F#ZeR5xL~dNZEj-9NyRDnms{nrmM_bBna2v}%{H?Mw9f|Z& zQI=HkowFmS`HaoPAK>9>nwez{`RTU?qsR;9L}hy$>#QEy>-uDL_6bXY|ywE%^EO z--JOYUo{CxQAbEQ%;tnLwr5JkRnFb>ZxW63?p?0NJK$xKn6)oZg_4eJt+}DuC~*5# z`b5+^i&${qBmQT#@_<8m{=Y=c!6E z(#2TCpu3|Y8v3P{oMm?D};}0fBgMXh?rRMIKlIhvcC9NBS%s6u;x7I zuP?hxf+I-^=J;f6FgQKa&f++T(<;X775?U49lkFm&c3K8NmVA^DcPlvQanUQL*0{_lnXwqnkAFIAjK>>nPB?JCuWXmL-0zq=KMR3{&Rs@2 z6Tbg7<>Zz6ks6oO0^X_8X@#0V)b+7?6_Y(t!tzfIRJT&(xo=~Fr+px1u_3&H*c1pbjez57SX|38nN zUK+1&i|x<@^$X%()YC#v51V}^M^zJWg~b?fqcDH`%Wkm&D2etN12*>fi{<|Fmjd{} z?|lIw&p$5;gHrH-=)u^Mh0W*LTLo_J>A#a{(1`a2!;`V=Nb4Dip~|NjkX@s7f&(M9 z=p@Sr!UrBhJ0w5#3tp*ACgDWnHzO<{q1 z30bM9{=2Z)75n=up*->!Bxy<#FF`&Nlb|QVJog^*Ov}YNUg^1$Dti`n!DO&@9mk)A z4xJtwn z=@DVEj~FdTc~ZLoPAJwms!oHna6D}K7i7BFf)AB zi&$GtwV{D-AyTx;`bs|)tAlC}&9fiNYnS$Y)+%Z$j5kEeaIn8>D-R{k4xNuX_~-oo zYast?h`;;gj&e~;sUeADhI19rv)O%)J-Y%1Jv^Klvb#fUSQukRu&od_C-HZi{%a3( zK39J?F*k{0>ofcNi~M0}VOf%8j+i+N3ge+ilEidGoiNd7bFY8=Vf}U4fF{LFzBoAk zd)t3~^nVTYFAEe^lRLY+r>R8iY#yXXJM{m+%a-AJ36XP$!P}Xl!zKziiXL=xo6-J{ z!CsToG~(l-FNT(;s#DNfl%Ja$=2o7Y8CRi;minM03#rf^f5$(G|6$_)V+m+1@g3kV z2k(L{+1eEf>h@c_DIU>IywPsyn&#y_XnyjVU>opk%-a5cXO8Uyzyd&)*qs~E-fb{r zRuWh|l(}Bb@>4eRSIg5t&~3EQ+;gtkqyo0LIkNMQ|7*|xpF2Vc%$l5L%5n=>maRK~ zow67>LPaYpqb(;_aSi|{YJnTw3AS%kmmA&xa{w#sxXhc~Ea#{lZ7V>y+pr>cYh0a! z`yo@Bs0<6YQAR?u8K){oTxPTtET2Rc)AoPelS)Ty&tJXd73^p?Xxtx6NF6|7$j~O=eb`JpW3$4$pKx#TJuS^RG5vYgM@k%CVXz z_xV{DYxy@6S_W@r0^D+p|D3=2zmhqd_G zh1t%3SiA<`SE+NMN2gz%V+ek?DkXP-Ke<1{(HsPfb9*XP1aI(@nnj0u!N{gJ()|N4 zgH!X;9?Sf=ZIMag7BetCaCCl5a_HEBQq0U@Eu2-4rJdD+!v?=u9VtgQT=gZp^@J`X=Xzmd5K zzln*sV@0>J$|XdxjQ;u7i$Jw6c1wK4^UR7JQyMW+R*d1B!TrTQDwOHy=-6dsv>|d2 z3R=dmzE^!qkBxmQYWhALx6caxI^f~#%N;p;x%}>in;Q@X1A{}sqw({*=zI{ie4_2= zmq5!adF@U-p%4*uzmU}*6Kdk#URD?uq$smKEN^LKf|yD2$Gkv-PkDLA>SN$hxm76n)25!~LO)Zbj>^{L z^6%e=2m9u$J2rPF9kA8Be>zE}BywjDk94bG!p$bI8UL6bf1U4;vlcz=CT%rL?1==zNT#fzxXJPECk5D4anY;+P+n$7hx8%5OyYW5IKiX|kJ zH~>J878^{hfjj)oTe~9;T@Z68jx-c9X{5y9N~L;1uxTlzaxrA;7ii%}Ddn7WV5y*? zA$b%P7(LxIT~n-C)fQA6&n!8ge7F@sc*RV=J>Y}nV3&GcdCn&2?3xVCj!qgnVn)}q za2(yD&)I`iH_QhPeU%TEz=LExJX!>8GLkHM%R!jXK%^ja_;l7DdAxvPg=kjRZakG(LPL@_ zjzbyltq$j~yGE~GmEM=Ulw}ZAR`6dp5fNZp68qq-`SN8|NKH_<+cY58FJx-UXycRl zl!JpyYVR((86AlXXTDW0Ol7^jK!r1^x*PSxWm*U7Scjnb&HdMn%>A#^qacS~snQo+~~<$GBPvfOeby#%E{GD(n^a<>mT`Z ztco~A&UjY%OgsXe$r?g)p3c>}7P6Shz4*Xq3LW^|E2R?R$SWNH`KsFXu7?O`{&}_Z zaMONrQ0VP^=B>(;Y5xP5V08z&0D`=1MfSBS@HBz;>1@;L)!VoM$cMi5^^3ssFGE50#9nY8WgOE$J1Y1amksz*d?*QL5KF)g9G#U7ntLu#rd zrV(*#IkHznS_tiJ^q~I1tU%wF7fEnxDs2+6Vff!Q^Fy}6%( z(ZcsX#KtG9pMGX4cHo5#tMz?}`a|*p%HidI`d#j0;P2n}J|tNVlF74n|FC~LeXA7X zN=a#NKoKtMe<-ewvp$e0VLg=`gFpzsKX=IvT5P}UZ$)m{6sAY~Gz;fy5K=Io_lzf? zS;k|$tGMicWG`p7Ln*>6ox^W!ZQWn<`Zi2=om?M0|M6oFeisyKkoX3^nz!-rJC~H`ziS@gbMvxf}`t^D91P7^2A`AP471#P22Im}Qxl#@!rBp5ij6lKH4-k`17$-%)INe;CE?^?{HWqb?;IU8MV3n*(=dqPh zA?8VB8X*_bGSCsf1{_Mu)z_cU(D*ES&es{2>Sez^G%4~;1h{9x9{e`=eHR`y-sQz+ zLrjom5%<{AVSL17-;_tI)oJ+|dh+~890~A9UrYHC>RAY0brfi}%lHC#Dw$b(YC_~Y z`a3+wY1-AZGI`e8hNa#~FGYuXeEL%4b5)hKRG~D!_3Pp0@wD|m4kT>20U2Bgm+ZJ=o`Q)&+p*4Lc7&p& zV1Ip}G#75>@84cwe$EA84!Kc0BqL3DRpUOuls{FbF394e>Y|-GY|d}6(xA=ZC+A8Q?~!33?0yd5Nu-sIzJ4MV6;)QXot+(#;r=U0yyLHi zC8MGrP*TiuSP>k@d9U5>5#VxrXx-Z>PAxq>@m|qo_dhkyURGJY-MH+RY`3jiryaiJ z!IrKH(Qmpv9V{*LSaMR(6U8cZc6|>6*ApZ+VtK2Q4MKN~w`g1&OEi+Sy}i9LM{j-V zSemU0j|O{0K}WDQ>tAp(U~7w3wdy=L0_-3<<|pJ-id#K+rMGj1bNu}Jm<6yp~F!at9YYsDimbb zLfp1Q{>k<_6PpB|9xsglM{q3koBa3}aawH(R*GYvKbMe%PNb#1*$_6k$o)VYu+fV0 z$fS|?e?LpV>-Fa*|1Pe^#|rdT0N6#7FUDjrm-n&Yb7gk(myjlP|9x0l@3bnKl%pkY z?cjjJ)Ee|*jxQ1zSt0E&X5iu%o`PEo2|D03zZrjeDl{hAdvH3-XK0|fMDtR>v9h2e zr0?lmV`$O}0dTV7n-8XPb|mX0B`SHYB{UOqH81O9rX{7B)0a6edf+EXRl^3KgR!hz zU!3P6S9cS%bEavLUX$H%KPn>+GQU?O8{7i7-f--k594v%ZVyOt=&^j3bere&gftlW zAIy1biU^te%=NY4&~cyNo{_86J$o3;g9u!j&ZPeohcd0OEXi$O74aPRZN_#!X{!#< zr|z$6++yJ#|7?-B5S9&=XDmflgs`mSG-%sJOZ@ZqKD!$TWVGV7o&>M$1;WBJIb3^D z`O>=8xAx3FjA2xcHA(R_euK9->Dh=r$27~rjQb*|XGiNWkS&QZt(3-wFLSiee$Y9_ zUe4S}uaM8+>XepkIdx=<-gDeryYZRO8X|B+&y-%|JpU21ORl)jX?o^xY9HzgwwTxE ztSIEb5`mGbyiE(>6NQb@JcS$|N%|iCcuEfbJqQVfGcoSDExphe!#mI24a58@A0fse zAZTA%o2xsCRg3zJnMG9(w`#9}t{Ulysyqe2Hd>vU%_j1#2nD|-p=ec`62L)afI_nF z0j0yn=6uq~$BX{tH7GcEe5KL#97l!q@~b3z&$CJh4(G9s7;4|SdnbF%CR=nm(~JFS3ArUG*nLlLeJK`$6}MrUci?W71T4Q1Rxs;@CA1O08gzf4m-aQ>N%$pUoW6AH{yt6ja>N>eJBM zr%L~RT70vopzXnC8|e()V)pq50%4!A^vt2FK+$wJ(xa7YrNN6A9O!D3__cMrQPD7X z_pW%ZuBIz~JTAwgDR(iZ-{L46t+5edum~#AHL{~&BP$>@P{er7)dC7x5;LuvHHBNv zj#4NBx0+3fi-~IqS#H}r!R9j_e?CRwRcq#_t^;3nRGrhPlXqP11SF8(1JJ4@`Rz5u zW;T^w9o~W*Ah}&kb&LkSmp;>}k^N-wWOW6pjC`k!!fF6%wsAV%a;21#kG(45t99He zjt>gLuBxCeuGo5OYhLePKbj?@_2HA)tVL6gQ^6b2G6R>wDM3hS6q7U-n|YUv4ell| z_`iSu{%SiXppv1y5)~OqTfH{` zAAIT1aIA9H@B`+M*W+QG)<2w6{x%7^Ts-njtTwM#hk-EebCAIL9lXyXy%#a$SgyiZ zo6Jn>AT19FLcJNQ5JQZ9$a1w`52ds7JK3IO#CLvh547+kI0Z+Vlu3>|N%7_HKR14}zNuShM4*t5z#N4v`7+eq{*ph$XrV4h{RNQ_^3D?P)}Vi_MTB7wTFX}3azsx1)^{CZ!!ZPLZsW*u0mzWf zw_fO(7B|HF==PvwcQcE%L`?3W`eR-n?<1ebNvp)W{CQb4Wdo_FD2c#*1eQ3Oy`XeY z(jz^+eCFvhF&%lZY>?%z8T3a|g~iu{=E_^YZpb_I1@*Nrq}X`%}Inw%`d94 zjO|$~YJ`)R4V&9Hn!mQm4ePWpHP5t`YH3U2;T)<@RD~mi3(A0t3aBgI7M7L);A;w> zO9O49Af9YeX-q7|VRF_^i4k#agX6U$kCDOh*>gDa>92^17oIa#!6Y=EmWfKgJz*^) zWR6z}(R`|0s!y6-ji-63x)Z%%K)CWDWh#A=1*CGeV!=*R(ZT9F9uuDEPimg9I8(9N z^mLP)pgG@uKyYRd>D0jFQ$eOlEO(!KlD@(87(xa;eK#p5;dF>{U}$e-*|;@a;xy+r zk7b5sn`dSG!WatTIK_7*YCc~tly}~Gc<^rF==Q>CDvAB{GU^4as{hXFh>9K8KD!D6nb#TNaX>FXGM~c0d`4Nfb#*0lm&!9u05{j< zsjIB7KZ4~@o+|hT*S-Vk>c@yog ze9Ttc`E33%iM~*A(em@7-8&r2LaBqMqxi+dLug^*BckRH8Yp`085O?SSxGwSE=83i z0RHs@rl9WRwg^_mRKNYta8VzHvkk>f4&q1hfk-KptTn@{iqulF%Vc!otRmd#xTR-= z!QibmtbqBtUkC&wy~RuijMA*wCQgnRfDy?d*9Fj_#W=@7Z0%@v_bR$K5&~NNBH3$o z9W#)iBZ+zd-E$D1?@WG=Mz%&5im4*aPJyL@VQ!_+QNZA~!A|H1AnEvG zlx+~S)#Z_3dB>Kt8Cxx&ar2Ttm zqJ`*q#BS)tYV@1y%Tru5(InvzAy2{pKhtOk2rIfZ24KYQzF5RVPM?PxT-Hq;7kz?6 z(!^aO%hhdXIfy&hYjqvYa$e{Ab69E>P4gIon<(A3o69}ouwWUg<2AjPM-DSM$zQQt zhY`~5>pc4>4@C0v{*b|dr)%y9z*{8bZ;b`4Vs^VF*&)pCrL7&iBkCY^4@qa>Q0B$h zD?GdAtslIiY$3TEQjZ=B!-LbsIC#CsD-BhSL@lu;$=NBWSgs?+0)F3P(ZvDC745JI zYP!eYm&!bb{TTBk%2M74mlaukMADVWSMLOs_j%{9<-Tm zN<^Myd*@WeQBw2Dd@QuX@We9)g9vfDzbsX@(3NEyrrPA#uyKR zZDZlu<^L){(D>j31A*Eo^ z@I^e1Faqe*LpogAD8~2V9~@Dg9&-E;fDHhL#~rH$3XgV&ZQClfzw_(0fc|p&>bG9-whD-H8&J^^A9ZvE75shYbr}NJe|Js3Jiz)#9moJ9|vzw3egO4QeqzcA5jP0-;{8xQs zEqdCGrsdSf`PvjJ$AYZ~0`@ToG<2zWwR$97YUwHF%qfTa4iJTHPQUOV63Fsg!Tm-Q zcs;T#$I5o1KvNbWa-Sr^Xt@kUWh&1}{Z&B?69Qot4vNfkGK8eU@JoNuR#e(eyj+eT_Gl@%;ftvi;Hq zS;keEk&x>zlbM4Nl$EqmC^01K|DN*j!v5>8Pv^YnF|I6dQP{vnO26!@$P`mCypWP8 zld{)8!>C20h?lQtsENnPM$bWT1$zf&DPJNAe8I#D8V9e!pWpHb5Z?VdwD!?S{1z^Q zesTxP=C$(?thZaUHzSK>C#1+Zo@s42JXheN=Q1YH-dS1GhlmhZ;AQZ82jH;2b-?+k zs}HasU%ayr7Ot_Op%|)^BTOUi1xn!?LIE z&uOQuMVAgkD(r6|WTl(6v!4DFb+su3L|{Seef3vx2q?O)rmJs{Z?3F!`&YM!C7;EY z#SqI$3wyTabNKt4ae}G9>Q@$pp-5eNAhn8rDkUw{3gVA_=`~9-Vlk2^%E78Fr~#3; zB@^>P&Wpth`VzRMgw)5%x+b^&z9&5{>%d!FPEgxsKS0uq6(`X1*tfwdF8N@!^XT3` zGNS9IF<2g83f^aDArdC!xkVM;E|Nj0t$Yn1;N~nV$+aE*Tqgl6VTA}CkDKpZ1UHEIyS)v4gGPn48lb3RBvXQ z+BxE=1tnCGHaAGDseWildG*uvyvMr7k$zVUwt+{8U36fF&_QxNW>%D40yYtU&p`rR zoA1=h79RRfo{=8nu>StX<{ccS{)ap-))$_LTPFNk=GBoTX$tgeyqTxPpOhEOQO0}oEt{eGm7Mq)Q_t=%RcInljDc>ebe!I?FIQAxz3lej< zjvOoBv6B$+j~F5({D7yWL1Qb2Wu(sDe1)6-8r>JVEXb~#;2Sdcf@uYxsS z&nFoEI_R0sb!Z9VT}y!Iw^eoA95Bad7O_O5$#tVpX{TMRluoCu)AS~F>>MWLOpQvCdVKuQ%6Hc|HK z_G~>{h+flo4erlQIjO-X>rs0dUo|ifcFiHs!5*n(mCKfL>b+98EEY3H>1|5s{A<57 zhNNO&Mo-89;$epW15SitwPVaN|8Dwqd=TDLM@-Cf75#Ob_yKCTDBMy$V+eiSLb0@Q zonNuR_U0^pNW@sBGaMTNP}!U+b1a_up7IHrvgV*zKFenQNa4{I(HDkwSG&Mgr0YwE z`EyG00uMu#?@JWVU1*TVeWVRWvaQpChWhF^ti(INzBFPaHHn;H=y14kJ3V3g25#6{ ztdan%-7G7V($D9yM2)^_cW?%JkdjJ&X=3sjkygzHsGqRoH*X0 zu_BwvZcnqx&zl9#>%U|p-(o+1+|P8_G6enei~19Jg?W>CV|khEJ+_BP&en|IAMrRU z+4QJz`?Js~mBpnjEodg?0y#Kv^YS975c1J-2?)N`J@SUM5JZE-k=yc-8a1wOU0fuv zBCLZ{knXh9HHY!Mw^7c0vh*dSK3q=R*~G%}=hF6sDncpi_iY*>-;rdN(;j-GNj@UQL1J%stB!74KW=cReY4 ziHXvUCtMwbzC7+9uvq+;2YV-ZtUZ>k>%4`PC101;jU!9gFpUa{0&Ym+uu90wFC~uw z`4v&4qUuthR!-fl*Pxy!sw_r`l{Z#*vbE?p%(k$~)3+^HmM9uet19n@u;$;Sh=f_u z4Sx6WY|(*;?L&c`p}^$n@gss$@G7B=5Ud+WDO)hN@xDKf#)t-)hicAfAT3lxd@#s* zzBs6BbL_&&C4$aECj{_Y4x9%p`DK2)JF2atqc?b@h|StL-Ogs)&%{-z0Q@v5^z?i@ zKpM9yPI%dlfQYI|)&MPW{ zQ}A@kK+(NF?szf6oa>{sI_?r<^x?24wEiD~TR3@Gr7UVQGBaTBRjXRnPcf@C`|t<$ zk`<%WG+5~Xo7eB$PyZ`{y-$xzMrvH5FJ+*R*lt-rTb$Vf~ZsFGHi0{tE_wln^2d6jwPD@K1L;dM^pV-6j_+M5y6MoTr zTl_^I{k7?Kcb4?wM`iDTOuU%ZJY55 zr>^=fsnxu({R4{=l+u_l_c?gkQJw9Sy_V)`{$%ZOWM^hy*brohSW9E-Ai*kn`HkY@bq z3k)z^$?V=s0nEvt0M6UTsc^F2i!e5hMp~K9I$tXHQ_T*=MoJE&SN=!`_$t+g%B1jo z6}hSxFY|4jS~9RrCu)v)hkG30*)fT~>r+JjmR27|@p&w7Fwlcl-I&|a$}PuMl}mfv z`C*F!3HorQnYFHTnDxWV%keforsRXuQ(|?9lavd*c^s6cl{@;VdO&IDRN920%{M5G zG%^*jb|`PT+R!cRb>)j|Ji6)XL^xi5)P{Mv{-TsdXVpJWmHonQ&c%}La8K5=6^8Ae z^_MCfN^J^#;n;%D0Am%x*o{~0NNFJLri3l|0#lbZM9008y)7)tjWx-hyljbC=m^aZ z$($d>jjBUFyt)dzbwYYgt&VmB&~q!NDv2AqRTW9B9SzUBb}7xDi`6O&bG)>`0kT@0;rb<~4#L`gX4_KG7;Z^N@Ot-e8a+XA!TtujW)(GN5yFbLwK70YYkXe>t)yW{6Hl5keJW9g=1j^7fCSMoWR0 zgKSAO!f669tJb_Yv%zpb``bXL)8>7=KZDY`@y~=76*y$>ec@2=WX4!{Af-S1r(J$g$!Sho&aCN!cAS)YGaoH9 zF`J}|*Zo?|4rmPy)j$Y^N%Ct} zNRWFLG2cTl{!)GhFk|fv_@I=Pk;8qsO72*oaCJcl$Tu`ARmqa9l++&F8Z)|WCfs^b zT|_3>aaPcr5pV=^>V_mG*K82cF10=X>|o|C(7xiTNZ!ZuaN_Mq$n;Et4~Xd#8=LBn zleFB^ds}oX?Pvg0Bu+v~KQUnv7YpR~-J_08cb!>xC$Q3smRW+0PTgc}*xgLFHPLou z9titlqM5-S1s3sMi*F;m8wd7AC@fpg7f$7^uGZB>*{}4|X7d5{{@0FdYe*R`=GbaW zZV{&~F=Mzkf%xs=1D#Q)Tm~z`g*=WmUX>w0vF5q5p?^1)`Q)}4h?s8DvpzQ|m!xyd zW_^XCd!y;qk}ufmkV2qk&Hl-+YUCWBT9?nNeP+X&bqvq31=LRdrbRm>!(Sm) z_2-*Myl&y+4i8%$dg^R@?z4G_EY^2i9ja)E?UjjU+I~j2WoBgo{~Wo^Kx+|QH7ldG zqtZpr!;RcGMt|DY6J>RQ?osol^jWx^1AT#YswU&idnHmMH4CH)7f&3q;br%G(P(k# zj=+5Yy;@LWbY!B#)~8b7`0^C@em!wHg0c(Ufpa$_0Pgt z{m=a?*DVX@?n6sZd8D!PzNS;&dYRHaX@4_6K7k_M-1jF1^1(!4nYuvzprDp)!o;^& z!e&n2ODqqA`(ylx%~1B$2`h{*#0C%onfYUC9rrK46n=+Vy=wVIOquncnD?LPMrA|^ zGA*-sz-;&P;Bl11wsEtm|cp^QecUW~^H z7^jejb?VDt_chfQ?%`JhzsN6^f|--gDeO!=b{#dP)fJ9 z#$0qMr<=2|J`5jAqucO^O_KD9U-t3!AJ^>^9FS;~ z;thXsvPKv>az}r&q_r;@(4}A|nHq@73w>ke*f?+_rKXv!lFimq++2vt%cJsS_%muV zCNxBKp#HgRB{lBxT8pcIFaVp$N(4ArJqCz^zV-O<)q`S-d@GStP*5E+5ze?}52^g6 z*v-?mMY$nbJ+2dQ-y^78D@xV(i_#FJ9olyCcz-!LdDwv;e*aeryT@FS;LuR>midSfxwSDBX(g?M()|U2S zrjA=MX|C%H7z9chQoN3_l-!?vgp{^daEYn}qfZ0$viv~`rF!}A{OZUe&{J5IKlWv` z6y;?Ot|&YfC0L`InK!yU_*f%r!=(M{7s{bni6^7d(v=nGVt5T_tF)0}?;cV*6w{XU zrdBzFoCBiv@k`Hnw;C$`om)a$ycw1%`0}Zxv-dfpri-bqpq z%BCx#G{wBtkz34Y`C5)3b(wn^xs%WAx9n=xckX*HNLRLZcv2Pf^A(qK+}kfxq}WIseYamm=L~>8v}LqYh71 zrI~@{0PfF7*_iP+QIn-WElK_!!zn8r@=O7E$L;lzy42^a79hfoO_z(4lMd%&VCo$8 zmg+bZ`rw|?R=$E|@jFZs_)6vRp*T%S2)6oQR`VL?ueQ7vU5YcRK3Q2{!5+;0wH``U z%#&w)$7#^z;SiD9n8Gof4$Nfe3`bDCVbde z$B|boS6}W8e1K`d7^tafYry~Zq)PhM;y2ePG8Ja|(Q%K72lQt3Rqd`!lVbfuF2B1I z5#c^nB8?B6u>MD2_fKE?`Z?tw{)e9T8w3|}0fg3z=z9>iL(OB-%;cq^pSGhRy%#&f zSGH@os8o7{)d59IlW#6Qm)-*vAJUaM1FFgMfK+HUx*%KU{7TI!H?Mt4ZQ_%c+WGdQ z%mPp3nn>QEAoxsP?86_s*hRe0_#abQix>J%b!J&d0^#H0f$sqv9enq=ww@JuZ@qA% zUd{HqIxbkiii-yuG?Eu(2m8lwTy5(04aRs`UZ2J#yc<|7rz*oT+jK*;f*^!yvRju{ zY~5BRQUTe#{@;IwHy>0jcv4V1q{qB}yS26F|y=l}-3eEp)ELbf{2bMQvA zG`{C+$_|B(4E}Np;whjf8#Qyso(royt>%+!Cwidbsbfw76)8XAxUuNz`ed zZF4zGdUXfzX5TkMV>p-zdH`V=y@$*8M!!F{2wF2;5xrX7%-zkotW(uDBsssKHa;{a zDY-`bNk*^A^zq?swh(lDXn=@dR?qw9rf@Og(VDZPhezYk;wq8a`0@B?aGt;?7HZ05 zUQEyP1=3hexxI>STQm=oHk|Nmreaadu@n2eR<*vOC{$`r>N^bh(rTZqIJG&U`D+*) zE9JcqfnTPR;o(=+_H#FuFaagO$M@%jCJtbDqgEZr%%%Sd%*PE`Vnmffaltt3NBcBYEqcbbHLmy?8#KzXV<*U$q`#nqp(* zO?u-+0D8xmur`L0X3NxG*Vg=)1~ydftqcErJ5n!5_A5oj5oQvlcm4=TsK zXm-rXTzcw;5@71BiU-IX&pWc(k=|~!$7e186ML(I}EwE4`o5fOQPTJ#S z1HL}xJU=b3Zg*km+E=t1qxvJw6gxTt6$OEz1@a3qDBp)u-nz8~RUWYmvzCAaZ>-|c zo1gm+n1G!>f0#@5-C)%-*~sdTD&p2e_sY!l9*9U|2MztPz)V;WjNZ(oFTTZ&Ram)w zv$qjx3=qB(8F&cZuN@#-k0hkdIRi3!9S%idXi9UH<@*3<*#q7<-AdfPiU3G-tHOO{ zn+8&Bcu7QzTgW$*+M%YcI}WF>HbewEkM4e8>=QB<_PeUY9cYF&l)ro(zR3RJV*ZOJ zX4+G@B6E6gpjoR@|DQ?oKYi;8FZs6e8-RS>_lJf?HVJJeYG!=4t>qTGh@^YVU0KJU zt6wZUm;c#@RWg$;R1Q3|h{^Y&?EVFnU6ElJv~Rp|SRvN!ui3OA8=oDwmR+`}4#uFA zEoPm!;0ZDw%{MNi=z25y@ncs(@KVawn?bGn~ zGxLQ&hJ^-L>seO=iK}c+H0n0|X!EP}Ty9R~Q7`P8mBTC8FJ=CG0-*9vs^&5vo|}A^0q%QKjI#bG+vN>m%KVWpWFpQF z7NXmN->fD@440y^uHok_iq#DCDqhCe3Z`RA(azT|LVxeT(obd==MfSTrsU?WGjGt5}6nUhRR`Xt=vDf5jVF1WWL zO;wg15ZD_-?nv~KK7^A?rg$i>Yz|bhjaT%Kf)%3-G;<&Zl7oTQW_~s_`24)eulZ0k_2(V~ zLTOe@)YFROqK~&RjCCN!+0w!nywjfbn&_2H7 z*-^hIj}fd47zirL={XHs9oEqP7#2zBxV}12kWsUgv|gqNnfmF|TDV|}kx!ESk+gJf zk-z7TS#4zW=r9R5hg+AnVOSo(&S(0=@-;eh;Ko0$FWRb*U0C`BDy}m{rpqLP9X%(-{4BoE=~}z=jY-gv@lZhF>neh+ zYbK(|9S-dP$XnLXiztJ42FX_;Q$F+LMT45F*{k1Z$8%T@zCA7y6%|dmGdJsC*3FS$ zNoB_~jzqnc=YDAf2}P)xzh+shd&HSjWmlj>o>SZqsDL7z>KYYBg~+(@biVSdC8biS zteKzjUbKGk!eOzG=!2fAh>G)+-=b;ph33o>b$w0c;V2sP>$BFDnXBSGD_&ihrh5+_ zFgrtc1tXmkhk~HqMnsM~u=`C2si<#T5Q}RcMz$o|Rd1>Y+TGj%^HbE^Q(vaE^oGI( z!Ite$cL!2=F~8bCGtVZoyDR*vT-z$5{QxA~mKN@SR+Ea5$r+j;*?yU`7jh_>&p(-{ zsb|YCLY`iJc7KY)A>>Q&`?oe#ia|ZQye@IO-!(gt0ig*Za9jV}n=$+0#K5Dnm!9E#o6h4;1qbY8!&K=ekmZR+P1@gsi($ zvDekQq03=eUt!da=1w9Z7loj6QD9Ku6M^}i_8(`&8}QOyIkDMCtn6>el~bW9K(6&#ut0}~CpvqA$(55yRM z`BG0k3%?CS+IVO{NSgBf!CAG<2R>U zAqwfCQ;q4^WeLKQk!?Mmt7CxNocUV@*>a8-q_Zln%SXYK{8YIjI*TZZS~u+ACAT&J zeSEIU7yo^ZQ+VBs&tN<~aQq(#3};2Xt!1m_1Ixn3UJ=Ls+S92MV)LLH*U5i>r#g*` zETpfPacUKIqZN~TEdTKkgtOTSd&CkXilr7yc$Y6u(S&aB@<_0gu$fp1So)AL!RXOe#4eK5{V>U!QHvoQI*o$!%4b1^4ayxT}X-C^tW z*jEZQOByBY%$$_`PW5L>uFrUQLTl5}HotF@NZB#9O^7X$K=ev9ztlIMq+hu>N#7!B zu6_4=xo&||V)zSN+@0T$otY`aC-=}ddvolC;eoE#(by#8|3FUm=|Hsx< z$3?YnZ7HQ;=thP{8l<~BL_oSjy1Tnu>6TEsySuvwq`SNM#(UzP<9Gj`-=4kKyYgAj zyHh9`>jN+!OE*15e^dSb4I+&hT)8GEV;zP^J|_4d7=@c*#oXRst;u zk_$Mqv1PT?zRn<7591s*Yf>6kPF4N8nE+PM-C&pFH&9qezg-ds`zI67wKmcU&Z;m6e7 zXYR(OjYCu@t!-y}k6+pE?b81d0dP>E8^XM6|3y3p|mq zgq-K<2}XAVNRN3`pI)FBm+;o`Q%FWDZ031-WOKbiNMEUEyudCC+}FW}JQ{g#S~?=` zx#wid=N&Q|#4Je}`|2er`CP_U&RPb!Cp=rSyDYz1(;@U?l9G-vLvQT~IbMiGYs}Kp zvKOYs`^9U+$Jk332_MA3wfr3#nWTLzhMZ3+goxLz+LnKcyiD|$71;oP4d>F^8+^K* z+b}m=R>Q^`mG29QV01AJ%R={cmzZ0)7+nz{EY;i{X?U0@_r!nflm7_emNgoGE&UHO z6qjtLSVQtZ^bBU)JhE`+%+JpQWNun?n>1*wevTji5yx`>IQ>0|hC;0<4U?W(f+9%J-oBFKm72(%AkxS_V9#?3*>q9Z6Yd%te z2l*t}viUA<-{d!LoBgrd+gGFM+!bI9AH6c=E)qB2y}U77p2*R1&w)ZKcGC-z@^!yRdh5doyrT7xZ9W)mn{sisu z&h)!w%AcyHC1x zmr5K}OtSEJHi|8QeO6yplrQH=$gPB`pb@$UN!j$|kkK65vs-r9pv&Wt+9P#Cd?R$@pTV4^{X0|w?Ll!vLn#fh5OQt@THL8P+iht-+5SOpsc~yVc5W*?O zo?4FGZBP`}nkzkzXQ%Fk?jY|-r};cO!%q#E!59OFXm1mR?v$g z#7sP*uONv4*x8djKk#>tF13cb!F6ce-@iMJ(~%>rT%QCD6A_cJ$Yj@NOWazm(UPtxYn%@mj|FOvh2R?zeePIqjwP6c%Bx@UUp*jiDE4QL zxaCo}&wKi-%?$RaqPu!%4Z^wh4wNk2D?`h>vy~qjP;O|<*o)aGU=6HVF`t$4J-2*6 zco4leOMn}YO{9dWEiR_rxZX6!oM~nLz^ey~mIXT^E+GLsIXNjqozk1^TF|%dZQ{%j*`pSD6ggA<{4oxk;Ijm7r`DyUu^=tgb6dL4Vzb}@DYg#VZe&FrjY4DnE9t8VS~oQUpifw zwAk>lvijU-gdN@Kia>T=jzw-m0E>tWkplH4)e|D_hV#0XJLX}BKkM^P?T=pf6&%^5 ztNd0xpr@xR?9~k$K~v0soCVGg=to{pMoPV6*Gu+ZN_OvvoO^hgs8NB4&2T4|dB6d7 z*mRaD^rB}Z>hjOKM)`jP75}e3w4Dd0Wm7#n%o*Q+jJnqMeO_^`# zP>$;Vc==z|X;y9(LebpLMsgyXA4xrfIxD2Ahb2aHOEmrD0BX;-F0R**{^CAg1(DI; zNS~&;no`pWOvFd$uE$4DeE1L+({O?6^bB(eg^qS-sL4 znwXrRAZg}}!N3U(q>ewWx!gO&7jVsWZ2emSz;TrauKH_Y?&Zi_&s{Ul%)7)cQVtA& zs%BtJ;7-T(LD6>f2Asyhhd)i$1|F`u&T(})+9~&2F$6X}K~xbvp?}QVOr?2JOSeJ* z&PPbrHTQskx9gSf^e!rMSI@*1+8kNb&+UwiwvnO7QX;dGt7N@)v)L56i2l~yUz!*% zy!tpzjh4ZU2IGinYGE$sBpW(_o35n=^mE{TY;??Rsq*h|(>?OoyviZBJ~xd6tF+kR#b#-dz88(=Rj|GdBivSI>gYA7L}^+u;6~u zzCDgIXxl7SP4kZrNOOWFK=WcoLi4MvLT4IrlrS$0T3$wnfym4qb~;tvrLWWbl!@gl ziyI3R8j@85bW zIeh}tQ=G^S&){hIP+F;Hh~E8q;=5l2QaLhTo~F5)&wJT4ft?pH2rQ_WYbIE;H*S7b zA0fphS67#-a*J0J(kQUdqqE*Zr*T42uK@~wHUmWEpWf7%t4O;X3n9I6kfIR9SN>bD z3GL%#y68+?`$O3?gD2TJOvF@L=dGR^s8W=ade)k-DZJ@z+GXFZI~Hf{(pvP0bB6Yj z3CbA$pz*Ka+PAqff7T{Z{a zyBu%Pg@m4WRa2jxA2O_09i&Hzm;S|5`a?NvBA}6POg;&@LeXod1iGDQIt(H$LaK2#bJC&J_D@7BHt1XjcH$@t#r%kcB?hT~wyebqLIz0^+1m^9mh;x=S*vST zt6C0D7r*wLo}G3U7JihGCkRIkZVR}(_A5(d2Q3G6m+jGS%qIS28VT(_@(yuxq< zB)%k$Cgw>^Z<=!qhBsjQ%7XvS0mR5QwJXe?Dbi2fvoDg8=pM z`CXV|a}5TZEd+rNeX-&J%~w{xlxuXMx(uNRKL%;J>|dlU&)EiVlss-9%S_xZh-% z!vcdAe;LECydHXm$hg}OelYl*)%jfZa+eZl!m7}!xaWa8ajgpsewFUJ8|#GQ4s4*P z1|I(-wEy!dL5#k$v&abv)1>B}=!>;tUFh04+;p7*%gAtik1fF@5n{nk@UURPz-O7q z|64GtMW`W2uzl8JOUe8x!zFV+@YGXd$n~0@$Tpq7Z6;g; zc4~gJ=)Z~+-CcmGg+-SNTuqCMEwW1Sps>5i8-3;_#wU<&;VsYQwmV~l1k7#+qx7Qq zKNZDyD#b_kT z1cNgQ`nJY}xHDz`$#2s9L$qLT{rJ!(*}a4L?)>a>N<)DLdDmx`e)E$Pw-E5|4F8F`HPK99J4pdOLH6l^Tq1XR)4mnr)$kO)xj^- zeqB4NbIxsOKtjOI1AeAC9jke_b*eC@bXkMO@g4f358wCtf25Ot_@Q+;1->YRh;Cl{ zR$olc)QT$BHzh}wRE5+%`JKE;IP7TO_0G!IvR=u?)-;sB&=FeNxGd^%qb_P}nhpq# z@JvH}{{&>|vAZ67Jfkzv5Dq38Kf28g+GBe{I zFN4Oe;^Y^pRZP0Zh9I0%N777YSb#v>){GS~kN4biYy3yR*E`A>q(vNkA)T?l+4E{i zk(RFRj9h$B?$UNfKfN?PXJLTgQdLQwn~SU5V$N;)Jv)w|yYRKsH!_X^Yq5)5;g1Q%Vu_YBecPWc)R>{mBqkv;Ey16nOL^D<7K}{&LsdY zPEg>6Nr%R#6S@)6+O(act;oYko`8E8gY6K>b1q_U&l^eBYPPkN3+vwbfVEGRmYCNA zOt~AAOl-E$1a?vX%U|H!L#wMVidxY3{rCbiU!IM_$vKn6%MF;*< zbEA{eZAVpraBG(dbQ1YTb`Ms&vPjveSTmCHZz4{h1c;`Q)Vw5w2uScCWRTJ?9 z=jNsgf*%?xYH)|_*iy!qr_lR=-L~tR9ay-HnN>sQ6{Ow_SP`7)XW0|^ItJZ$X{cEK z=Z#VpV?&k5Z$vliWox3O-ARnR{E;jA||Qb9eFj=B#HMls@@FllI;t=kh)5aU|Pq*!W}Tx$`&$ z$+UrA=hC6^hDX29UtogWLk+IH~fyzQqr9=trrpyX9^^{3Zh))jZ ztZRlvLSqJrcdK#Lse#D8xny<82N>&YXtUvOWm?{;O}?;cvN`&O^mUzrD(fnWY>3x| za>nV`YN)BIPJbfXDze-#_vnWl+^r8>eAC9iPQ*+yxJA(#_jb8ZWKl6EwU?D2OL{9* zi#>b16y^Y^VBb0viGm#t?;Q~dtuM-j?5a@EPMlF$Uwmm%?ey0%?Y0c!AjV9jh<0eM z@`7Uvg?+mE9L^}GIf_w;-equB7Om6XJTb-OJdGQI6q>+vk2?V}1J4BeXSDqzG11*c z{T1%+uS9Q8{XOR?T{FVN_hVvEL#(~bA(>gaHXUDic=B$YD1&u!v>2r&w(u42<@QJM zZ~soUqEdW{)7f%Lml(l3bW{FPVCD(lK9!icR3SiotISqzrp#6wn-RzzhT$I%f$HjDrUnib;4G z?`cqgZi+_I^g{i$#mh1ijqp>uOye%Pw5 z;o%e+kyYZEuAl}YoQVIaoAGi!-aoN0XErvXuk{XQnK3YCv%zh*&!0Q$?+Q#vXckdy zZ6Qu$3C3u+GcWfhUPt?Z^-~-|R$lSnu2kxk7<76pEAr=TTX41{Y~aq~^yQFUuK|I~ zfxS!BnE~U|5Z(`2ug_fhB|>NOO5G}9i%*zD@j%YBu19}G%43V5{Cq0V;)F41VEE;b zMy5)eD}8VF_;8=`Xwd>#8g8HBUCmz0+9fY!$u5~>aXmtV(V$k4N+1qcqQMC2BadkB zpZ{n+g7q0+nFk08fQd}g-9x?DEwe1LKKwIHn2$@Qh(+ZK{^cL^Xwl8(O zXm@j3?hkxrkjKk!o39F22Nqm}2i%5dPjw(g=B01%Rp9>!v54@HmX_(d+B>6iJ3ohq z5_j}QQiP_btFx-V5ji@RNXVmfl+?)x7=B}ji-THQ)5FIwn3AIb&&h%x$Aa@z`Au+O z@=6v5{_c?p5l<{hai2)Op4%i2X|>xTkK1%#%u4u8F;mp%u+Cu?*}h7xfDINjF|a2$ z!GB{Djot>*ty+k|Ao8dFZct9*&TRo4?+q3;h=y%j0$=V_i^EU8QYU9-#8^VOUdp$a~rS6Y>c3{O;PShKyRYu!hvg$O8mc_BU zR{Y5D)e{-+Z%4yjGAwwX0^$jr+pIpejYsfaK{{E*WI=?*-b6OKRm~o4WZM$$FK^CS21D zsF^39{AbisQTEw)QPhx^2lWM0_ES?Ws{4&fPJ>#GLnu@VIom98_^7n#r$Q*E3NKjL@-Aa; zGQQb%+=OcpI1d;wpVb@YlUw2Mht?u2j?ZDhlUN~ z?B>#QfkM`Bv8=AK`nkBIU4Wv^?zOz)_BTX)~IL{&c+Lx5y@`drF47us}$m z;fmb#sYuh2@}*?kJ=i$VOg50UivGH&Fb`A{R)Cp;fi>Tq!59*9&0-%p&Wp0Uf%cJ*FH%i zN8PrhS&)~u!rw9f4G@4)Ml$GK8RQEjW)vcMy?UlfTq5A$wF=eqwzS+{dfvm?^XOLG zyTz`}undC4(06?d0)<`F*XlbTijl;8u4XwRpU6BMZ-my8^{PtHc2DcH@y-$#EHw2I_H+Q&Hy7$d1nRzMuk9?sWHB88~@Hxn%xlM16&7YJDQ0tjr=M zjXyEkJX&%z-~1gql|504Xa_6Q493N!q2qWfs{JiDg=W|wvzK)&vgKrFtIp1@P$n2P`qYpk z&}mth@*{a_VQvfo!q=%=l#si_gx9M)Vvqy^P}wue(GqY=mp zm_bHyZkGm)IIy}8bagGuYK|%mCWpzFHK@N3j9?TZn2}jHOCk^A{fHo4l-*ClC%yfg#@i7ZXNF{|OmQ2T6P6HFe;4LJ< zk@#W1>aoE=yorekYG4&oN5=}-lcZ{nAQtd?31N(CkRfEmqsYQ-6>Gix!cbvMTVDT; zD&c&9juX~DZAqT%9|@$#m{HbMyB33+Yr#KCCrrxVcx~$&ysf{zichY2Gdf8yUCWw8 zFr^n4&MA~OhlnJ{sot|o;uyNC`~My*1nq{N#Mvxo%fQZ7n_Zg^qdYTW8pF771f;sS zb+ctLF;O+>i()=p)-a0N;2eD%Ug=#CjsO;1p(dq zhK9_bAWp*hrF-SC?%Z3I?E=<_W3XawVz8Z^1QgB}1Nf1(-=Kq#AuzLo zM>Y2)e*ZTPDac`R&*Rg*Bvfw5K*VJ_tKFFf4f4x6q&XSQ5CBl%e=0=0un$Q^(T|j^ zi2$>U;pSmF-CYZ~d`m7tN_%PhVAvr?F++MfOU9k?yWvjI(DQSKn1q}JVtat3V!1Sj zWySlhp{by98)7mtXdn|4YHKUeHo-;o;aq^s8(VM~w_*K-xZs(&Mg1Ohgu?{Dds1OX zERT+;O5gQmRPw}gH|ADNZHCugt$P_uoKVg+N2Y9Z*-8cLNqJD9`Q@zA=loBU%mxiS zV_aNTwrZe8Q)e#7%vFn&q+(Cg%GC4#6CGw34=#P=;#QNcZ;mCP*1|am#Ku**Y2qaQ zv6^9h14>5Q7=9EZL+$qP;2?Bdfp~mxm}hi&=a5#WW|JpqqUu)tSB}#c4bDsYqVr~d zt1}YX-;%fEBNKIj*4Fl-x>rpH&?l{hwXPTN;q z=bSCuNu&g8cx%ANGJTeqKaCzWsst_a{48f1tCw(hXl3s|qD92UrLnw5F-CNr(IcEz z3Oc4gaZPJVGuSxe5!1HEkIsHIaAb@h{dC^3F`u!Mc=c(=Zb^ZPklA^(;gEGsAb!|c zLnGK0udd(2e$t$NT_ocW|=UER2)IC@S^$)M?bupe_-_6q!6 zaJX=?Ub&KzKY$+6P9bI)wjaxV)F(I?$+t2cXfNP{-Q2>942VykgYPlhc|WD0(J1>1 zQI`i_SKJu;X#THWIoNJ=QPFivP1qaG48v+TS`*J)=0Ub6Y1Ua4B@4yv4AzC6jm<~% z;3XvBtpP1*YoN=?{ODLByy%Eik$GO82Oc@+f8k8vP(?mY4sRD4K)g~_2RSrrWH9s9 zXBSJz`OWUwmjJMQywL>g0V7duIpfMoTD3XFooq3pvn2-?B z8Ds>lZdNI`HcO4BVaZiT=#b1NJ#RSFv(MA3hO)HvsaU_;YA@K_*16ztcU>D3Lj9Rt zh*eY!zFSk+tU>oK<7r(;(Y%RV>>Q_RkEXjZ4TY3wC{*a@bCW)xf+eV2vnnuvgty=!RZkM+JMMW+XGk-p}K4ZSNQ&=;aqS zv0)HQ+R2bqH;Fv`{Z~tjqZN9ywGqU6)VXar@~N>AF3;%5uI+JE_IP6{Wi}uBH9JHY z{ddWX`bVJmt&7t5-TC(OD$&<#Z8$_4$gfoMIok#%%^jgf)&QA7NCyJnHDWpakoCAkgoQmK;#uF0M={z)z8hoyJ1QKo6=)uHI|+D# z(sDO7xe~N}jpfqhE{P-9K<(0BlGmy}c?5=>z=$}~>V1$Lt;S4%V`@rF{9I6u{t8tP z2m@0u@l!xVqX&Z)p_k5H#h-M0%9OH-kSByUtg25WIES z@YdlQrT;5YNiInZ5eci$oV(AKaW-?D4KwqNEzs`vRIJ|A7Gdbgdg10~sS|J4V(74` z`IB3b8OX?A>z`Lk9GZcAEsYj%uv4QN_LOv;6PxQRW4e_(l^#NhrlmzBnqUJQN{=~x-pD!FR#16RwFFvRT>eI33&5ZGR#B}o_X6s;md!NISL&dDx=XQO2mFkT)_sw0jbw2Snss% z`YFci^&;1j;@o5i7#=cB^q3Sxt8HpJ%Tl5Rq#`&Q^gPq`N63gZZ{~wnB##I>-@4tk z%M*G0BJ26K!ZFJ19=DX-u1>wS4S%2gty#-zd?_O4YsP-)H)#3WJFav=&$%4;g!Vl_ zAD~oKz)!lO-Iy`i@DQC6G^Q+=2fM{`N#L8|%%kw{=trkFb;&k$1#d73^Uyf>pm)FK z2{)9s_H4Erm0;i=kSAJs>CshQEBBj33ROL>#!XW5x2N>r4%w%OMr`lI_XukiF-<&k zn5#p^t^3PJ=;EfY_Y3L#UfF;tkW>&&^+`&?1|$ARHDOH+%(bse2#+W%oNqMk93*x= zV(M)@k9;BrR(DM;1+XEOKj`Zp!VXI%hu8pW_S}BaUj*bKE!WqF87%s^iDwMXu#wQj z=8mh*!Pby zo@fi>W6yl`U#orJ94z?#51mSAr+y2rGN^q@W>q{Z|qu;lr6l`Xu}iASmc&D8fub zrg;2O^s{1yi=*@s3U)N$C~> zvO-ie=18*rHO_hl#SZ6}{rX{1hB=3PFEQl1MO-Cr6&-1iYFy{e2)H5WyR!-j!1XZS zx>2^6R1W3jhZI?8U)~4xv;C!-^Sk|SP7`j_cbVjKX{CKDP$hzJh?1G!>&ubF*wAu z{Fy?rDFpiGTa|qIMrT)77~)mYu%j6i9+fjue1GvYaCUz+YIa$^J^j|Gky@=LU!`y+ zpXyo%&;hSH*GzJRh3qr}_3c9Bby05YQ$wbtuS>fw@c9^n-Q4HNJ)GewdLjih1)$OV zawFmwe~4#(X;&Z{p9TeP6{VgYzI$KzW54^cp|$sl0GGs-p$EpYAzed5T|q@9EG*2f zy0&Ie)?3~-LG&#%&3%CWXA6t=m15pF^E&kJ-}h`dJfhv*A8t%?tH0gGxbYf#baZrt zM9Xs-K7_hVQSemV9DU)*bHrhB@(W4fwHEx6n3(;RU-cZR4&ef)ySqChq?AB0U9%ZH zwp4EVqbeyQ6)YT_q#dsd;Y5S*)?4kwhntxes%~5+Vf&T_-ZC7`Cp>vTxn zb)B9^MQ6=8Y9yAUhinu!EM|ILElI!QWSVKqHz8&&eJJv7$-ZET}zt=no?J*>)BzGD99*-j%60dtSzGkpBgi^0H zCUcjU=0OmjsE49Bl?do7U>v7kAVq{qaew=P3@Awj_dt2%U3W7>gWD(A{i9wHnmt@B zp7y#sZgcX@LM3;#bZd#-%y+DT7bWR;-PgDrl&F?_EbNE{Aj84+ESO~aAC$Jigh~#) zLE#%MgNHvq4gSgh7E+Xj$mJ9~T1bD|)(YbGBtClm`^)o#K#P^WQeHWi<6v_A*|vu0 zEfIZZU2Y%nD-#vraNmyAQ4OzW~mP}EZI4RwU_4N$^I5;3{ zjXWcAm&dc<(>+RJ-N#!=m(2+inZ-95o0h~BD9)rD()Vi3SAv8q_vPB%Z1~$_wjrkZ z0GRv0BJMY|5Zgwj*x3zYodzrxwQzxSOEfgNyt#-6>{wEKmv-ow8eYvwHgg!M)U@ud zjj1rQ7%{oPM@BOdXtBr$f#!gr>x9z~%gahpC|D#*UY*cCj-SSFq*h%}3$1s%7b9ioYpY;71lcsp$B8$@n7r>h7Jqeq zKTo+m?yK_m9BXl-;^O+Bo`SjSZ0p&<&q2%U0Z&6Z!~h89!*@C_80~7bi8U9?*)Ol( z1h006(|_T~j(gD(IXMpWGxcs*MyboZQgkUUEfrBvVC?LkIXYe=*)VSh-yHOv({_jO zet00|IC(51j2)V*7ANoNN$9h^W)mYCf@*2;WV|_q!Aa(Pol!k_{eh}o$s->s9Rad2 zv9)($>N>>!>cODr2vG8yIl3RGSpYt*T>hfO6JQcBby=F~e4JaqCgvs1@z#Il`uP z->9;>du$%$mgveYeyjyrT?=uOX2|^KXy1H#AlGw3byn6znqb9EuCsoCj!x`?51Bh|t~ z$;=EtK>biEP=^X~aqF9O}YJ<`we{G=X?If6<`@q2HgFV4@IvrG0Y zj*D(@Icx^VRo81zK#{vi9EU3{Q2fsm&J~y8p@;Ej~NJxc`A7T0V zy}>)TQ=b_5FU-;(9)0}hQ=7zmu6iZLbCF_rpb~$Vz zD5Q?3-s z)asfHOxSqkWjxVgY&&L0i(l6Hyk|9WbE)6_H4A_pBi=QVLXv-;IO_Yi`u+$~9^j^zL(LM?0jaJcF)GU1!9 zi%Wen>O91AT&JN+eaHwCPCD-Ab9Qcxaqqu%-Iw$i+#9Pf`Rg9vZw-6lB!MJS*d0R| zamA8=q=d=&#Q-7Kr?ET+5@kCD`5RmV5qMST_p(>>%C*E55NqoR$dKuC>Iv=NDHPOK zqA<-#3%cO5F)@wSZK_DvD73p@rviXU(RU2q?$DWZcQ__#w)r8x zR`boDp;laX-Zwvt;scF8>fK+etrN83n2aQm-uV@sV#ATNcrF<~9>f-$U1Tmp1uWFH?YURa_IUU=?+;F+L)`T*lhk9Ec7 zv-@x{*ijrlv>hEAIQr&OW>U8g{hF(b50K49$u3&O%`EsD5^z|OvMsm3rNjzI=vc^68>e+g9-Be*ZA=*E% z0A71@rdsK;N}9%WL@Od95-M>JMEr8;9aCe%uI=*NcDtK<6Vc9VGV0-R6j!zZ3dF`y zTsF&nU|C|oh)t)r_-a4OD=%Wblasobf(JM8Or!0x*49f%h=vvfgMhUbCtcP(Z;Ku{ zVO15gw(R^f#^$)lC}Z7b>&u+pAnp{C+4QIH?8M$L2gKuKkT>bhPp`^Ca31du81!yK zWG-3-u>>BUti2hF%P@A%*xMYN0GG~3i@urtLJ*C!Z8t5%dRv1=F3$}wVWx!9#4FBi zxDN;nJb~iRSI)Nfv+Kz_E3DZY13M=CE#e}3PMYpVBfWRSPqb|>%TQwTZ7+#!@pqtr zKB6anTJ6W|hHI1IH2;epq%m_YdU`naWslV1W$DVK9C$}Z$Ih{_>9u>5F8E;RrSZkZ zK=L6xM!jM}LEpF~l{wM|{mD7Xm{+1(LA@&R!Qch)a#(iNe(NmhBX=oQu@FOt#?=`fhdo{!kH$oTUTTDS8iB<1koaF zvnW%8{9A7VTwafJ`?}b2dwb8mf8+F({iA`jA{MycBUbHwLK0&C1OAMkn9uKwsVkNq zSW{PDQo{URk98fAw7BlR7+MY2ewCo%QXT1gIB|LX|Rb_Vbj}$E$GY1tCMMn{UzgT#`d( zy&r~7D)f7>*ZFw6>YWz0`JbP~H{|k@?uV6Ldcl*}q9P(D7YURN}sWWrH>Ymkh z19GyzrtO(}cHQ0}c^`QC6C`o-3tPTSx>dI${6X8(6@v`Ffx$+^L9&6~u0vz3$!MC1 zoohVia$7uQrmrrX`RE0km1b~q)%Nu}fx6mS-MvZjPTeY7wkfS)p_})j+F|<+o;nU^ zTgDmM@i34^TY0y$bk6IUFSqC`iLG*}URd-khh|gTZ6X+6H!bWU9M4SP0sFzk_eGl0eD!C&)*Ag0sSi&#D{R?g*lG3|4PIX4 zh~CfPKeMWGFWPNeTC$2p@|KTEJgKf@!2db1;c3XPcM#^Vt;Dz zJEo%Th6@DKI(#oQ?QTtz!U$^z&AQoCz>lpti!~hnOjo9-#+Hi2Aw)Yg&>Z%KSOKzf zqjHVdX1Q^tZ|Fmbcp!_ao52eCc_6DXPCVi9+vNO%auXE>2JgfJW##|6DDq$Y6p;?H zlwDhx9Obe&0gmIHD*0;p-A z!SZ;tKI1yZM>H|9J9vGt{#FNs~Y2?;&C`ZTW+i}9%JcTAp*EvTwKWX z9~~aH0B7&QvpAFj+BMDsVdIAb58g!?>*#nxzm_6$1W;9V-`%gcTjS})`Mnc1U^%?7 z_IkkHpvn`z3unLzFRz0M6umRX4>bQlh;gxw9*4Y#7C2v!;U*(8TqIMf?F~?=vsej{ zO`Qwpb|tF~=a9OwS5^+R&1IHh7M50e`|ZGatrGzyq_h-MNmGjw$R(;6G)qrM-BGkI zU|hKMuB=g8h%Y!qX$ffySXg-!E^4^N2S3?^mXS=7+U9=anM6jCyeF6c?)q5=WD;Jfc9@2}NVyKynbC#tW{Oc6Fe(5N*;V|%u}b<`IE$UXHZek6di1F^{S zTZ!ls&C$yl9-Ek&2JsdkBySjg{}MCJ&~h1J8#U!R)X6w;^KyQ}OerW40fnh9SoFc9@py6D9pI z3ZDx;9CM;G$d(~LlXEJhI+d-oZXpyhP$I*F(Do5f z^C2>IxYOrm%gs##G;6<;VF0=~tYUF+hPS%kKH6@&qO%S9>Do79tOqT6`wYLc)|=W& z7j=P+3bb7 zKQf$?p-rsSpDdyF@a%Ha5!+|&v!0j&Q7j-RqFSNBm={2}hti6I;@0=RKW2x;I%kmiWd{F9$^;_b?kl1K1LB zJ!}}^r)hLiyMsaMaNY}{5vKv#@&=`Q*z8^b$eclSfl!C7Nd%dp5sU-z@9rFw6OJ)c zt$2hWjnKT@H?ZJMfeInqQ*#qHS3~AJtgMLOjSI<}hE{CI;M1o^B}oz5M;PNbvb&zV z8scf$T35)A5ueB@DOs*p9U}W_6CR*qoQ8#&Fv&F7&&y42 z$TZkp|DxGh8kj@e<<}%lb}lk5*Qg<(dFSglK%t$(;qT9pECE>MPGQqSn)7xO}+4ro)t76g>Huow;<88f&{7 zb&MTyvQEm;vcr_6i@N5z9ADRPJ?{Q}?TLrHkU`%yszOQYS%Y+Ie1F`i0}7n`5r~4* zz(|hA=#YT_LAoN@(e@#HJu2TNJuS|x_6$c`wOMR8V_W|<{21*p?&uwOO!=^|Fqpxl zWft}Ha@g`lcWKA<_#tlIe|04_r?BPbJmH8V>WIv0vRX3bGT(A>>YL=Ez5Qf{d+JKY z)O9Uyovi9&MnvuB{bFN|+6|XMrCFlsuT){~@~^sIyJqhAzg5Keu4k?=`LOWT_vN-1 z1@-OP9sk&nrK`*Ft{6(7&~^?{Z(Y^IK#Rkj6boD5I+I7qDId*Bih=QchwJ=UY2ZcW znr23(0HJ2a%lQz0qAKA1*kXf6`rPzz>{=|zg&V1o_N#UT^dOswE}4<7;e>lmr_UmU zrrNg<0gDOkr9PG)jiX2!ZQ%`Hr8|D;Nl(QdQZd-zH$V8J%_NyY4cY1yYiaEX zn>>!(4`aZ+10eD7WiUP|B0k@ic?`m4@1fh-Pvn{mn7Op1P7dYWx}YlD1-VeYXNB&~ z3tf@YS%P^2x3%>}%*LqrZu2YB4F}BD0Cq|Ylc*|N)ClbU;UR%qg?=QUpHr@JYYP*H z()yk$dMEOLo|=y3Duc5iBSVgq;)PnjNzFVZ?8=1i!5y2&d5H;&2{#HjEN+g*P6Eeixg)bHamk*6gVRy&IC8=XWcdS3(ec`MSnp8LS?N*rdITp>fIX&$bs;a`-t1GNLa}T9k%M)UcC-%)SRkOsL^+|_BF-_i^ zSI$(sA3{K{0e81&2DdEw0ItZ33j_wVEb3QE^_F~70cR*f;2GO8WVh_C&&qe5Cth%| zvj-7FP8dKZW#y9Q7bxN&0h}-V_vhX$eT{2Cx$wUD9f7qfy< zkL{=X2BZ)0iq2yh>Ai$<5E(g7i7%(&`B)d?eCKe74D5hSE7+npsGM?w2{ke7)~!1^ z4GPSSZybwX_RF59Mchxsp;>#38;eO}6JE)##e2{&$wxH-*015~gQ$z2?=qjjTn8Im z@zc$#Grr}1hOAb$jiR3qD}Ayh#$hiH*GGWa3gcz77fLTnF^CkuuSlJoqN}sBEH*m% z-G=zvUGJS%fr0*SZjxOIg-bTeJhsM+7AlqVj~zutzt}oCJJzN)R7N?xDmCei=D!@z z-R9gNXn3x+F($Ivz^uN$#&SIi%p4$%ZYC}pVG{AlY_suwpP|IG0JWWiFgx~4ZDv(< z%#r?cuXoPYl$MOwgaKBfEuT|d%zP^H`ErW?>sOYMO4BtoQh|aVi`MI+u~w_eC)sCN zi1Cfl07eOj`_B93_~@yJI(FaK*boAqNxBCVCu#Sceoj@E!0nY*rafYUuFMPQw=%1q zQ-1S@1g z`|9cd>&B~AJ-=+dH_J){?>Q*T`c=!CRB8PuLp@4O5q_1aI1sdJ!zTMKxBA?1`%owg z-BgLWTu#FqC=_$$cWH5ZfbyuPlmemUk|O{B)cmHo{XyOh zMNDX=yL{r{>TdLnr=74mxyjZ|_I>nIl;)}SwB=~R)MzoezOAIA8~ujo*|RO#&Zg!v z^~X!w$I0o&B7)V~MZ@{(K8Mo+R79HW;k$_DEOq71R&Awh8$R%WDan!E{XqYAncjot z&F$PHDQ9~&8)qzC8dyK;%MoyH`r7+!&?naCvA8`E29w{}IirK_S!-=C(k?W%B(nIj z2cxxIGMG9BkvRp(BxB1<2&*ytMk`drtj&gsJLJ5A)t_}nV+BE86M zw8(TY8Mo>sFf4g?Ya8;QrJzsDk|RM}2&Z7ozd7^HE>;wNY#WQ$p#>j*7_IXrTP`ES z;O<$^PG=`(eeVb&miA}rJq}w-Dj_U{uMu(!{&UdIUmZTHh;R(cL5aM*J6!7AxJfPO4Om&x=N$6U!XLw@;TkCE z>&1Rn%7sqGPh)tSA4MZj<3?w^-ex1WG7DQe!p5g;FAhh_HuqfdzQfMByYxd^v$WWs z6!bVBFyK*mE~UBph(iRe#YY%c}!I1kmF$ZpCw&4QKfE! zQK-+uoG{h?taThCa#wV&E~n%qINPT$WXiGK8-npTFzu)`$CPy+v7MX6y4ef>ITqr0 z0Q;aQDB-<`aC7L=E<UL?S zEc#0*;38oFEc`^>w10Fra!(jM7XEzI)rg3IBo?-{<>1M-20QA4ek2xJIK_jFo}0v> zlSx`-pC#>GQ?;*q!LO z0H>(E;;LQR;z!c^ouLw^OVv-GYz&(glIxekpXKj#$MWd1*oXGAI9DMBsPevwy^)ym zJwHcOd|OX)v`vn%)Aq=QL^Zoz?%=X%0t z@?h#kvZ7l>sT29XZrM-%cZ2n$r(@Os^V1?Pm*2vdv$mEPN7?gY1WAEx1Ys`@$=@M1 z`Dg&O{Z~{M-NWQyW=;M@Oi$d)+BNX`fY^EMkCIPG2{YNzU9GbFlLodza3~axE;fPk zFgPesYCXKmdIZ?tSBi@N709GhKPjQe{+SQ0vyp)E?PfWlhhp`S{d7=KdfLY3mXyJb zIN2lZw>>6IC|hKXf?r|YZEN}9NZ;*b`EavAHy*o^s%rHw)=Z&bv3LDN z-HlaMbI5a+vsUlh6_M1v1#ahYY8);;;~ZwDyR->iY;~is_DKuhmPz##HfG|5&<-(c z)hw6C!_(@tqXn3u z6iOHvuikVvzA$<(m08i|O**Rz2j|gxesP(CO5j@Uj}It>*YNE?Y7@HVD}|@Bq3T|{ z$$YYy^$PKn` z>YJ)J`B#KJdY+`RG6xmtQn0Xkxv+OVh$;oVwY|aK^vhDhtn8z_Nx|u^?O$Mo3!5LB zL`eZlc}qO+?&80|5$jy!B48*6MjK;QPQ4=m)+%m=7GTk)TGraT>Hg+U$db~lm^rbo z$W6PFf4H%bDdb_s*%E8Mj+@cvHW7}HKw)TaoB5`j1#jgu>aT@W4tQKEy4%Mq;cFeY z{ZZ?)IlUV*H|_-P2oqt!q1ykK;F|u1}e$Yn&|Sa#uKPY z1>n(i->uQ^HhAqYR#?l=toL+fKzQ+PJG%V??Z?+s&4*RsmbznHKMBL-kP&a64`QIk z_gLSok!`lUkeauy>87=#`$oB1wkR0iy+ya@dl-Fh=?)f?(XB2(yGTXFbi-J~Mlda# zp={z+q8ot9Bqd)=UO*>AGPZ?*56{o)GKG`C(d|rMz!mYzl^zNLQK`dHBWK&Y=dkpw zQ4Zc?F={9|`$3xS;`M-8gQo=PS522h!CjEt`U;K;(s{F4iQu1F(f>ocbudzh%$)~7 zxdzg{OUC(L6Nh};a$O@dKi~^wS?D)&6n?tzS(=<)qhS~V%RA|o{vhLHAcWIRRA-a_ zR6GT%WY*m=bHp>pw%&Pzs7kzuldYF@zinP0rPGLb2#}hmG<5K!Y9ZZ9arXJMvt>mc z>D|?>wUu9c=I<14YRmZQ=Gncacoe}^x-F3~dPzk&1@gp>U678xlJx+W^r&{HM&fHK ztWROwvJqdEPGcrd(Llj?Lus$KOJ*_x#`(XFUUyy}v}iXN-x1Su=!NKz{jGn~kcDZK z%Mgl!{}MR;U3MjO98kx+1UuM&%4)JtlJz_tn?IDsvtd=Oz4!M5;)&j$WnCK40GIq1 ziw}aGz7xVdnjJrkI2P}6!ED!t3O-`P8=(@ch?x~X_?EIf+;(wqZ*OPl>B8qJlrXz` ziM`cUXa6Oe^F5%%Ad*{t`o$cApXKOP=6SM$m{m}^8waN7t7F=4J1@20vNDtCc$*9O zFRo*zd#J|qrWVYPr0o*YP^PoaL*`yl)g3QtQdxdVqWZjIUauZf)V;7-E$TR16}&bG z$Or%ZJ(Fp2e7YMs$X3e!r2(zsJsfIAr0#V1&Y{{N2r>QZ6C3V#R(hXi24}T8}tSNOa9T}{O zp_g!NwjLuKXXhty348kar?qq(%65Fv7kcVubTHYOM`5&GxPpBKd&u^hF zJ|FO?fF2h680hj#GSLy@P6H0E1s(AW3rf-UD$0VN$o{nMjtY5lwBob%jubSs;D|E9 zIYv>tteC^*CF8p>E?zHFWX`mB8uX=bf`dGm#sWp237#sOh+pPr}elrpTWC&UocJiq`uo znHsMsSyV}-pcf_z8@Vq32_t@ALSdXw!(?qC;2$HS{_=f(TH+lNU$b8$l@sDoktd6I zj%A3lro`(pN3#Hz8gsq`H*crrSIKi=1>^8Z3)S9qPoeyZl~53?4!O>-sW#mc2APe< zSK+|KTkBDj9dE7qlI}njHs&Nr?2XhE^tCnsWjz%o&Q~ec`F=__`u$%)2;afrPR z<3(@O19+cjzj}3nh?$03_+8*m+sgUHx39BieNTN@UebOuy?@(!0~6fC9Bkb6^j_Cr zHTd6WUT$3qJEu`URn&7wUqa}M@d%4I;C50c{{cHpy2k`S@b26Za7YY^FQ)iV zyTf7BDd?R1AwSwJa7Ha75lMBCZN>a|KgVwd09FUhem&Yf8y-}Del19!YbUl5T7ahF zYt!w~h?UbniYUFXpj0ZUaQXadHl5&50eQ%#L6KgyIQ4Yl=BCjXS;>qG)q)e+)|)rG z<8pewTl!almC(T?0`tUQ)@3K@{(iE*|5~B!Ygj!8!fRNe+g_0Dnzun2w!%#=<~lO6 zhG?W;%3y4?E@WVqg}1t?p>khPp>ho;0bi>tPxb$K3A@(_c@qW1{0r}bqc7i7^Wu>u zYcf(i51@d$>XW}!ZOpE~jFt~;Ve_%c6=)*HU#~&&}OPoA4 zDqhmR%w+aC$OJ9=T?0bKs9*_bdEqr?I+J=56LVr9G~AeJj}7{U0+5U8Z6>BNHt3xC z_3&)c3Q!I~I-X%{?YJ_%ulnch%RPfZHM7Zn|C0aH`f17dau$WJJFhPOxXfFi?u!R6 zPpP|*9$F_QD<8(?{wi76uHoH|+?9X+#m4e4pXZGF5Ya$dV z`rB~q;*nK|Nu^14PEYNj=~FrLo@eJd!~)Yy)+G7Z0f1DSozC1J=a-1aU*AT29@`A* z9EAl%(F z*qdwuZ@5kuI;&};GT$q0*QjML*ET6xSUONJE-5lP>qf~BRk!}^G25t{Y4sG$;yqOV z4`0xz{=03bSO^NV`Ikp%WO_NhlS~0@9w}!^3z2=A4{?k}?^wv7yFg5ws!XkftYGF6 zVzkv>{J72_1^6g=2MTc5286BjM(|#+OJ`8&}ei??*Q?l**<*>zE=B`MvdM)Gi@+7YX2s;>HXTP zhFWhDxlfD#@HKXVFL&1(E8poHe)-Sy{4+o+zFq&ZSV-ACu*}REc`+m3YvMFi-Db)w z_$$^yo@$M23BpMUod)z*AG_*>FPJqHw0*YIwBD$0?zMCY9kt-JUsWIf^C}|~hKN@zg+ur zY-dj!C)c|6^IMiV>Rw6R!AsuAT3*4d$Nw|zq0#;~c4=Sk^!8uA0Ss!7>*(6}TslIR z6j>*4!b2SMc29H)bP(`NCnM2kBBSXkHQ#n1wC$5ROB!cBYHQP^ie@D8u^U@^iBtwx zza$tE#6Trtzzm;j&4|n1MYI`Ix1x){rW!E6S)ZQX8|UW1WRo_dB2nZ^jNB8^oJ#3{ zUA6Dm8FR3(MklR&F2|V0VRm+*WU$znJ>GT>^Gh{3xhkJ>bOH9NN$V*1)qd&YVBhtF zqzo%C2W-JQ>=7!d{od|X1m_#P3=+SFrrtu`neOJ=Op)MPS}A$EC~bn(mz5gm2HE&c zi)w$T>7Rc0zE@mEKs^5wBL60SMP6zn!E2#6oP5gsV&Pt_-}6K;EU1(JoE0~kz=g8C zJc9hl1JkJ}zeG?6eeU7|T-w7ca&%9gjPEl;LRw!IQRf`%hZ&s1t$_wdDb@xdfYt{% zDqwY=wRAr+WiXP7KC<3<|3}}p3SH#u=SB@?L(4$VR1G~^8{`QqTs6pI>qkU$l>SE% zi5E!SQX*OR855n$i$au0=uX3-MQ{KE$)q{Ip9tXym<@_Zj<;f{eCcx)-(y}`mc1p5 zT}T>1<4I+bDjyG8FUMMpAKdezwtGN60eHOKO{RDXiJ;0O$9*Ypsob2g%R}XNl5)e} zbwQ-#(WZrPebvmEU|7mr8FPDPG3elNs%srTc~5CSua>RBZE3?-33Hwec)#ht3b4G{ zzh%~P>p__7_%ZjkKq=5Qd%BR7_GJ83|9V={S4B~`-$dZq!*iA)j5m6G4P_mfO#9!H z2aLjxlYsXQ)h4T{dg|{#T}L*gZ+}@^Vak7OR4`sdpaIB6h~JO@p!z)T5S6;xRQIxg znYv>_bKAo)wY<3$Qx>6xuL-# zdkEw(nM@dfi?5C_ijx?+2xXo&DN31L=GkdJ88jioPq&Hc}!yX(0{%VbAAXRTp z6{|&5vi}{ImR;c$_#kA23B_yfs#&X#c|C{|SF;M9%#p*cJKv1wPnrsR1Z?kd0MDf| z^!Mc8@ySX3ao*rln6BGb^2>YgS4Ye1XhkNx#$EH(+Qs~RL3D(p5ub$>^zn?!Kf=b} z`kO!VnGTpA3H)-q_c>@Dl#@H07U@(FydJbV`;#aNB1U(%BU_0&0gQj6Gec$f@!g4J z<12LY=oLCW5Pb=n>R0?zi*i?`c7EpV3GIqhV}qfjvjl1b3}6}~wO#D=Yq5JnEv&nc(gTwWrs z>yjpqd(nw!V5S4exjaBNro(58CN~>uZuhZ)u;K+PW8}N0&g1J`XoOJVW@|0vW%3P0JYl&(|CNu(iZe;2fNKlD!aqO+x5LKLb%WkY>lyU!S^59k9+KT zNgK=X+6o0e@=Z=zvYDmWjl2lwIvYE7bE^M(SeI21h%vK$ydIE7B2lAz@@uM~-!k@W z6YD;P_i{`AGCc>kMQxrHv-qgHKLqqqw36CD>Sn^^`84Y4? zl5lDhM-E0(vWso!|KjccL0_#@Eco`#+ur+S`O z2!ErSg3H6%vLE&I5`^!2@QqauJ*PvQ$5N8m z_wn*^U^1`upx+Q^5Y-&qzrI3RxY>|ZmD&$nA|FhbuN+6wT@KN9)xBYr0Xb>saBScAsRcyQ!H?mbGXv?vBk}6#U%6BsM(9{>rx?J z#{&7)S|>loFKW(`IFTUA5D+K^4Lx#Dlh$;P3j_4n*}YQJUX5eFSn7W0EOA&PIx1rLsjf1F%MJwaDhNcr(>Vq9KXUnT02YZw+WVKtyH z2b{EHVCZ?4oK@Efi`&sG9KP&;MDEL?X>z`V^%bwnL_8u zov;vjr1V`+syf5beN)InxOP1IZgc%Kk;|A2D{Q; zB*?0!%IZ^tx#yta+@aSeRO%4a^cK9i!_%z7LOT znhwI6u6eu}oQqfj=H|d~{E>9lH=f8*Q`WW_N#gIMdcr=Bk7c}hoy~%Hg}#&=Yv70a z#_7{(GvJ5(;~~EXHJrh9?j1Or%Km$)-W!m!>BEV}nnm2~tN|c`ESfOh3nkAX;&cJR zZubs5Qr>K?1S^A%cy4w}UMo~tF03Y^HuYj)Vb`jXc06`*Pk@f@GS#|8s>h;Qg== zNB)+;4`(a9FMdo}Bu9QMD`2j&kP=NrijufLI}8xUV%X6NH=6s%%-ujAvXCS@wxhyh z^Q~J4$^ESj-Rt58v?$64i=8odPCf5BPsRC@&)fJ9nGeO=NW#3x)jUN0c1bO*%^z2n zR~h@2F6G{02mi>3F~&wrnFx{+JA)=i5wWJ7F5;%4o(W8<=zT@lmW0ZWJhW&>Pk-2! zW$iI*eh_n^>NU+W>y2!$gCSaW9ylo-h4g+ZDs2y4(FK-2^=iP$;ex|Vgty?QOYe=f z(LGu*k4$U~N#m@HgN%g(12c9`Jm$~* z!A6w=(2f@*?Aer3S#?(qNa$Koz~W^X z_TwAnRYN&lgvhk666DL-^Wsp#Bbxn@+B6~LRhQiNhYKfF`q`XL=UN47^n&@XX6#-w z^als8fMZME(N>Wy(g7GOx3jj1D)XKT!c_Er{C+Sr$)&5d{Qr%-_`|a7kg#(AX$`}5 zhPHn9XOsN}{J&VlL0AA+4#Id{0p>w^Ul|vBGQm!7x62+bLiFe%O%7@ys07N?!RBH8 zL)X`%rW?+)lY5WCJ`K82rhv)io#X;)3@p&tu%i6cxQM<@o%qq`!`REL#+H*$`a$_N z52KD^Ukc)CUv%bkq?bd_Y`A74{QVxM3LI+}iKsd1!yXB`l>_;keqSg1JW*f6LwJgYOY*2Fwhd<0|M*DonyWyax! z=bpuX@oigvn^%|lsn|ZOr~&FCkQzf8GKB<;e$xQpz)EjfS}ta| zm06?X<+YRIyI)b;u8d96p(JU=i32eP4c`ux7vnX4EE}?_$$@#gG%AN8D$bMnZcDbVlqb@^kb z7q7`_Fs$l~WxVQijy`X*LKTi{KeKOL2#WGLvQeJ)eDmBm)utP2ki!LtYkiz5muWg8 zR2X7klA;a)LoCu}j$Uin)O~xQB=ads+~N#cP|QsZ8!cPw%)VKV^YVjqw(r{jqJ0kb zwlC?wQJi%Lgps=!=w~wt+8+vIqK+25@JDdDa>qw!ZamtTnGJ2>W0C$2kyj`lCVeuQ`? zFcT&+BmdG^Jm9@n=s9%#63^jMFXttBn3(mw|FbSf`m-{O!$gg&><5~(3z2Wtj;GE2U+rz z1h4)s%ol8L`_24#tkJoJyCz<~42lEysM!=aL!h6HSFpKn9YbG5HTK~cg!c4}UV6;B zQV1vy0BjLPGH4N8$-UJHOgA07V(njoUI>1unMhtq@`JvMh#5WQJtF8U)Ewev%09*p zYeI?+u4dsxm$X$%EDIO?T706Y3In8=tBz2H*1vX*%s@o6IfHhfFnKCovhS+UhH{$Q zXL3@2%|WygK#4fu!M?)gWGka-&Cd-zO5|5&IhX$ZolqiawX~K9fz9=+y3ETck*lX!S@<@8i!HvuqS=KHk&ieoIvaChHr>Wej?ZA@14g z=`3V!hJ`T7mQ_y;YZ+q@l)n-JA(ax`ZQSaFtEfw_`c_T~8)NvacPjRouz1`C{kJ<_ z_6t&PYU69drfNf0iFs|evtOt(79nX@`zU&`R^{6MS6d2hm(%Ek0lxb+d_9&fAQDXc zk#+dWYRx@C#LNYwAuEI0UfWZs5sZmR8#CSPpBcF4ivt5CnA<0aCZ2AUr)6^GT1 zpj-CbQPP{nKqT@Ymn85ou8`Y&gL#HOshi>p_w0bzP=*k2XMl3S z0^Zrm^m0}X*h*i4L1c?@PGheWLG`WtG%TVjB&eA#_NtmXD&QDJ2Eob2*YPBS;crj}=0B`+k`}z!PEVK$?dInIC@&-Y1 z>1(e}(c3>_`ukUYnv~4JUzB8Mv0fupIRp1zLoU zbN%eJj+-Z9{Nr&Gq4(F$)-z8iGjg4vly*g*>qXP{*{6OA!rqMUp1lsXhxAnN>bIkS z+pSlPHCF~!V})GB3bP5S9Z~7_{p8xkL}2x>Ga}sY%`whwI-3Mb{80m}jR{i~Fv#h+ zY>n2ET#TJ2gYtG3IHR^P$evey!x*5_-n#i7)^+N5Nr?5drQdAj|5aHC7Xk)r_!-p` z@A6Wcr1YO{xYtG#*JSF}7J-$I*_7;1Mj}wwK2%E_Na^F9E-u#;gBuSH`QTJn>Gy~5 z34?ffl9Qbtu6y?Bs`&-&?@h@N3F4u^??L0!X!n_4jTy=F8_oYvk_6*7?t1@8 z%KttNf20i>qQ2;F`BLd?XD_rr05#d-IeVQJ;;C&R2UQ$Al*2PVwy_`aDsJSFqK zZ|Z189ba^<61Pz^*dlI-zX8&`D)jc)&_PA@C7~B>mUZ}1D=*!Qj#KRApe>!CZ&=>) zsMc)7LlpcJ`T#{om3Dke=Xb3~y&qfHf^ z^GuocBR<-T{0?_9N@1-kKQ8F(K@z@x)D8J3#~%W}H4OO7jjO zV9VE-*mwsX9P+|l+44}6i(h-4j*wE#v$5S(em9|;NMbfD&t%>_TF=1>o_aXrW+6_0 z?w3}1Sh?8zQ8$PBoE(>!Ajra}4I*`uIk*!Rx281IR)5OZ~GvPl?;Upx(%f ziY`8fOfAfsFTG?$?0MXQ7PAJVl6pkzY{kCI^K);T`J!bB@KFU2xNy^-Ou-m$he3Fi zk=5(A^<-@OK(em8d;oW9I6TkT(v6w^3hr%Hy$K~Xb%2KP z|C;dcE_PhmCQ`;$vb>R7 zjbwS9`RJ7-J_Q#|Q}3K>DyQlB*H)fROL04RIIbGL{%aP1@(^n#hRZR0W9;)?+q;F^ zQAet|Zm(9vi#S50F`gNOTetAa^~)5jEQl*}Fd)Uq<@{|#;}W6<+sZhuNBb4=@-SwI z-}tn85%Q?}G?H=#e)kk~15F&W79u6In6UVCqvwZ%F|kTWh*J`Nm_)u&56L1GiQqK? zCUZ1K45!0$WOMA2mA+XrZG&WuKhboX=|P8QUum7KG6_+gsOy7oMm5!idjNbBHzQ|F z^hnPMZWYS>*}YH^u8!Iei1)ymJ7ACYWx-j;9Sv6vjs81_);(_+N^oQa?`a#==L*7H zZMxpMK!0OY#emhE889x!wW6}{BRNl>069+B%m3^Gb6V z4PsEu-YF7`b8{j>BrzivXa&~vYCk)RMHJ7E7%8LG6T$(Pzv$Q73*s&>NP{_|j?e_2 zBAyjnrcZ!0R>*hFTH?#fD&5HiVIaHTm&o4&h(-1y=)VXcese#julzhUDn0c<;vra0 zX)J?gX$0-NKHQ_>^A=>3c@UJM-MeaOz_!n$`f(HU=|h$M8Q%6n1ZpF`;eODzO-Q*C zPfC7K{aL+^QHe$dwa*EJaslv`(YDKLnLp)wgIi^OSxYzV(ImV?xVWzpOYE3_y(DH4 zopG&b0QW_nRyXe(GVZ*Z3I3uG(t>a2A2)}*(N00zB}St{XQ0L%+hL9|`^$yUf#X%b z4@@fTSa2^q0q`-@tmNsO=!++P^t>!z93 zeeqKZ4-C=UZ{A&Vdvh4yt|y<=6>@1K#Jp2IAIH)J18ccqG7jYmCP8!L)<3wNMi7G9 zv0CX>R~earc$i)F40zTX8DB7Rm}m9$I%yTfjCG~4v$lG+G_A(~CsNUc9Z@rSC3HNZ z_P7(%RXjAr9)5g~E?9G!D%M!x^H^9n?hi=N#w4vim{KT% zbxCx(Fe?hZcV&heI`;CHwjc`^KkM`hvrASg8o>3#u=^O)M#x8RW~XqS~kB z-8nL$D%tXt4hHXgy^EY!{gc}L%|{C+h6w$aj5ML+HTDmYn+tM_bN2@CX$5bms}xw# z%ytB3xtEKz7iWKwz6Ju{mfKzMZOf=@W{I711dfTn>prtbE^w(*;*Eu@ds(}v2wf5o ztM9j-WsPdp)Uv+SyX(wB6ZOno-qoyMV(;-=nwF|1&TqTf=Gz3ir7wOBa@f!gtHS3T z;ih)i{@LFcOtyHC*nd2p)EeAYxW%>)7a-< z0>PCweJRpL_+u<94S#aQ%di!Tb{y@~HPVzJ+35Z1d90b;xPnvg(CNA+@{_c<*Xqui zDt-Q4biE2F73Hd#Hc}(o=?||t!j`3c1BtFJh~kC61LW3wvKXqm2L+}=s3v?fL~CR* zC|lgn=2j7;orq?~nrd0k?z%(X6k|S8HMVZfKb{t15SDU13=SC4G3&X>hc3oPlLh^_ zUxs8BB{{iU9UXtmQ9#H2A>K%!f7Uh#9v`pb;;jB6?QZ5dMnhJ0^#{A5AqP{~Fi;o% zF1h~!B>(-xIt|wP)sf@gWSvJjFTJo&H;d2J8p9UqlWlRRPZ3TN+C?b8yP;iPp>_|h za0O-hUXal|v!vT6 zDin5S{uPJy9$l_A6T{ko+=9@$tXpa?;=VGjX)C4X+$pf`5qIx_Tb+_QBxJDt-gsan z9FW6so5|sjpVI*y&rwDha1?3&VTv+~o5V8bDg&ntZVC~$J&uwk827_!$z?qdCXKyZ z{Wkq^{*`ppTrg{WZq_-ZetsJ``^S1Szb`2r2t89d_bukdrQDv>sJSuAD6vUV;8G^v zHv@u3;!-7}U4c5iCAlL~qKno}A5HSw<0-a%n#mo788*xr zuY2jvL@_0ij3sC#)1WEqp? zL@QAtmRxVrw5D9CHd3*cjScKI@454piJlG;KJA6u4D!R{URLo7I zv_;>MU~M6vqklDn&dzn0pOcwsl8B9b=p3@Au_%T$LNaEIavp(jJT6b5nt4%71j4kn zXl;?P>U6!Bac;8B)QHLs9kSPc)*)qFUaOep7&m49m-Nb%M#K-QGHz~uAjtG_p|@z3 zEt)w%(-Zq5$;F9OocQN(k93uE#U@U`VNnaBrF?Cl1CW)}Lw16gDn%`)XkI=xEwV1T z`FQ}WW&!gh<21 zQbVCWzwKUoP}6~dI({nnLOjB&&bpmLT*1Vc)3n`Q$H`a=3Y%M7^58LS?b*tkTF_B{r~Z|7GD~xS8|tb0A&{N$EZaZghgK0k@ zu3zp)_V}|miJ5EMn*tnW750AW+D{7*lJ&{p3)c~CI_LQPX{l<#mlLTEZ&%Hy2d0%S zs{g=-8U4_v4O1R%=fY(ETS3x*?5E?=eO9bT7~|JjP_BCvCw#v#!Wm$D9@m$v#zlIR zV0+&$%DW1}HFHMP?$;x8_u8e<4w3uYe2CHrSPbs)aCow0bhqkO|fUo>o_i?^Tb-akr6z`^{|)5wzrA z4Jp_$LY?h$i<7so@yvJ=9DPH=15PE4k9OO1)5hp^n9bP{;Bi?6S3}x(l=3z()3H0o4F@1O)2}>9x|WbL^*TBK&u=3RBU2Lree;LFuC5+jgi~q z^-_(~D&Nf(u)pWI+36M@GUB%7xDeeqiKloA>M(V@ZH>k}tpm`?nK;t`;ln4ItkQ)O zxBe2Vu7_VW|)dySyAfwwhUdNDnvxx$4B~YVa8QtpHcJDJ3G|OF zM8uoV_F3b8CmbGy4x(hYT?;6rx!L&u3+INNVonRQ`pi45z(7w}#@Un*dXi=QgWM9yn9Xl$p zBR0+5la1V9wS^@4E?uid!aD#J8)(8-AW}+>GRj%v?qsQoq};7uz%@@&|6J&%k_#Hu zAG{}wK6Zh}r5BISXiomjBDo1!gzXyV`vuvgt*=RfDli2Jxuut?CZPA88n7!iPcPF6 zsF$>TkPn?o_Ngi^;U~!->~i+kkX1Ba7QP_}8;3~Q3&PvGIj{q=KuzntX7P7vo}|Zm zgYLyy`O*KIb*1FH`nM$HB^XojXPI}l2!%&A%^%zFA}{hZMqF~EN(S2U z4N{5Yy_jy>SG|7VohP1rEwPtzD%H_lcr8J)XNhp^&Z2bp$AZ;m1o-j>tHBWtr?7Vz zZZC zlfjB}xiGmz;BWn9Ot8?ua>}rzwS!5Ke91Cakzskm!FcF&CuBN--?*Et!F4$`j=9xJ zltvfXDQ<0%o90l^o~os7NwQWstQI$qNLj*$4sOwKFy?CQIG+0|u%P+Lvvm}bvIW*O z-hWZ`W9&StkJTXZPr)^ek)nY6QuJ}C2E|zGl$h>pD=j=T=XLnfPJ+hRLE`3f>o}2$ z9hWIj^ym?m5^Ntwg|uMU1kK~?l?XuoxQ?!XhElX6Y@{bw^~U!~L)%KNFZELlsApv+ zZ?R*#F^hlG@36X>9i|yHgVyE){_a8kziep7N6efUph=?i&IjJDzy|oqh2$6$Xa-xu zBJ;a1hHS(#UJKcXtnS-l$yf-wp8Vv`#U1ktUpVlx^=#Gqvx{a}YZ8Lt-}t(6SH?=r-)K|nz1T>zG<4q z8%Tu{hm`AL$3uaR3|S_PijpVjpDH~lOo=MI?vDc_=rhb7T^NUTXBq6^b}dg?Zg556 zD~sIQnG(cizzawkBh};U>s(b{$c>ALQ4;i%5htUK^anrkkb;GR9(QAGAQAGGF4|m` z=Q8SFKHPRF{lQ?0yv%qfV&NlpspUD`}gr_APF7nlWbb44rJqx&Y1dJ)UvU@(oW$ zNLn=$rf=;5v%RL3sS?~lxDk_;vlU>8c;}jNB)alw;;Y5jj&H3nZJ)<4q?M6s<7MLU zx*{S;(=f<=W|q9W9&W#SIazpc;gH5#jOmJ`_<+MnelEs`Tr2ln48nqn`a>DD+CJzn`jvX_e z8w%4QO^&rgSyWTkf;MjeF1ig2!9Iq2@9 z0!NqeM(hV#-V`4QzF~uIoho@0m4Dd_`XxYtWJlb8d#M~8nbL;b?{dB&f6b0HI9#9R zO(!$exx$}<$%3#qq?XQ@vB_b`Hs`i$+>_DmG@YPmiRGTJMKALE1B3eMR_eO&uu*HZV|Tgsb}oDy~XVP zt5O)lIhoHC!?zXpF1%mg<$_gZzF5h0F_WeDrQ65shAfgZ(qJ1l)o=}ctbDd2baR=DKa$3hR$%i- zz|b~=MISw*oW;!FspG$e8XG)67<-PXd06A+Gr*Hbg6ULj>3DO*^Pa|$Bl$H90-O&+ zI}ENj9>fm$ZbtrdQt=^6Q7dl0mLNCzbj79WROWpG2ka1zLou6lkg9LZs%|1RGfBfa zi&0V4DmOY~zy0iB6(I8ma7yfJI3onI&JtnaC=I5G@urhdI#KVeI&s0L8#*#b7)piS zkw>iQf#3Td(`qj}vWy1~r&sP|w;UuFVm38�qRF%MkT_ZtgMZIjHh>`4w_>!~3bp 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 literal 0 HcmV?d00001 diff --git a/architecture/ingestion/engine.md b/architecture/ingestion/engine.md new file mode 100644 index 0000000..b3253ec --- /dev/null +++ b/architecture/ingestion/engine.md @@ -0,0 +1,376 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Ingestion Engine +Established: 2026-05-26 +Maps to: udlm/lifecycle/ingestion-model.md, udlm/contracts/information-providers-advanced.md +--- + +# Ingestion Engine + +> **Implements contracts defined in UDLM**: +> [udlm/lifecycle/ingestion-model.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/ingestion-model.md), +> [udlm/contracts/information-providers-advanced.md](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md). +> UDLM defines the brownfield ingestion problem statement, the ingest → +> enrich → promote pattern, the transitional tenant mechanism, the +> auto-assignment signal contract, the ingestion lifecycle states, the +> confidence scoring contract, and the authority/priority declaration +> contract. DCM operationalizes the engine implementation, info provider +> integration, enrichment policy enforcement, transitional tenant execution, +> ingestion scheduling, conflict detection and resolution, write-back +> implementation, air-gapped verification, and provider priority/fallback. + +--- + +## 1. Ingestion engine implementation + +The Ingestion Engine is a DCM control plane service that drives the +ingest → enrich → promote pipeline. It owns: + +- The Discovery Service callback handler (incoming brownfield discoveries) +- The Manual Import API (`POST /api/v1/admin/ingest`) +- The ingestion record store (`ingestion_records` table) +- The transitional tenant residency monitor +- The enrichment policy evaluator (delegated to Policy Manager) +- The promotion gate + +The engine is event-driven via `pipeline_events`: a new Discovered State +record without a matching Realized State triggers an +`ingestion.candidate_identified` event; the engine subscribes and creates +the ingestion record. + +--- + +## 2. Information Provider integration + +The engine supports two integration modes per Information Provider: + +| Mode | When | DCM Behavior | +|---|---|---| +| **Polling** | Provider does not support push | Engine polls provider's discovery endpoint on the schedule declared in the provider registration; each poll cycle's results are ingested through the standard pipeline | +| **Webhook (push)** | Provider supports `POST /api/v1/provider/ingest` | Provider pushes new data on its own cadence; DCM authenticates via the provider callback credential (see [`../credentials-and-auth/provider-callback.md`](../credentials-and-auth/provider-callback.md)) | + +Polling implementation: a per-provider goroutine (or equivalent worker) runs +the declared interval; each tick fires the provider's discovery endpoint with +the standard query payload. The Discovery Service shares the same scheduler +infrastructure used for Realized State drift detection (see +[`../convergence-engine/recovery-and-retry.md` §4](../convergence-engine/recovery-and-retry.md)). + +Push implementation: providers POST to the provider callback API; the +Provider Callback API validates mTLS + interaction credential per +[`../credentials-and-auth/provider-callback.md`](../credentials-and-auth/provider-callback.md), +then routes the payload to the engine. + +--- + +## 3. Enrichment policy enforcement + +UDLM defines auto-assignment signal priority (explicit tag → resource group → +request history → network/location → naming → provider context → none). DCM +enforces the priority order via a Transformation policy that evaluates against +each candidate signal and produces the assignment decision. + +### 3.1 Signal evaluation algorithm + +``` +For each newly ingested entity: + ▼ Run the signal priority chain (declared in platform layer) + │ For each signal in priority order: + │ Evaluate signal against entity metadata + │ If signal produces an unambiguous tenant_uuid → use it + │ Else continue to next signal + │ + ▼ If two or more signals produce conflicting tenant_uuids: + │ Higher-priority signal wins + │ Record conflict in ingestion_record (ingestion_confidence: medium regardless) + │ + ▼ If no signal produces a result: + │ Assign to __transitional__ tenant + │ ingestion_confidence: low + │ + ▼ Write ingestion_record with assignment_method, assignment_signal, + and ingestion_confidence +``` + +### 3.2 Profile-driven enrichment policy + +Profiles control: + +- Whether auto-assignment is permitted (always permitted; what varies is the + confidence threshold for auto-promotion) +- Which signals are enabled (organizations can disable signals via platform layer) +- Whether enrichment must complete within a deadline before escalation + +Per-profile signal priority is declared in +`platform/ingestion/signal-priority` (per `ING-012`). `explicit_tenant_tag` +always has highest priority; `default_tenant` always has lowest; middle +signals are reorderable. + +--- + +## 4. Transitional tenant execution + +The `__transitional__` tenant is a DCM system-managed artifact created at +bootstrap. The engine ensures: + +- It cannot be deleted (ING-003) +- It cannot be renamed (ING-003) +- It cannot be used for new resource provisioning (only ingestion assignment) +- Entities in it are fully auditable and visible + +### 4.1 Residency monitor + +A background worker scans the transitional tenant on the cadence declared in +its governance config (default daily): + +``` +For each entity in __transitional__: + ▼ age = now - ingestion_timestamp + ▼ If age > max_residency_days (default 90): + │ Apply on_max_residency action: + │ escalate → notification to platform admin + │ block → entity flagged; no further enrichment until resolved + │ alert → notification to ingestion administrator +``` + +The action is configurable per deployment. `fsi`/`sovereign` profiles default +to `block` and shorter `max_residency_days`. + +--- + +## 5. Ingestion scheduling + +The engine maintains a priority queue for ingestion work (separate from the +Discovery Scheduler's queue, but using the same infrastructure): + +``` +Priority order: + 1. Critical — security-relevant brownfield (unknown resource at sensitive provider) + 2. High — manual import + active enrichment requests + 3. Standard — scheduled brownfield discovery passes + 4. Background — bulk migration ingestion +``` + +Bulk promotion is supported with profile-governed batch sizes and rollback +windows (per `ING-013`): + +| Profile | Max per Bulk | Approval Required | +|---|---|---| +| homelab | Unlimited | No | +| dev | 1000 | No | +| standard | 500 | Recommended | +| prod | 100 | Yes | +| fsi | 50 | Yes + dual approval | +| sovereign | 25 | Yes + dual approval | + +A single `BULK_PROMOTE` audit record covers each bulk action with the full +member list. + +--- + +## 6. Conflict detection and resolution + +UDLM defines the confidence descriptor model and the authority declaration +contract. DCM performs ingestion-time conflict detection per +[udlm/contracts/information-providers-advanced.md](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md): + +``` +Information Provider push event received + │ + ▼ 1. Schema validation against provider's declared schema version + │ Strict reject on violation OR lenient warn per policy + │ + ▼ 2. Authority scope check + │ Is provider authorized to assert these fields on this entity? + │ Reject unauthorized field assertions (INF-001) + │ + ▼ 3. Confidence score computation + │ Per-field score using standard formula (UDLM Section 2.5) + │ + ▼ 4. Conflict detection + │ For each field: + │ Same value across providers → corroboration (confidence multiplier 1.15) + │ Different value → conflict record created + │ No existing value → new assertion (accept) + │ + ▼ 5. Conflict resolution policy + │ Apply declared strategy: + │ higher_authority_wins → use higher authority_level value + │ higher_confidence_wins → use higher computed score + │ higher_priority_wins → use value from higher-priority provider + │ escalate → conflict record; existing value retained; human resolves + │ merge → combine values (array/set fields only) + │ + ▼ 6. Entity record update with full field provenance + │ + ▼ 7. INGEST audit record with all field changes, conflicts, scores +``` + +### 6.1 Authority scope conflict at registration + +When a new provider declares authority over a field already claimed at the +same or higher level: + +``` +DCM checks for existing primary authority on the field + │ + ├── No existing primary → register; no conflict + │ + └── Existing primary provider found: + Create authority_scope_conflict_record + Action required: + - Demote new to secondary, or + - Demote existing to secondary, or + - Declare explicit resolution strategy + Provider registration blocked until resolved +``` + +--- + +## 7. Write-back implementation + +Information Providers may declare write-back capability. DCM triggers +write-back only via policy — never automatically (`INF-002`). + +```yaml +# Policy that triggers write-back: +policy: + type: transformation + placement_phase: post + rule: > + If action IN [CREATE, STATE_TRANSITION, DELETE] + AND resource_type == Compute.VirtualMachine + THEN trigger_write_back: + provider_uuid: + operation: update + fields: [hostname, ip_address, lifecycle_state, owner_business_unit] +``` + +DCM's write-back executor: + +1. Resolves the provider's write-back endpoint from its registration +2. Issues a `dcm_interaction` credential scoped to the provider + operation +3. Sends the write-back payload (only fields declared in the policy) +4. Records an `ENRICH` audit record with `source_type: information_provider_write_back` +5. On failure: retries per the policy's retry config, then logs and notifies + the policy owner + +--- + +## 8. Air-gapped verification + +UDLM defines three modes for Information Provider verification in air-gapped +environments. DCM implements all three: + +### 8.1 Mode 1 — Pre-verified signed bundle (recommended) + +A signed YAML bundle is generated on an online workstation, transferred via +approved secure channel, and imported on the air-gapped DCM: + +``` +DCM bundle import endpoint receives air_gapped_provider_bundle + │ + ▼ Verify signature against organization's pre-installed public key + ▼ Verify bundle expires_at not exceeded + ▼ Extract provider_registration, tls_certificate_chain, schema_definitions + ▼ Submit through standard provider registration flow with bundle attribution + ▼ Provider is registered without external internet contact +``` + +### 8.2 Mode 2 — Internal mTLS (internal providers) + +Providers internal to the organization register with +`air_gap_mode: internal_only`. DCM validates using internal mTLS certificates +issued by the organization's internal CA (FreeIPA CA or equivalent). No +external connectivity required. + +### 8.3 Mode 3 — Periodic online re-verification + +For environments air-gapped most of the time but with occasional connectivity +windows: + +```yaml +provider_verification: + mode: periodic_online + cache_ttl: P30D + on_cache_expiry: + minimal: continue + dev: alert + standard: alert + prod: suspend + fsi: suspend + sovereign: suspend +``` + +DCM's verification scheduler attempts a re-verification at the cache_ttl; +on failure, applies the profile's expiry behavior. + +--- + +## 9. Provider priority and fallback logic + +When multiple Information Providers assert values for the same field and a +priority must be resolved, DCM applies (per `INF-007`): + +``` +1. Authority level (primary > secondary > advisory) +2. Within same authority level: provider priority (numeric, higher wins) +3. If still tied: most recent timestamp wins +``` + +On provider degradation or suspension (trust score < 60 per `INF-009`): + +- `degraded`: confidence multiplier reduces to 0.75; provider continues to + serve data but with reduced effective confidence +- `suspended`: provider stops accepting new pushes; existing data remains + but ages out per freshness multipliers; fallback providers in the priority + chain take over + +Fallback registration is via the provider's registration: + +```yaml +information_provider_registration: + authority_level: primary + fallback_providers: + - provider_uuid: + activate_when: this_provider_suspended | this_provider_degraded +``` + +--- + +## 10. Confidence aggregation API (DCM implementation of INF-010) + +DCM exposes the per-entity confidence aggregation endpoint: + +``` +GET /api/v1/entities/{uuid}/confidence +→ { + "entity_uuid": "", + "overall_band": "high", # lowest band across all fields + "field_summaries": [ + { "field": "owner_business_unit", "band": "high", "score": 86, ... }, + { "field": "cost_center", "band": "medium", "score": 54, "corroboration": "contested" } + ], + "lowest_confidence_fields": [ + { "field": "cost_center", "reason": "contested" }, + { "field": "asset_tag", "reason": "stale" } + ], + "computed_at": "" + } +``` + +Overall band reflects the lowest field band (conservative). Aggregation is +computed on demand — never stored (freshness changes continuously). + +--- + +## 11. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `ING-001-DCM` | DCM assigns every ingested entity to exactly one Tenant (real or __transitional__) before it is eligible for new requests | +| `ING-002-DCM` | DCM blocks PENDING/ENRICHING entities from being parent for new allocated resource claims | +| `ING-003-DCM` | DCM's __transitional__ tenant is system-managed; cannot be deleted, renamed, or used for new provisioning | +| `ING-004-DCM` | Every DCM-ingested entity carries an ingestion_record in its provenance chain | +| `ING-005-DCM` | DCM fires escalation action on entities exceeding max_residency_days in __transitional__ | +| `ING-006-DCM` | DCM requires explicit actor authorization before promoting brownfield entities to PROMOTED state | +| `ING-007-DCM` | At promotion, DCM promotes the Discovered State record to Realized State | diff --git a/architecture/ingestion/workload-analysis.md b/architecture/ingestion/workload-analysis.md new file mode 100644 index 0000000..fa89fab --- /dev/null +++ b/architecture/ingestion/workload-analysis.md @@ -0,0 +1,267 @@ +--- +Document Status: 📋 Draft — Specification in Progress +Document Type: Capability Specification +Maps to: udlm/lifecycle/ingestion-model.md +--- + +# DCM — Workload Analysis + +> **Implements contracts defined in UDLM**: +> [udlm/lifecycle/ingestion-model.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/ingestion-model.md). +> UDLM defines the ingestion model — how discovered resources enter the data +> model and are classified through the ingestion lifecycle. DCM operationalizes +> Workload Analysis as the active classification stage of that lifecycle: +> results are delivered as Information Provider payloads and stored as +> `process_resource_entity` instances of type `Analysis.WorkloadProfile`. + +**Document Status:** 📋 Draft — Specification in Progress +**Document Type:** Capability Specification +**Related Documents:** [Ingestion Model](https://github.com/croadfeldt/udlm/blob/main/lifecycle/ingestion-model.md) | [Information Providers](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers.md) | [Resource Type Hierarchy](https://github.com/croadfeldt/udlm/blob/main/entities/resource-type-hierarchy.md) | [Discovery and Drift](../control-plane/components.md) | [Kubernetes Compatibility](../../docs/specifications/kubernetes-compatibility.md) + +> **AEP Alignment:** API endpoints follow [AEP](https://aep.dev) conventions. +> Workload analysis results are delivered as Information Provider payloads +> and stored as `process_resource_entity` instances of type `Analysis.WorkloadProfile`. + +--- + +## 1. Purpose + +Workload Analysis is the DCM capability that actively classifies discovered resources +by their operational characteristics — what they are, how they behave, what lifecycle +model should apply to them, and which DCM Resource Type they best map to. + +It answers questions that passive discovery cannot: +- *"This VM was discovered — is it a web server, a database, a batch processor?"* +- *"This workload can be migrated to containers — what is its archetype?"* +- *"This resource has no DCM UUID — what is the minimum viable Resource Type we can + assign it for lifecycle management?"* + +Workload Analysis is the bridge between **Discovered State** (what exists) and +**Intent State** (what should be managed). Without it, brownfield ingestion stalls +at the enrichment phase because the Tenant, Resource Type, and lifecycle ownership +cannot be automatically determined. + +--- + +## 2. Relationship to Existing Capabilities + +``` +Discovery (DRC) Workload Analysis Ingestion ([ingestion-model.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/ingestion-model.md)) + │ │ │ + ▼ ▼ ▼ +Discovered State WorkloadProfile entity INGESTED → ENRICHING +(what physically (what it IS and what → PROMOTED → OPERATIONAL + exists today) lifecycle applies) +``` + +**Workload Analysis is a DCM-managed process resource** (`Analysis.WorkloadProfile`) +that fires as part of the brownfield ingestion pipeline. It is triggered by the +Discovery Scheduler when a new resource enters Discovered State without a matching +DCM entity in Realized State. + +It can also be triggered manually by a platform admin to re-classify a resource +whose operational profile has changed (e.g., a VM that was a web server and is +now a database). + +--- + +## 3. WorkloadProfile Entity + +Workload Analysis produces a `process_resource_entity` of type `Analysis.WorkloadProfile`: + +```yaml +workload_profile_entity: + entity_uuid: + entity_type: process_resource + resource_type: Analysis.WorkloadProfile + lifecycle_state: OPERATIONAL # while active; DECOMMISSIONED when superseded + + # Linked to the resource being analyzed + subject_entity_uuid: # the VM, container, or other resource + subject_discovered_state_uuid: + + classification: + resource_type_match: # best-fit DCM Resource Type + primary: Compute.VirtualMachine + confidence: high # high | medium | low | undetermined + alternatives: + - resource_type: Platform.Container + confidence: medium + rationale: "Workload is containerizable per MTA assessment" + + workload_archetype: # operational classification + type: web_server | database | batch_processor | message_broker | + api_gateway | cache | storage | monitoring | unknown + confidence: high + signals: [port_scan, process_list, resource_utilization_pattern] + + migration_readiness: # if MTA integration is active + containerization_score: 7 # 1-10 + blockers: [] + suggested_target: Platform.KubernetesDeployment + mta_report_ref: # link to MTA HTML report if available + + lifecycle_recommendation: + dcm_lifecycle_model: standard | stateful | ephemeral | infrastructure + rehydration_eligible: true + notes: "Application data on /data partition; OS on /; static replace eligible" + + analysis_metadata: + analyzed_at: + analysis_version: "1.0.0" # versioned analysis ruleset + information_providers_used: + - provider_uuid: + provider_type: information_provider + data_types_used: [port_scan, process_list, os_metadata] + analyst_actor_uuid: # null if automated; actor UUID if manual review +``` + +--- + +## 4. Analysis Pipeline + +Workload Analysis is an Orchestration Flow Policy that fires when a discovered +resource enters the enrichment phase: + +``` +discovery.new_entity_found + │ + ▼ Orchestration Step 1: Create WorkloadProfile entity (INGESTED state) + │ Linked to discovered resource via 'operational' relationship + │ + ▼ Orchestration Step 2: Gather signals from Information Providers + │ Port scan (network topology) + │ Process list (running services) + │ OS metadata (version, packages, mount points) + │ Resource utilization patterns (CPU/memory/disk I/O profile) + │ MTA assessment (if MTA Information Provider registered) + │ + ▼ Orchestration Step 3: Apply classification ruleset (Policy Engine) + │ Transformation Policy: compute workload_archetype from signals + │ Transformation Policy: compute resource_type_match from archetype + │ Transformation Policy: compute migration_readiness from MTA signals + │ Validation Policy (compliance): flag if confidence < medium for manual review + │ + ▼ Orchestration Step 4: Write WorkloadProfile to Realized State + │ WorkloadProfile entity → OPERATIONAL + │ + ▼ Orchestration Step 5: Trigger ingestion enrichment + WorkloadProfile classification informs: + - Tenant auto-assignment (if auto-assignment rules match) + - Resource Type assignment for the ingestion record + - Lifecycle model selection +``` + +--- + +## 5. MTA (Migration Toolkit for Applications) Integration + +When the MTA Information Provider is registered, Workload Analysis invokes it +as part of Step 2 above. MTA provides workload archetype classification and +containerization readiness scores for discovered workloads. + +```yaml +mta_information_provider_registration: + provider_type: information_provider + information_type: workload_analysis + display_name: "MTA — Migration Toolkit for Applications" + endpoint: https://mta.internal:8080/api/v1 + + capabilities: + workload_archetypes: + - web_server + - database + - batch_processor + - message_broker + provides_containerization_score: true + provides_migration_blockers: true + provides_target_recommendations: true + + query_interface: + # MTA receives discovered state payload and returns analysis + input: discovered_state_payload + output: mta_workload_report + async: true + callback_supported: true +``` + +The MTA integration is the primary implementation path for Workload Analysis in +Red Hat environments. In non-MTA environments, a custom Information Provider +implementing the same `workload_analysis` information type can be registered. + +--- + +## 6. Consumer API — Workload Analysis Endpoints + +``` +# Get the WorkloadProfile for a specific resource +GET /api/v1/resources/{entity_uuid}/workload-profile + +Response 200: +{ + "workload_profile_uuid": "", + "subject_entity_uuid": "", + "classification": { + "resource_type_match": { + "primary": "Compute.VirtualMachine", + "confidence": "high" + }, + "workload_archetype": { + "type": "web_server", + "confidence": "high" + }, + "migration_readiness": { + "containerization_score": 7, + "blockers": [], + "suggested_target": "Platform.KubernetesDeployment" + }, + "lifecycle_recommendation": { + "dcm_lifecycle_model": "standard", + "rehydration_eligible": true + } + }, + "analyzed_at": "" +} + +# Trigger a re-analysis of a resource +POST /api/v1/resources/{entity_uuid}/workload-profile:analyze + +Request body: +{ + "reason": "Role change — web server migrated to database role", + "include_mta": true +} + +Response 200 — returns Operation: +{ + "name": "/api/v1/operations/{request_uuid}", + "done": false, + "metadata": { "stage": "ANALYSIS_INITIATED", "resource_uuid": "{entity_uuid}" } +} + +# List all resources with a given workload archetype (platform admin) +GET /api/v1/admin/workload-analysis?archetype=web_server&confidence=high + +Response 200: +{ + "items": [ { "entity_uuid": "...", "resource_type": "...", "archetype": "..." } ], + "next_page_token": "..." +} +``` + +--- + +## 7. System Policies + +| Policy | Rule | +|--------|------| +| `WLA-001` | Workload Analysis fires automatically for every entity entering Discovered State without a matching Realized State record. It is not optional — it is part of the brownfield ingestion pipeline. | +| `WLA-002` | WorkloadProfile entities are versioned. When re-analysis produces a different classification, the old WorkloadProfile enters DECOMMISSIONED state and a new one is created. The chain is preserved for audit. | +| `WLA-003` | If classification confidence is `low` or `undetermined`, the WorkloadProfile compliance validation policy fires and the entity is routed to manual review before ingestion can proceed to PROMOTED. | +| `WLA-004` | The MTA Information Provider is the reference implementation for workload_analysis information type in Red Hat environments. Custom implementations must provide the same output schema. | +| `WLA-005` | Workload Analysis results are stored in Realized State as `process_resource_entity` instances. They are immutable once written — re-analysis creates a new entity, not an update. | +| `WLA-006` | Migration readiness scores and archetype classifications are advisory — they inform human decision-making and Orchestration Flow Policies but do not automatically trigger migrations. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/integrations/automation-outcome-providers.md b/architecture/integrations/automation-outcome-providers.md new file mode 100644 index 0000000..334af5b --- /dev/null +++ b/architecture/integrations/automation-outcome-providers.md @@ -0,0 +1,126 @@ +# Automation as DCM services — build for the outcome, not the method + +> **North star — an intent-based service model, where the outcome *is* the intent.** The goal is to get +> organizations to focus on **outcomes, not methods**: declaring the desired **outcome** *is* the +> expression of intent — there is no separate intent artifact to interpret. The catalog/system deploys +> that outcome; *how* it's achieved is the platform's concern. This maps directly onto UDLM's four +> states, which open with **Intent**: the **Intent state is the declared outcome**, and realization is +> the journey Intent → Requested → Realized → Discovered. DCM is the system that deploys outcomes. This +> document applies the north star to automation — the principle generalizes to every service in the +> catalog. + +**Why:** the consumer's ask is *"I need this thing on this target"* — `Observability.LogShipper` on +`host-Z`. That is an **outcome**. The automation that makes it true (an Ansible role, an AAP job +template, a script, a container) is a *method*. DCM providers must be built for the **outcome**, and the +method must be an **encapsulated, swappable internal mechanism** — never a thing the consumer, the +Resource Type, or DCM placement sees. This note settles "do we need a generic automation spec?" — **no** +— and how the homelab's ansible roles become DCM-consumable services. + +## The rule + +> **Provider = outcome. Method = hidden.** + +- **Consumer contract:** Resource Type + target. `LogShipper` on `host-Z`. Nothing about *how*. +- **Provider:** identified by the **outcome capability** it offers — it `realize_resources` of type + `Observability.LogShipper` (and other outcome types). It is **not** "the Ansible provider" or "the AAP + provider." Its name is the outcome family, not the engine. +- **Method:** inside the provider. Today: `ansible-runner` invoking the the estate's private Ansible repo `alloy` role. + Tomorrow: an AAP job template, or a container. **Swapping the method is an internal provider change + with zero impact** on the type, the consumer, or DCM. That swap-invisibility *is* the proof the + boundary is correct. + +## Outcome-derived services — declare the goal, derive the work + +The leaf outcome (`LogShipper` on a host) is the floor. The **goal** is to drive **outcome-derived +services**: a consumer declares a *higher-order outcome* — "host-Z is **observable**", "host-Z is +**production-baseline**" — and the concrete services it needs are **derived** from it, not hand-picked. + +``` +Outcome (goal) "host-Z is observable" + | derive +Derived services Observability.LogShipper + Observability.MetricsExporter ( + … ) + | realize (provider; method hidden) +Realized on host-Z +``` + +This is the **Composite Service** model — a composite outcome whose **constituents are the derived +services** (the existing depends-on DAG) — and it is the answer to the open **Application Definition +Language** question (`adr/016-application-definition-language.md`): an *outcome is the application*, and +it derives its constituents. + +**Where the derivation lives splits on the Data ⇄ Policy line:** +- **Fixed** outcome (observable *always* = this service set) → a **declarative composite** (data). +- **Target-conditional** outcome (a Pi derives X, a server derives Y, by host attributes) → **Policy**. + +No new machinery — the same boundary, the same four-state lifecycle, now on the composite outcome (its +Discovered state aggregates its constituents' health). The single data-driven provider still realizes +each leaf; the derivation sits above it. + +## Why this beats method-providers + +- **Four-state lifecycle works on the outcome.** Intent (want shipping) → Requested (assembled with + sink + labels) → Realized (shipper running) → **Discovered** (is it healthy / still shipping? → drift). + You can reconcile *"is the LogShipper healthy,"* which you **cannot** do with *"did the playbook run."* + A method-provider (`Process.AnsiblePlaybook`) is fire-and-forget; an outcome-provider is reconcilable. +- **Audit/provenance** attach to a durable resource, not a one-shot job. +- **Engine independence by construction:** `ansible-runner` → AAP is a backend change, not a re-model. + +## No generic automation spec — the genericity lives in the provider + +There is **no automation in the data model**. The only "generic" part is an *implementation* detail +inside the outcome provider: a **data-driven Type → method-binding table**. + +``` +Observability.LogShipper -> ansible role 'alloy' (var-map: spec.sink.url -> loki_url, …) +Observability.MetricsExporter -> ansible role 'node_exporter' +… -> … +``` + +Adding a new outcome is **"define a Resource Type + add a mapping row"** — *not* writing a new provider. +The method binding (which role / which AAP template realizes which type, and the spec→vars mapping) is +the provider's **private catalog config** — the vendor-specific layer. It lives in the provider, **never +in the universal type**, exactly as Koku's native metric names live in its catalog item and not in the +FOCUS type (`koku-focus-adoption.md`). + +## raw ansible-runner vs AAP + +Not a consumer choice and not two Resource Types — both are **execution backends of the same outcome +provider**: +- **ansible-runner** — lightweight, homelab-grade. The starting backend. +- **AAP** — enterprise execution: RBAC, credential vault, job history, and **surveys (≈ UDLM E1 + constraint profiles)**. Slots in later as the provider's backend with **zero** type/consumer change. + +If that swap is truly zero-change, the abstraction held. + +## Running an automation is *also* an outcome — the automation as a service + +Even "just run automation X" fits the model — there is no second class. The outcome is **a service that +runs X**, not a fire-and-forget job. Its realized form is a **registered, invokable automation service**: +an **AAP job template** *is* exactly "a service to run an automation," as is a DCM-registered job or a +CronJob. That service is itself persistent and reconcilable — *does it exist? can it run? is its +definition current?* are all drift-checkable — and **each invocation is an audited run**. + +So every consumer ask is an outcome → a service. The only thing that differs is **what the service +provides**: + +| The service provides… | Example | Realized form | +|---|---|---| +| a running / configured **resource** | `Observability.LogShipper` | an agent running on the host | +| an **invokable automation** | a backup service, a cert-rotation service | a registered job (AAP template / CronJob) you can run | + +The generic **`Process.Automation`** type models the second row — *the automation-runner service*, a peer +outcome, not an exception. (Executor-neutral: the specific playbook / AAP template it wraps is provider +catalog config, never in the type.) The four-state lifecycle applies to the **service**; running it is an +audited event against that service. + +## Worked example (homelab) + +`Observability.LogShipper` (`udlm/registry/resource-types/observability.log-shipper.json`) — spec: +`{ target.host, sink.url, source, labels }` — realized by an outcome provider that runs the +the estate's private Ansible repo **`alloy`** role (journald → Loki). The consumer asks for a LogShipper on a host; the +provider naturalizes the spec into role vars, runs it, and reports `status` / `last_shipped_at` for drift. +The homelab's roles (`alloy`, `node_exporter`, `smartctl`, `fan_control`, …) become the first real DCM +**outcome** catalog — the live reference proving the model on actual automation. + +See also: `data-policy-boundary.md`, `adr/021-adopting-external-standards.md`, and UDLM +`design-principles/adopted-standards.md`. diff --git a/architecture/integrations/itsm.md b/architecture/integrations/itsm.md new file mode 100644 index 0000000..3a528f6 --- /dev/null +++ b/architecture/integrations/itsm.md @@ -0,0 +1,666 @@ +# DCM Data Model — ITSM Integration + +**Document Status:** 🔄 In Progress +**Document Type:** Architecture Reference — ITSM integration Type and ITSM Policy Type +**Related Documents:** [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) | [Notification Model](../runtime-features/notifications.md) | [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md) | [Authority Tier Model](https://github.com/croadfeldt/udlm/blob/main/governance/authority-tier-model.md) | [Consumer API Specification](../../docs/specifications/consumer-api-spec.md) + +> **Design principle:** DCM is built to *replace* the infrastructure ticket as the primary provisioning mechanism. ITSM integration is additive — it enriches DCM entities with ITSM metadata, enables ITSM-initiated requests, and provides bidirectional lifecycle traceability for organizations that need it for compliance. **DCM never requires an ITSM system to function.** +> +> Two new additions to the DCM architecture: +> 1. **ITSM integration** — a new Provider type (12th) that speaks ITSM system APIs bidirectionally +> 2. **ITSM Policy** — a new Policy output type (8th) that triggers ITSM actions as a side-effect of DCM pipeline events + +--- + +## 1. ITSM Integration + +### 1.1 What ITSM Integration Is + +ITSM integration connects DCM to an external IT Service Management system. It handles: + +- **Outbound**: DCM lifecycle events → ITSM records (create change requests, update CMDB CIs, close incidents, link tickets to entities) +- **Inbound**: ITSM approvals and decisions → DCM (change approval recorded via approval vote API, request initiation from ITSM workflow) +- **Sync**: ITSM record references stored on DCM entities as business data (bidirectional link) + +ITSM integration is **not** a separate provider type (it doesn't realize resources), **not** a notification service (though it may create notification-like records), and **not** a External Policy Evaluator (though ITSM approval status may inform DCM policies). It is its own type because it has a bidirectional contract, manages external record lifecycle, and requires specific capability declarations around ITSM system connectivity. + +### 1.2 Data Flow + +``` +DCM lifecycle event fires (e.g. request.dispatched) + │ + ▼ ITSM Policy evaluates (see Section 3) + │ Determines: should an ITSM action fire? Which action? + │ + ▼ ITSM integration receives action request + │ Translates to target system's API format + │ Calls ITSM system (ServiceNow, Jira, etc.) + │ + ▼ ITSM system creates/updates record + │ Returns record ID (CHG0012345, INC-4821, etc.) + │ + ▼ ITSM integration stores reference on DCM entity + │ entity.business_data.itsm_references[] updated + │ + ▼ ITSM integration reports back to DCM + itsm_reference_created event published + External record ID in audit record + +───────────────────────────────────────────── + +ITSM system approves a change record + │ + ▼ ITSM system calls DCM API (via webhook or polling) + │ POST /api/v1/admin/approvals/{uuid}:vote + │ { decision: "approve", recorded_via: "servicenow", + │ external_reference: "CHG0012345" } + │ + ▼ DCM records approval vote + │ approval.decision_recorded event + │ + ▼ Pipeline resumes if quorum/tier satisfied +``` + +### 1.3 Capability Declaration + +```yaml +itsm_provider_capabilities: + itsm_system: servicenow | jira_service_management | bmc_remedy | bmc_helix | + freshservice | zendesk | pagerduty | opsgenie | manageengine | + cherwell | topdesk | generic_rest + + # What this provider can do + supported_actions: + - create_change_request # create a change record for DCM provisioning events + - update_change_request # update change record on state transitions + - close_change_request # close change record on realization/failure + - create_incident # create incident for failures, drift, security events + - update_incident # update incident on resolution + - close_incident # close incident on recovery + - update_cmdb_ci # update CMDB configuration item record + - create_cmdb_ci # create new CMDB CI for realized entities + - retire_cmdb_ci # retire CMDB CI on decommission + - create_service_request # create service request record + - link_parent_record # link DCM entity to existing ITSM record + - inbound_approval # accept approval decisions from ITSM system + - inbound_request_initiation # allow ITSM workflows to submit DCM requests + + # System connectivity + endpoint_url: # ITSM system API base URL + api_version: # system-specific API version + auth_credential_uuid: # references credential management service + + # Bidirectional webhook (for inbound) + inbound_webhook: + enabled: + secret_credential_uuid: # HMAC secret for webhook verification + + # Field mappings (system-specific) + field_mapping_ref: # path to field mapping YAML in Layer Store + + # CMDB CI type mapping + cmdb_ci_type_map: + - dcm_resource_type: Compute.VirtualMachine + itsm_ci_type: cmdb_ci_server # ServiceNow CI class + - dcm_resource_type: Network.VLAN + itsm_ci_type: cmdb_ci_network_gear + - dcm_resource_type: Storage.Volume + itsm_ci_type: cmdb_ci_storage_device +``` + +### 1.4 Required API Endpoints (ITSM integration implements) + +``` +POST {provider_base}/actions # DCM submits action requests +GET {provider_base}/actions/{action_id} # DCM checks action status +GET {provider_base}/records/{record_id} # DCM retrieves record status +POST {provider_base}/inbound # ITSM system sends inbound events +GET /health # standard OIS health check +``` + +### 1.5 DCM Entity ITSM References + +Realized entities gain an `itsm_references` block in business data: + +```yaml +itsm_references: + - system: servicenow + provider_uuid: + record_type: change_request + record_id: "CHG0012345" + record_url: "https://corp.service-now.com/nav_to.do?uri=change_request.do?sys_id=..." + created_at: + status: approved # DCM's view of the record status + last_synced_at: + + - system: jira_service_management + provider_uuid: + record_type: incident + record_id: "INC-4821" + record_url: "https://corp.atlassian.net/browse/INC-4821" + created_at: + status: open + last_synced_at: +``` + +--- + +## 2. Supported ITSM Systems + +### 2.1 ServiceNow + +**API:** REST Table API (`/api/now/table/`), Business Rule webhooks, Flow Designer + +```yaml +# ServiceNow ITSM integration registration +itsm_provider_registration: + provider_handle: "servicenow-prod" + itsm_system: servicenow + endpoint_url: "https://corp.service-now.com" + api_version: "v2" + auth_credential_uuid: # api_key or oauth2 credential + + supported_actions: + - create_change_request # → change_request table + - update_change_request + - close_change_request + - create_incident # → incident table + - update_cmdb_ci # → cmdb_ci_server (or mapped class) + - create_cmdb_ci + - retire_cmdb_ci + - inbound_approval # Change Advisory Board approval → DCM vote + + # ServiceNow-specific field mapping + change_request_template: + assignment_group: "Infrastructure Automation" + category: "Software" + risk: "2" # Low + impact: "3" # Low + # DCM fields injected at runtime: + short_description: "DCM: Provision {resource_type} '{entity_handle}'" + description: "Requested by: {actor_handle}\nTenant: {tenant_handle}\nDCM Request: {request_uuid}" + + # CAB approval → DCM vote mapping + inbound_approval: + webhook_url: "https://dcm.corp/api/v1/admin/approvals/{approval_uuid}:vote" + trigger_on: "change_request.state → 'Approved'" + decision_field: "state" + decision_map: + "Approved": "approve" + "Rejected": "reject" + "Cancelled": "reject" + external_reference_field: "number" # → CHG0012345 + + cmdb_ci_type_map: + - dcm_resource_type: Compute.VirtualMachine + itsm_ci_type: cmdb_ci_server + - dcm_resource_type: Network.VLAN + itsm_ci_type: cmdb_ci_netgear + - dcm_resource_type: Storage.Volume + itsm_ci_type: cmdb_ci_disk + - dcm_resource_type: Kubernetes.Cluster + itsm_ci_type: cmdb_ci_kubernetes_cluster +``` + +**Inbound: CAB approval flow** + +``` +ServiceNow Change Advisory Board approves CHG0012345 + │ + ▼ ServiceNow Business Rule fires on state change → "Approved" + │ Calls DCM webhook: POST /api/v1/admin/approvals/{uuid}:vote + │ Headers: X-ServiceNow-Signature: + │ Body: { decision: "approve", recorded_via: "servicenow", + │ external_reference: "CHG0012345" } + │ + ▼ DCM verifies HMAC signature against secret_credential_uuid + │ Records approval vote + │ Pipeline resumes if tier satisfied +``` + +### 2.2 Jira Service Management (Atlassian) + +**API:** REST API v3, Atlassian Connect webhooks, Automation rules + +```yaml +itsm_provider_registration: + provider_handle: "jira-service-mgmt-prod" + itsm_system: jira_service_management + endpoint_url: "https://corp.atlassian.net" + api_version: "3" + auth_credential_uuid: # API token or OAuth2 + + supported_actions: + - create_change_request # → Jira issue (Change type) + - update_change_request + - close_change_request + - create_incident # → Jira issue (Incident type) + - create_service_request # → Jira issue (Service Request type) + - inbound_approval # Jira Change approval → DCM vote + + change_request_template: + project_key: "OPS" + issue_type: "Change" + summary: "DCM: {resource_type} '{entity_handle}'" + description: | + *Requested by:* {actor_handle} + *Tenant:* {tenant_handle} + *DCM Request UUID:* {request_uuid} + *Catalog Item:* {catalog_item_handle} + priority: "Medium" + labels: ["dcm-automated", "{tenant_handle}"] + + inbound_approval: + webhook_url: "https://dcm.corp/api/v1/admin/approvals/{approval_uuid}:vote" + trigger_on: "issue.status → 'Approved'" + decision_map: + "Approved": "approve" + "Declined": "reject" + external_reference_field: "key" # → OPS-4821 +``` + +### 2.3 BMC Remedy / Helix ITSM + +**API:** REST API (Remedy AR System REST), webhook callbacks + +```yaml +itsm_provider_registration: + provider_handle: "bmc-helix-prod" + itsm_system: bmc_helix + endpoint_url: "https://remedy.corp.example.com/api/arsys/v1" + api_version: "v1" + + supported_actions: + - create_change_request # → CHG:Infrastructure Change + - update_change_request + - close_change_request + - create_incident # → HPD:Help Desk + - update_cmdb_ci # → AST:Config Item + - inbound_approval + + change_request_template: + form: "CHG:Infrastructure Change" + Location_Company: "{tenant_handle}" + Summary: "DCM: Provision {resource_type} '{entity_handle}'" + Categorization_Tier_1: "Infrastructure" + Categorization_Tier_2: "Provisioning" + Change_Type: "Normal" +``` + +### 2.4 Freshservice + +```yaml +itsm_provider_registration: + provider_handle: "freshservice-prod" + itsm_system: freshservice + endpoint_url: "https://corp.freshservice.com/api/v2" + + supported_actions: + - create_change_request + - update_change_request + - close_change_request + - create_incident + - create_service_request + + change_request_template: + type: "Normal" + risk: "Low" + impact: "Low" + subject: "DCM: {resource_type} '{entity_handle}'" + description: "Tenant: {tenant_handle} | Actor: {actor_handle} | Request: {request_uuid}" + group_id: +``` + +### 2.5 PagerDuty (Incident Management) + +```yaml +itsm_provider_registration: + provider_handle: "pagerduty-prod" + itsm_system: pagerduty + endpoint_url: "https://api.pagerduty.com" + + supported_actions: + - create_incident # for DCM failures, drift, security events + - update_incident + - close_incident + + # PagerDuty Events API v2 + incident_template: + service_id: + escalation_policy_id: + payload: + summary: "DCM {event_type}: {entity_handle}" + severity: "{{ drift_severity | map: critical→critical, significant→error, moderate→warning, minor→info }}" + source: "dcm" + custom_details: + entity_uuid: "{entity_uuid}" + tenant: "{tenant_handle}" + dcm_event: "{event_type}" +``` + +### 2.6 Generic REST (Custom ITSM) + +For ITSM systems not natively supported, the `generic_rest` type allows template-based HTTP calls: + +```yaml +itsm_provider_registration: + provider_handle: "custom-itsm-prod" + itsm_system: generic_rest + endpoint_url: "https://itsm.corp.example.com/api" + + action_templates: + - action: create_change_request + method: POST + path: "/changes" + headers: + Content-Type: "application/json" + X-API-Key: "{{ credential_value }}" + body_template: | + { + "title": "DCM: {{ resource_type }} '{{ entity_handle }}'", + "requested_by": "{{ actor_handle }}", + "category": "Infrastructure", + "external_id": "{{ request_uuid }}" + } + response_id_path: "$.id" # JSONPath to extract record ID from response + + - action: inbound_approval + inbound_field: "status" + decision_map: + "approved": "approve" + "rejected": "reject" +``` + +--- + +## 3. ITSM Policy Type + +### 3.1 What an ITSM Policy Is + +An **ITSM Policy** is a new DCM Policy output type (7th, alongside Validation, Transformation, Recovery, Orchestration Flow, Governance Matrix Rule, and Lifecycle Policy). + +It fires as a **side-effect policy** — it does not block pipeline execution (it is not a compliance validation policy) and does not transform the payload. It fires on a DCM event and triggers an ITSM action via a registered ITSM integration. The pipeline continues whether or not the ITSM action succeeds; ITSM failures are logged and alerted but do not block DCM operations. + +**Key distinction:** An ITSM Policy is about *record-keeping and integration* with external governance systems. A compliance validation policy is about *allowing or blocking* operations. These are complementary, not competing. + +### 3.2 Output Schema + +```yaml +# ITSM Policy output schema +itsm_policy_output: + type: itsm_action # new output type identifier + + # Required + itsm_provider_uuid: # which ITSM integration to call + action: create_change_request | update_change_request | close_change_request | + create_incident | update_incident | close_incident | + update_cmdb_ci | create_cmdb_ci | retire_cmdb_ci | + create_service_request | link_parent_record + + # Payload — fields to pass to ITSM integration + # Supports template variables from the triggering event payload + action_payload: + : + + # How to handle ITSM failure + on_failure: log_and_continue | alert_and_continue | alert_only + + # Store the ITSM record reference on the DCM entity (optional) + store_reference_on_entity: + reference_label: # human-readable label for the reference + + # Require ITSM record creation before dispatch (optional — see note) + block_until_created: # default: false + block_timeout: # max wait if block_until_created: true +``` + +> **`block_until_created`:** When `true`, the ITSM Policy behaves like a pre-dispatch gate — DCM waits for the ITSM record to be created before dispatching to the Service Provider. This is used when organizational policy requires a change record to exist before any provisioning begins. When `false` (default), the ITSM record is created in parallel with or after dispatch — suitable for notification-only use cases. + +### 3.3 Example Policies + +#### Policy 1: Create Change Request on Dispatch (ServiceNow) + +```yaml +policy_handle: "create-change-on-dispatch" +policy_type: itsm_action +enforcement_level: soft +status: active + +match: + payload_type: request.dispatched + conditions: + - field: resource_type + operator: in + value: [Compute.VirtualMachine, Storage.Volume, Network.VLAN] + +output: + type: itsm_action + itsm_provider_uuid: + action: create_change_request + action_payload: + short_description: "DCM: Provision {{ resource_type }} '{{ entity_handle }}'" + description: | + Automated provisioning via DCM. + Request UUID: {{ request_uuid }} + Actor: {{ actor_handle }} + Tenant: {{ tenant_handle }} + Catalog Item: {{ catalog_item_handle }} + risk: "{{ risk_score | map: <25→'Low', <60→'Medium', else→'High' }}" + store_reference_on_entity: true + reference_label: "Change Request" + on_failure: alert_and_continue +``` + +#### Policy 2: Block Dispatch Until Change Record Exists (Compliance Gate) + +```yaml +policy_handle: "require-change-record-before-dispatch" +policy_type: itsm_action +enforcement_level: hard +status: active + +match: + payload_type: request.layers_assembled + conditions: + - field: tenant_handle + operator: in + value: [payments-team, pci-scope-team] + +output: + type: itsm_action + itsm_provider_uuid: + action: create_change_request + action_payload: + short_description: "DCM: {{ resource_type }} provision — {{ tenant_handle }}" + change_type: "Normal" + assignment_group: "Change Advisory Board" + store_reference_on_entity: true + reference_label: "Change Request (PCI Scope)" + block_until_created: true + block_timeout: PT30M + on_failure: alert_and_continue +``` + +#### Policy 3: Update CMDB on Realization + +```yaml +policy_handle: "sync-cmdb-on-realization" +policy_type: itsm_action +status: active + +match: + payload_type: entity.realized + conditions: + - field: resource_type + operator: in + value: [Compute.VirtualMachine, Compute.BareMetalServer] + +output: + type: itsm_action + itsm_provider_uuid: + action: create_cmdb_ci + action_payload: + name: "{{ entity_handle }}" + ip_address: "{{ realized_fields.primary_ip }}" + os: "{{ realized_fields.os_family }}" + managed_by: "DCM" + environment: "{{ tenant_handle }}" + correlation_id: "{{ entity_uuid }}" + store_reference_on_entity: true + reference_label: "CMDB CI" + on_failure: alert_and_continue +``` + +#### Policy 4: Create Incident on Drift (Jira) + +```yaml +policy_handle: "create-incident-on-critical-drift" +policy_type: itsm_action +status: active + +match: + payload_type: drift.detected + conditions: + - field: drift_severity + operator: in + value: [significant, critical] + +output: + type: itsm_action + itsm_provider_uuid: + action: create_incident + action_payload: + summary: "DCM Drift: {{ entity_handle }} — {{ drift_severity }}" + description: | + DCM has detected significant configuration drift. + Entity: {{ entity_handle }} ({{ entity_uuid }}) + Severity: {{ drift_severity }} + Drifted fields: {{ drifted_fields | count }} fields + Detected at: {{ discovered_at }} + View in DCM: https://dcm.corp/resources/{{ entity_uuid }}/drift + priority: "{{ drift_severity | map: critical→'Highest', significant→'High' }}" + labels: ["dcm-drift", "{{ resource_type | slugify }}"] + store_reference_on_entity: true + reference_label: "Drift Incident" + on_failure: log_and_continue +``` + +#### Policy 5: Retire CMDB CI on Decommission + +```yaml +policy_handle: "retire-cmdb-on-decommission" +policy_type: itsm_action +status: active + +match: + payload_type: entity.decommissioned + +output: + type: itsm_action + itsm_provider_uuid: + action: retire_cmdb_ci + action_payload: + correlation_id: "{{ entity_uuid }}" # find CI by DCM entity UUID + install_status: "7" # ServiceNow: Retired + retired_at: "{{ event_timestamp }}" + decommission_reason: "DCM decommission — {{ actor_handle }}" + on_failure: alert_and_continue +``` + +#### Policy 6: Close Change Record on Completion + +```yaml +policy_handle: "close-change-on-completion" +policy_type: itsm_action +status: active + +match: + payload_type: request.realized + conditions: + - field: entity.itsm_references[?(@.record_type=='change_request')].record_id + operator: exists + +output: + type: itsm_action + itsm_provider_uuid: + action: close_change_request + action_payload: + state: "3" # ServiceNow: Closed + close_code: "Successful" + close_notes: "Provisioning completed successfully by DCM. Entity: {{ entity_uuid }}" + on_failure: log_and_continue +``` + +--- + +## 4. ITSM integration System Policies + +| Policy | Rule | +|--------|------| +| `ITSM-001` | ITSM integrations implement the base Provider contract (PRV-001) including registration, health check, sovereignty declaration, and zero trust authentication. ITSM system connectivity credentials must reference a registered credential management service — no plaintext credentials in provider registration. | +| `ITSM-002` | DCM does not require ITSM integration to function. ITSM Policies with `on_failure: alert_and_continue` (the default) never block DCM pipeline execution. Organizations must explicitly set `block_until_created: true` to gate pipeline on ITSM record creation. | +| `ITSM-003` | Inbound events from ITSM systems must be authenticated. ITSM integrations must verify HMAC signatures or OAuth tokens on all inbound webhooks before forwarding to DCM. Unauthenticated inbound events are rejected and logged. | +| `ITSM-004` | ITSM record references stored on DCM entities follow entity lifecycle — they are included in the Realized State record, preserved through updates, and retained in the decommissioned entity record for audit purposes. | +| `ITSM-005` | ITSM Policies that use `block_until_created: true` must declare a `block_timeout`. If the ITSM system does not confirm record creation within the timeout, the policy fires `on_failure` behavior and the block is released — the pipeline continues. A blocked pipeline is never permanently stalled by ITSM unavailability. | +| `ITSM-006` | Field mappings between DCM entities and ITSM CI types must be declared in the ITSM integration capability registration. Unmapped resource types are silently skipped for CMDB sync actions. | +| `ITSM-007` | Template expressions in ITSM Policy `action_payload` fields must resolve using values from the triggering event payload. Template expressions that reference unavailable fields produce a warning in the audit record and substitute an empty string. They do not block ITSM action execution. | + +--- + +## 5. ITSM Policy System Policies + +| Policy | Rule | +|--------|------| +| `ITSM-POL-001` | ITSM Policies follow the full Policy base contract (B-policy-contract.md): lifecycle (developing → proposed → active), shadow mode validation, audit obligation on every evaluation, domain precedence. | +| `ITSM-POL-002` | ITSM Policies are side-effect policies — they do not produce pipeline decisions (allow/deny/transform). They may not be used as compliance validation policy substitutes except through the explicit `block_until_created: true` mechanism, which has its own timeout guarantee (ITSM-005). | +| `ITSM-POL-003` | ITSM Policy evaluation is recorded in the audit trail. The audit record includes: policy handle, matched event, ITSM provider UUID, action requested, ITSM record ID returned, and outcome (success/failure/timeout). | +| `ITSM-POL-004` | Multiple ITSM Policies may fire on the same event. All fire independently — one policy's failure does not prevent other ITSM Policies from executing. | + +--- + +## 6. Additions to the Foundations Document + +The foundations document provider type table gains a 12th row: + +| Provider Type | Capability | Data direction | +|--------------|-----------|----------------| +| **ITSM integration** | Bidirectional integration with ITSM systems; creates/updates ITSM records from DCM events; routes ITSM approvals back to DCM | DCM → ITSM (outbound) / ITSM → DCM (inbound) | + +The foundations document policy type table gains an 8th entry: + +| Policy Type | Output | Pipeline role | +|------------|--------|---------------| +| **ITSM Action** | Triggers action in connected ITSM system; optionally stores record reference on entity; optionally gates pipeline on record creation | Side-effect (non-blocking by default) | + +--- + +## 7. Event Catalog Additions + +Two new events for the Event Catalog ([event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md)): + +| Event Type | Urgency | Trigger | +|-----------|---------|---------| +| `itsm.record_created` | info | ITSM integration successfully created a record in external system | +| `itsm.record_failed` | medium | ITSM integration failed to create/update record; `block_until_created` timeout reached | + +These extend the existing event catalog with a new `itsm.*` domain prefix. + +--- + +## 8. Standards Catalog Addition + +ITSM integration standards and protocols used: + +| Standard | Use in DCM ITSM | +|----------|----------------| +| ServiceNow REST Table API | Primary integration for ServiceNow create/update/query | +| Jira REST API v3 | Primary integration for Atlassian Jira Service Management | +| BMC AR REST API v1 | Primary integration for BMC Remedy/Helix | +| PagerDuty Events API v2 | Incident creation for alert-type ITSM integrations | +| ITIL v4 Change Management | Conceptual framework for DCM change record lifecycle mapping | +| JSON:API | Standard used by several ITSM REST APIs | +| HMAC-SHA256 | Inbound webhook signature verification for all ITSM systems | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/integrations/kessel-evaluation.md b/architecture/integrations/kessel-evaluation.md new file mode 100644 index 0000000..366f547 --- /dev/null +++ b/architecture/integrations/kessel-evaluation.md @@ -0,0 +1,472 @@ +# DCM — Kessel Integration Evaluation + +**Document Status:** 📋 Draft — For Discussion +**Document Type:** Integration Evaluation — Pre-Implementation +**Purpose:** This document evaluates the potential integration of DCM with the [Kessel project](https://github.com/project-kessel) for identity/access management and resource inventory. It is intended as a basis for discussion with the Kessel development team. **No architectural changes should be made to DCM based on this document until alignment with the Kessel team is confirmed.** + +**Related Documents:** [Auth Providers](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [Universal Group Model](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md) | [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) | [Four States](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md) | [Accreditation and Zero Trust](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md) | [Control Plane Components](../control-plane/components.md) | [Provider Callback Authentication](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-callback-auth.md) + +**Related Projects:** [project-kessel](https://github.com/project-kessel) | [SpiceDB](https://github.com/authzed/spicedb) | [Google Zanzibar](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/) + +--- + +## 1. Executive Summary + +Kessel is a Red Hat project providing two capabilities: **Kessel Relations** (Relationship-Based Access Control built on SpiceDB, a Google Zanzibar implementation) and **Kessel Asset Inventory** (a hybrid cloud resource state tracking service with a common Protobuf/gRPC API). + +DCM has architecturally similar needs in both areas. The evaluation concludes: + +- **Kessel Relations** has strong alignment with DCM's access control requirements. The permission model maps cleanly, and the operational benefits — Zanzibar-style consistency, scalable graph traversal, shared source of truth across Red Hat products — are meaningful. Integration path exists via DCM's Auth Provider abstraction. + +- **Kessel Inventory** has partial alignment with DCM's Discovered State store. The fit is real but narrower than it might appear: Kessel Inventory is a current-state snapshot system; DCM's inventory is a four-state lifecycle model with field-level provenance, drift detection, and append-only audit. Integration path exists via DCM's data store abstraction. + +**Recommended next step:** Discussion with the Kessel development team to validate assumptions, confirm schema extensibility for DCM-specific resource types, and understand the Kessel Relations API stability and sovereign/air-gapped deployment model. + +--- + +## 2. What Kessel Provides + +### 2.1 Kessel Relations + +Kessel Relations is an authorization service built on [SpiceDB](https://github.com/authzed/spicedb), which implements the [Google Zanzibar](https://research.google/pubs/zanzibar-googles-consistent-global-authorization-system/) consistent global authorization model. + +**Core model — Relationship-Based Access Control (ReBAC):** +- Resources and subjects are defined in a typed schema +- Relationships between subjects and resources are stored as tuples: `subject:X relation:Y object:Z` +- Permissions are computed by evaluating the relationship graph: "does user X have permission `submit_request` on tenant T?" traverses all paths from X to T through groups, roles, and other relationships +- Transitive relationships are handled natively: if user X is a member of group G, and group G has `admin` on tenant T, X inherits `admin` on T + +**Zanzibar consistency model:** +- Snapshot reads: consistent reads at a point in time +- Zookie tokens: causality tokens that guarantee "read your own writes" without requiring full global linearizability — after writing a relationship tuple, the response includes a zookie; subsequent reads with that zookie are guaranteed to observe the write + +**gRPC API surface (from the Kessel project):** +- `CheckPermission(subject, permission, resource)` → allow/deny +- `LookupResources(subject, permission, resource_type)` → list of resources subject has permission on +- `LookupSubjects(resource, permission, subject_type)` → list of subjects that have permission on resource +- `WriteRelationships(tuples)` → write relationship tuples +- `DeleteRelationships(filter)` → remove relationship tuples + +### 2.2 Kessel Asset Inventory + +Kessel Asset Inventory is a resource tracking service designed to provide a unified inventory view across hybrid cloud infrastructure — OpenShift clusters, RHEL systems, edge devices, and other Red Hat-managed resources. + +**Core model:** +- Resources are described using a common Protobuf schema with a typed `ResourceType` and a `Spec` for type-specific fields +- Current state is tracked as an upsertable snapshot — last-write wins +- gRPC streaming API for push (providers send state updates) and pull (consumers query current state) +- Integration with Kessel Relations for auth-filtered inventory queries: "what resources of type X does subject Y have access to?" + +**Intended use case:** Giving tools like ACM (Advanced Cluster Management), Insights, and the Hybrid Cloud Console a single query surface for "what exists across my estate?" + +--- + +## 3. DCM's Current Model — What Needs to Be Understood + +Before evaluating integration, it is important to characterize what DCM already has in both areas. + +### 3.1 DCM's Authorization Model + +DCM's current authorization model has five components working together: + +**Auth Providers** ([auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md)) — DCM delegates authentication to registered Auth Providers (LDAP, OIDC, FreeIPA, Active Directory, mTLS). Auth Providers are registered through the standard Provider contract. Multiple Auth Providers can be active simultaneously. Auth Providers return: authenticated actor identity, group memberships, roles. + +**Universal Group Model** ([universal-groups.md](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md)) — DCM groups (`DCMGroup`) are typed by `group_class`. The classes relevant to authorization: +- `tenant_boundary` — the ownership and isolation boundary; every resource entity belongs to exactly one tenant +- `cross_tenant_authorization` — the formal mechanism for Tenant A to grant Tenant B access to a specific resource +- `policy_collection` — groups that activate policy sets +- DCMGroup membership is the basis for role resolution and policy application + +**RBAC via role mapping** — Auth Providers map external groups to DCM roles (`consumer`, `platform_admin`, `sre`, etc.). The Policy Engine uses roles + group membership to evaluate access. + +**Five-check boundary model** (the five-check boundary model) — Every interaction crosses five checks in sequence: identity verification → authorization → accreditation → data/capability matrix → sovereignty. Checks 1 and 2 are RBAC. Checks 3–5 are DCM-specific and involve accreditation records, data classification, and sovereignty zones. + +**Cross-tenant authorization records** — When Tenant A grants Tenant B access to a resource, a `cross_tenant_authorization` DCMGroup is created. The Policy Engine checks for the existence of this record when evaluating cross-tenant requests. + +**What DCM asks for in authorization decisions:** +1. Does actor X have role Y within tenant T? +2. What catalog items is actor X allowed to see? (RBAC-filtered list) +3. Can actor X perform operation O on resource R? (role + tenant ownership) +4. Does tenant T have a cross-tenant authorization to use resource R owned by tenant T2? +5. Is actor X a member of DCMGroup G with the required quorum? (approval gates, `authorized` tier) + +### 3.2 DCM's Inventory Model + +DCM's inventory is the **Four States model** ([four-states.md](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md)). This is meaningfully different from a general-purpose resource inventory. + +**Intent State** — The consumer's declared desired state. Stored as a GitOps artifact (PR-based workflow). Immutable after creation. Not a snapshot — it is the authoritative record of what was requested and why. + +**Requested State** — The assembled payload after layer enrichment, policy evaluation, and placement resolution. Write-once. Contains the full data model payload that was dispatched to the provider, including field-level provenance tracing every value back to its source. + +**Realized State** — An append-only event stream of what the provider actually built. Every realization event is a new record — not an upsert. Contains field-level provenance from the provider. The relationship between a Realized State record and its corresponding Requested State record is explicit and mandatory. + +**Discovered State** — An ephemeral snapshot of what the provider currently reports as existing, obtained through active discovery polling. Used by the Drift Reconciliation Component to compare against Realized State. + +**What DCM asks for in inventory decisions:** +1. What is the current lifecycle state of entity UUID X? (Realized State read) +2. What resources does tenant T own? (indexed query over Realized State) +3. What entities have relationship R to entity X? (Entity Relationship Graph, [entity-relationships.md](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md)) +4. What is the field-level provenance of field F on entity X? (Realized State metadata) +5. What entities are currently drifted? (Drift Record Store, DRC component output) +6. What did we discover vs what do we have as realized? (Drift comparison) +7. What happened to entity X over its full lifecycle? (Audit Store, time-indexed) + +--- + +## 4. Integration Analysis + +### 4.1 Kessel Relations — Authorization Backend + +#### Mapping DCM's Permission Model to SpiceDB + +DCM's five authorization questions map to SpiceDB as follows: + +``` +# Proposed SpiceDB schema for DCM +definition user {} + +definition group { + relation member: user | group#member + relation parent_group: group + permission member = member + parent_group->member +} + +definition tenant { + relation member: user | group#member + relation admin: user | group#member + relation platform_admin: user | group#member + permission submit_request = member + admin + platform_admin + permission manage_resources = admin + platform_admin + permission administer = platform_admin +} + +definition resource { + relation owner_tenant: tenant + relation authorized_tenant: tenant # cross-tenant authorization + relation viewer: user | group#member + permission read = owner_tenant->member + authorized_tenant->member + viewer + permission modify = owner_tenant->admin + permission decommission = owner_tenant->admin +} + +definition dcm_group { + relation member: user | group#member + relation quorum_threshold: integer # NOTE: see Section 4.1.2 +} +``` + +**Question 1** (does actor X have role Y in tenant T?) → `CheckPermission(user:X, permission:submit_request, tenant:T)` + +**Question 2** (what catalog items can actor X see?) → `LookupResources(user:X, permission:read, resource_type:catalog_item)` + +**Question 3** (can actor X do operation O on resource R?) → `CheckPermission(user:X, permission:modify, resource:R)` + +**Question 4** (does tenant T have cross-tenant authorization on resource R?) → `CheckPermission(tenant:T#member, permission:read, resource:R)` — satisfied if `authorized_tenant` relationship exists + +**Question 5** (approval gate quorum) — **Does not map cleanly to SpiceDB.** See Section 4.1.2. + +#### 4.1.2 Approval Gate Quorum — The Gap + +DCM's `authorized` tier approval requires N of M members of a declared DCMGroup to record decisions before an operation proceeds. SpiceDB is a membership and permission graph — it answers "does this subject have this permission?" but it does not count decisions or track quorum state across time. + +**Resolution:** The approval gate workflow stays in DCM's Policy Engine regardless of Kessel integration. Kessel Relations handles who *can* approve (membership in the DCMGroup); DCM's Approval Store tracks who *has* approved and whether quorum is reached. + +This is a clean boundary: Kessel answers the structural question ("is this actor authorized to vote?"); DCM answers the state question ("how many valid votes have been recorded?"). + +> **ADR-RBAC-001 alignment.** DCM's roles, DCM-functions, the role→function matrix, and role-assignments are authored as UDLM **data** (governed `access-role` / `dcm-function` TaxonomyTerms + `FunctionCapabilityMatrix` + `role_assignment`) precisely so they **project onto** this Kessel/SpiceDB model — roles→relations, functions→computed permissions, assignments→relationships — without DCM depending on Kessel. The Auth Provider abstraction is the seam (register Kessel Relations as an auth provider handling checks 1–2). This document stays frozen until Kessel alignment; ADR-RBAC-001 keeps the path open, not required. + +#### 4.1.3 DCM's Entity Relationship Graph is NOT an Authorization Graph + +This is a critical distinction. DCM's entity relationships — `requires`, `constituent`, `shareable`, `allocated_from`, `peer` — are **operational relationships between infrastructure resources**, not access control relationships. They express: "VM X requires Storage Y", "Composite C has constituent VM X." + +These must **not** be stored in Kessel Relations. They are: +- Semantically different from access control (lifecycle implications, not permissions) +- DCM-specific (not meaningful to any other system consuming Kessel) +- Owned by DCM's entity lifecycle model + +DCM's Entity Relationship Graph ([entity-relationships.md](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md)) remains entirely in DCM regardless of Kessel integration. + +#### 4.1.4 Checks 3–5 of the Five-Check Boundary Model + +DCM's five-check boundary model (identity → authorization → accreditation → data matrix → sovereignty) maps to Kessel Relations only for checks 1 and 2. Checks 3–5 are DCM-specific: + +- **Accreditation** (check 3): Does the target provider hold the required accreditation for the data classification present? This involves DCM's Accreditation Registry and is not a subject/permission/resource question. +- **Data/Capability Matrix** (check 4): Is each field permitted to cross this boundary given its classification? This involves DCM's Governance Matrix policies. +- **Sovereignty** (check 5): Is the target endpoint within the sovereignty boundary? This involves DCM's Sovereignty Zone declarations. + +None of checks 3–5 can be delegated to Kessel Relations. They remain in DCM's Policy Engine. + +#### 4.1.5 Integration Path via Auth Provider Abstraction + +DCM's Auth Provider abstraction ([auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md)) is the natural integration point. Kessel Relations would register as a DCM Auth Provider or External Policy Evaluator: + +```yaml +kessel_relations_auth_provider: + provider_type: auth_provider + auth_mode: kessel_rebac + endpoint: https://kessel-relations.internal:9000 + schema_ref: + + # What this provider handles: + handles: + - check_permission # CheckPermission calls + - lookup_resources # LookupResources calls + - lookup_subjects # LookupSubjects calls + + # What stays in DCM's Policy Engine: + does_not_handle: + - accreditation_checks + - data_classification_matrix + - sovereignty_checks + - approval_gate_quorum +``` + +DCM's Policy Engine calls the Kessel Relations provider for authorization questions (checks 1 and 2) and evaluates checks 3–5 internally. The five-check sequence is preserved; only the implementation of checks 1–2 changes. + +**Zookie handling:** DCM's API Gateway must thread zookie tokens through the request lifecycle: when a relationship is written (e.g., a new cross-tenant authorization is created), the resulting zookie is stored and used for subsequent permission checks in the same request context, guaranteeing consistency. + +--- + +### 4.2 Kessel Inventory — Discovered State Store + +#### 4.2.1 The Fit + +Of DCM's four stores, **Discovered State** is the only one Kessel Inventory could plausibly replace. The reasons: + +- Discovered State is the most ephemeral store — it is overwritten on each discovery cycle +- Discovered State does not require immutability or append-only semantics — it represents "what the provider reports right now" +- Discovered State is the "current state of infrastructure" — exactly what Kessel Inventory is designed to track +- Other Red Hat tools consuming Kessel Inventory would benefit from seeing the same discovered state that DCM uses for drift detection + +The other three stores — Intent, Requested, and Realized — **cannot** be replaced by Kessel Inventory: +- Intent and Requested State require GitOps semantics (PR workflow, immutability, version history) +- Realized State requires append-only event stream semantics with field-level provenance and Merkle-tree integrity +- None of DCM's lifecycle or audit requirements are in scope for Kessel Inventory + +#### 4.2.2 The Schema Alignment Question + +DCM's Discovered State uses the same unified data model format as Realized State — the DCM Resource Type Spec schema. Kessel Inventory uses a Protobuf-defined common resource schema. + +For standard resource types (Compute, Network, Storage that map to well-known infrastructure concepts), the alignment is likely achievable. For DCM-specific resource types (Automation.AnsiblePlaybook, Platform.KubernetesCluster, custom org-defined types), schema extension or mapping is required. + +**Open question for Kessel team:** How extensible is the Kessel Inventory resource type schema? Can DCM register custom resource types? Is there a type registry mechanism analogous to DCM's Resource Type Registry? + +#### 4.2.3 Drift Detection Logic Stays in DCM + +Kessel Inventory is a state store, not a drift detection system. Even if DCM uses Kessel Inventory as the Discovered State store, the Drift Reconciliation Component (the Drift Reconciliation Component) remains entirely in DCM: + +- DRC queries Kessel Inventory for current discovered state +- DRC compares discovered state against DCM's Realized State +- DRC classifies differences by field criticality and change magnitude +- DRC produces Drift Records with SECURITY_DEGRADATION, BROKEN_REFERENCE, UNSANCTIONED_CHANGE classifications +- DRC writes Drift Records to DCM's Drift Record Store + +Kessel Inventory's role is purely as the data source for the "what currently exists" side of the comparison. The intelligence stays in DCM. + +#### 4.2.4 Integration Path via data store Abstraction + +DCM's data store abstraction ([data-store-contracts.md](https://github.com/croadfeldt/udlm/blob/main/contracts/data-store-contracts.md)) is the natural integration point. The Discovered Store would be implemented as a `storage_sub_type: snapshot_store` data store backed by Kessel Inventory: + +```yaml +kessel_inventory_(prescribed infrastructure): + provider_type: (prescribed infrastructure) + storage_sub_type: snapshot_store + backend: kessel_inventory + endpoint: https://kessel-inventory.internal:9001 + + # DCM uses this provider for: + used_for: discovered_state + + # Write contract: provider calls POST /api/v1/instances/{id}/status + # which DCM translates to Kessel Inventory upsert + write_model: upsert_current_state + + # Read contract: DRC queries Kessel for discovered state + read_model: streaming_query_by_type_and_tenant +``` + +This means the Kessel Inventory integration requires **no changes to DCM's data model** — only a new data store implementation. The Drift Reconciliation Component calls the same Discovered State Store interface; the underlying implementation happens to be Kessel Inventory. + +--- + +## 5. Deployment and Sovereignty Considerations + +### 5.1 Air-Gapped and Sovereign Deployments + +DCM's `sovereign` profile requires air-gapped operation with no external dependencies. Any Kessel integration must support: + +- Local/on-premises Kessel deployment (not cloud-hosted) +- Offline operation when Kessel is temporarily unavailable (cached authorization decisions for read-only operations) +- mTLS between DCM and Kessel instances + +**Open question for Kessel team:** What is Kessel's deployment model for sovereign/air-gapped environments? Is there a supported on-premises deployment path? What is the operational footprint? + +### 5.2 Multi-Instance Federation + +DCM supports federation between multiple DCM instances ([federation-runtime.md](../runtime-features/federation-runtime.md)). A federated deployment may have multiple Kessel Relations instances (one per region or sovereignty zone) or a single shared instance. + +**Open question for Kessel team:** How does Kessel Relations handle multi-region replication? Can SpiceDB schema and relationship data be replicated across sovereignty boundaries? What are the consistency guarantees in a federated topology? + +### 5.3 Failure Mode Analysis + +If Kessel Relations is unavailable, DCM cannot evaluate authorization checks 1–2 of the five-check model, which means DCM cannot process any requests. This is a critical dependency. + +**Required mitigation strategies:** +- Read-through cache for CheckPermission results (short TTL, profile-governed) +- Circuit breaker: if Kessel is unavailable for >N consecutive checks, DCM enters a safe-deny mode (no new requests accepted) rather than a fail-open mode +- Kessel Relations HA deployment is a prerequisite, not optional + +**Open question for Kessel team:** What HA and disaster recovery patterns are recommended for production Kessel Relations deployments? + +--- + +## 6. Questions for the Kessel Team + +The following questions should be addressed before any integration work begins: + +### 6.1 Kessel Relations + +| # | Question | Why It Matters | +|---|----------|----------------| +| 1 | What is the current API stability level of the Kessel Relations gRPC API? Are breaking changes expected? | DCM needs a stable contract to build against | +| 2 | Does Kessel Relations support on-premises / air-gapped deployment? What is the operational footprint? | Required for DCM's `sovereign` profile | +| 3 | How does the SpiceDB schema evolve? Is there a migration path when the DCM permission model changes? | Schema evolution is a production concern | +| 4 | Can Kessel Relations store relationships at the scale DCM requires? How many relationship tuples per tenant at what query latency? | DCM may have thousands of cross-tenant authorization records per deployment | +| 5 | How does Kessel handle the zookie (consistency token) lifecycle? Are zookies scoped to a namespace/tenant, or global? | Relevant to DCM's multi-tenant isolation model | +| 6 | Is Kessel Relations multi-tenant natively, or does DCM need to namespace its SpiceDB schema? | Critical for DCM's tenant isolation requirements | +| 7 | What is the intended integration pattern for other Red Hat products (ACM, Insights)? How would DCM's usage interoperate? | Kessel's value to DCM is partly the shared source of truth across RH products | +| 8 | Does Kessel Relations have a concept equivalent to DCM's "cross-tenant authorization"? How are trust grants between tenants modeled? | Core to DCM's resource sharing model | + +### 6.2 Kessel Inventory + +| # | Question | Why It Matters | +|---|----------|----------------| +| 9 | How extensible is Kessel Inventory's resource type schema? Can DCM register custom resource types? | DCM has domain-specific resource types not in Kessel's default schema | +| 10 | What is the write model? Last-write-wins upsert, or versioned? Does Kessel Inventory support the discovered state pattern (full overwrite on each discovery cycle)? | DCM's Discovered Store is a full-replacement snapshot per discovery cycle | +| 11 | How does Kessel Inventory integrate with Kessel Relations for auth-filtered queries? Is the integration already built, or planned? | Core to the value of using Kessel Inventory | +| 12 | What is the data retention model? Does Kessel Inventory keep history or only current state? | DCM needs "current state" only for Discovered State; history is in DCM's Audit Store | +| 13 | What is the API stability level for Kessel Inventory? | Same concern as #1 for Relations | +| 14 | Is there a reference implementation of a Kessel Inventory provider for a Kubernetes/OpenShift resource type? | DCM would follow this pattern for its Service Providers | + +### 6.3 Joint Architecture Questions + +| # | Question | Why It Matters | +|---|----------|----------------| +| 15 | Is the Kessel project open to DCM contributing Resource Type definitions and SpiceDB schema extensions to the upstream? | Reduces divergence risk; benefits broader community | +| 16 | How does Kessel handle sovereign data — data that must not cross jurisdictional boundaries? | Critical for DCM's sovereignty model | +| 17 | What is the recommended pattern for bootstrapping the Kessel-DCM trust relationship? (mTLS? OIDC? Service account?) | Required for DCM's zero-trust model | +| 18 | Does Kessel have a compatibility matrix for Red Hat platform versions (OpenShift, RHEL)? | DCM targets the same platforms | + +--- + +## 7. Proposed Integration Architecture (Pending Kessel Alignment) + +This section describes the target architecture **conditional on positive answers to the questions in Section 6**. It should not be implemented until validated with the Kessel team. + +### 7.1 Kessel Relations as DCM Auth Provider + +``` +DCM Request Pipeline: + │ + ▼ Auth Provider (Kessel Relations): + │ Check 1: identity verification via mTLS certificate + │ Check 2: CheckPermission(actor, operation, tenant/resource) via Kessel Relations gRPC + │ ← returns allow/deny + zookie token + │ + ▼ DCM Policy Engine (internal): + │ Check 3: Accreditation check (DCM Accreditation Registry) + │ Check 4: Data/Capability Matrix (DCM Governance Matrix) + │ Check 5: Sovereignty check (DCM Sovereignty Zone registry) + │ + ▼ All five checks pass → request proceeds to layer assembly +``` + +**Impact on DCM architecture:** +- Auth Provider registration: new `auth_mode: kessel_rebac` in [auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) +- Cross-tenant authorization DCMGroup: writes to both DCM Group Registry AND Kessel Relations tuple store +- RBAC evaluation: replaced by Kessel Relations CheckPermission call for checks 1–2 +- Group membership sync: DCM Auth Providers (LDAP, OIDC) continue to manage authentication; group memberships are mirrored to Kessel Relations for use in permission evaluation + +### 7.2 Kessel Inventory as DCM Discovered State Store + +``` +Discovery Cycle: + │ + ▼ Discovery Scheduler triggers provider discovery + │ + ▼ Service Provider returns RealizedStatePayload stream + │ (current state in DCM Unified Data Model format) + │ + ▼ Kessel Inventory data store: + │ Translates DCM format → Kessel Inventory Protobuf schema + │ Upserts to Kessel Inventory (replaces prior discovered state) + │ + ▼ Drift Reconciliation Component (unchanged): + │ Queries Kessel Inventory for discovered state + │ Compares against DCM Realized State + │ Produces Drift Records (classification, severity, field detail) + │ Writes Drift Records to DCM Drift Record Store +``` + +**Impact on DCM architecture:** +- Discovered State Store: implement as data store backed by Kessel Inventory +- No changes to data model, drift detection logic, or Drift Reconciliation Component +- Resource type mapping: DCM Resource Type Specs → Kessel Inventory resource types (new tooling required) + +--- + +## 8. What Does Not Change Regardless of Integration + +The following DCM capabilities remain entirely in DCM regardless of how the Kessel integration develops: + +| Capability | Why it stays in DCM | +|-----------|---------------------| +| Intent State Store (GitOps) | GitOps semantics, PR workflow, immutability — not in scope for Kessel | +| Requested State Store (write-once) | Assembled payload with full provenance — DCM-specific | +| Realized State Store (append-only event stream) | Hash-chained, tamper-evident, field-level provenance — DCM-specific | +| Approval gate quorum tracking | State-tracking across time — Kessel Relations answers membership, not quorum | +| Five-check boundary model (checks 3–5) | Accreditation, data classification, sovereignty — DCM-specific | +| Entity Relationship Graph | Operational relationships between resources — not access control | +| Field-level provenance | Source tracking per field — not in scope for Kessel | +| Drift detection logic and classification | DRC component — Kessel Inventory is a data source, not a drift engine | +| Audit trail (Merkle tree) | Tamper-evident audit — DCM-specific requirement | +| Resource lifecycle state machine | REQUESTED → OPERATIONAL → DECOMMISSIONED — DCM-specific | +| Policy Engine | Validation, Transformation, Recovery, Orchestration Flow policies — DCM-specific | +| Authority Tier model | Approval routing — DCM-specific governance model | + +--- + +## 9. System Policies (Proposed — Pending Validation) + +These policies should be reviewed and confirmed after Kessel team alignment: + +| Policy | Rule | +|--------|------| +| `KESSEL-001` | Kessel Relations, if registered as a DCM Auth Provider, handles authorization checks 1 and 2 of the five-check boundary model only. Checks 3–5 remain in DCM's Policy Engine and cannot be delegated. | +| `KESSEL-002` | DCM's entity relationship graph (operational relationships between infrastructure resources) must never be stored in Kessel Relations. Only access-control relationships (actor→group→tenant→resource permissions) are stored in Kessel Relations. | +| `KESSEL-003` | Kessel Inventory, if registered as a DCM data store for Discovered State, holds only ephemeral current-state snapshots. Intent, Requested, and Realized State stores remain in DCM-managed data stores. | +| `KESSEL-004` | If Kessel Relations is unavailable, DCM enters safe-deny mode: no new requests are accepted. Fail-open behavior is not permitted under any profile. | +| `KESSEL-005` | Zookie tokens from Kessel Relations CheckPermission responses must be threaded through the DCM request context to guarantee consistency across authorization checks within the same request. | +| `KESSEL-006` | Cross-tenant authorization DCMGroups that are backed by Kessel Relations must be written atomically: the DCM Group Registry record and the Kessel Relations tuple must both succeed or both fail. Partial writes are treated as failures. | +| `KESSEL-007` | DCM sovereign profile deployments require a locally-deployed Kessel instance. Cloud-hosted Kessel is not permitted for sovereign deployments. This requirement must be confirmed as feasible with the Kessel team. | + +--- + +## 10. Open Items Before Integration Can Begin + +| # | Item | Owner | Blocking? | +|---|------|-------|-----------| +| 1 | Kessel team review of Section 6 questions | Kessel team | Yes | +| 2 | Kessel Relations API stability confirmation | Kessel team | Yes | +| 3 | Sovereign/air-gapped deployment validation | Kessel team | Yes (for sovereign profile) | +| 4 | SpiceDB schema design review for DCM permission model | DCM + Kessel | Yes | +| 5 | Kessel Inventory resource type extensibility confirmation | Kessel team | Yes (for inventory integration) | +| 6 | HA/DR pattern review for production Kessel deployment | Kessel team | Yes | +| 7 | DCM Auth Provider interface extension for `kessel_rebac` mode | DCM team | No (can design in parallel) | +| 8 | DCM data store implementation for Kessel Inventory | DCM team | No (can design in parallel) | +| 9 | Zookie lifecycle management design in DCM request pipeline | DCM team | No (can design in parallel) | +| 10 | Resource type mapping: DCM Resource Type Specs → Kessel Inventory schema | DCM + Kessel | No (can design in parallel) | + +--- + +*Document maintained by the DCM Project. For questions, contributions, or to schedule the Kessel alignment session see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/integrations/koku-focus-adoption.md b/architecture/integrations/koku-focus-adoption.md new file mode 100644 index 0000000..e105545 --- /dev/null +++ b/architecture/integrations/koku-focus-adoption.md @@ -0,0 +1,70 @@ +# Koku — upstream requirements for FOCUS adoption (cost provider) + +The DCM Cost provider (`dcm-project/enhancements` #57/#60) must serve **vendor-neutral** cost data so any +cost backend is swappable behind the same `cost` service type. Per ADR-021 (adopt external standards by +reference) and `adopted-standards-dcm.md` (ADS-001…010), the cost data conforms to **FOCUS** (Tier-2 +record standard) + **OpenCost** for Kubernetes allocation — not a bespoke, Koku-shaped vocabulary. + +This doc records the **upstream changes Koku** (`project-koku/koku`) needs so the cost provider can emit +FOCUS-conformant data. GitHub issues are disabled on that repo (it tracks via the **COST** Jira); these +are written as ready-to-file tickets — one subject per ticket, each with a *Why* — for COST Jira or a +koku GitHub Discussion. Current state (June 2026): no FOCUS export exists upstream; ISO 4217 currency is +already in flight (koku PR #6097). + +> **Tiering (why the work is uneven):** FOCUS/OpenCost are **Tier-2** record/schema standards → they need +> a real export + version negotiation + identity join. ISO 4217 is a **Tier-1** codelist → a referenced +> field constraint, already underway. Don't flatten them; see `adopted-standards.md` §1a. + +--- + +## A. FOCUS data export (serializer) + +**Why:** [FOCUS](https://focus.finops.org/) (FinOps Foundation, v1.4) is the vendor-neutral cost/usage +standard AWS/Azure/GCP already emit. Koku already normalizes multi-cloud + OpenShift cost into a unified +model; exposing it **as FOCUS** lets any FOCUS-aware consumer (FinOps tooling, the DCM cost provider) read +Koku cost without a Koku-specific integration. + +**What:** A FOCUS-conformant export/serializer projecting Koku's unified data into FOCUS columns — +`BilledCost`/`EffectiveCost`/`ListCost`/`ContractedCost`, `BillingCurrency`, `ChargeCategory`/`ChargeClass`, +`ConsumedQuantity`/`ConsumedUnit`, `PricingQuantity`/`PricingUnit`, `ChargePeriodStart/End`, +`ServiceCategory`/`ServiceName`, `ResourceId`/`ResourceType`, and the 1.3+ allocation columns. A projection +over existing data, not new metering. + +**Scope:** the export itself; version selection (B), `ResourceId` join (C), and OpenCost alignment (D) are +separate subjects. + +## B. FOCUS export — version selection + advertise supported versions + +**Why:** consumers need a specific FOCUS `major.minor` (allocation columns require ≥1.3), and a negotiating +platform must know which versions Koku can emit (the cost provider's `adopted_standard_support` matrix). + +**What:** accept a requested FOCUS version on the export (e.g. `?focus_version=1.4`), emit that version's +shape, and advertise the supported set (e.g. 1.2–1.4) via the API/capabilities. Depends on **A**. + +## C. FOCUS export — stable `ResourceId` for the identity join + +**Why:** FOCUS rows must carry a stable `ResourceId` so an external system can join cost back to the +resource it manages (e.g. a cluster/VM identity). Without a stable key, cost can't be attributed to a +managed resource. + +**What:** emit a stable, documented `ResourceId` in the FOCUS output (derived from existing tags/labels or +an accepted external correlation id). Depends on **A**. + +## D. Align OpenShift cost allocation with OpenCost + +**Why:** [OpenCost](https://opencost.io/) (CNCF) is the vendor-neutral standard for Kubernetes cost +allocation (workload/idle split, `max(request,usage)` over CPU/memory/GPU/PV/network). Aligning Koku's +OpenShift allocation to OpenCost — or documenting the precise mapping — makes Koku's container cost +portable and comparable with the ecosystem. + +**What:** align (or document the mapping of) Koku's OpenShift allocation to the OpenCost spec, and expose +it in the FOCUS export's allocation columns. Related to **A**. + +--- + +## E. (Not Koku) cost-dcm-provider — the DCM-side seam + +`pgarciaq/cost-dcm-provider` (the cost SP adapter) declares `adopted_standard_support` (FOCUS/OpenCost +versions), serves FOCUS via a `serve_data` capability, and binds cost to the target by identity +(`uuid` ↔ FOCUS `ResourceId`). It can perform interim FOCUS translation if A lands slowly — but the durable, +reusable home for the FOCUS projection is **Koku itself** (A), so every Koku consumer benefits, not just DCM. diff --git a/architecture/layering.md b/architecture/layering.md new file mode 100644 index 0000000..5f75360 --- /dev/null +++ b/architecture/layering.md @@ -0,0 +1,166 @@ +--- +Document Status: ✅ Stable — Architectural framing +Document Type: Architecture Foundation +Established: 2026-05-26 +Maps to: UDLM substrate / DCM realization boundary +--- + +# Layering — UDLM substrate vs DCM realization + +> **Implements contracts defined in UDLM**: this document names the boundary +> that the UDLM repo ([github.com/croadfeldt/udlm](https://github.com/croadfeldt/udlm)) +> and this DCM repo collectively imply. The boundary rule is normative for +> both sides; the application of the rule (what lives where) is recorded in +> [`00-split-manifest.md`](00-split-manifest.md). + +This document is the DCM-side perspective on the layering that justifies the +split between UDLM (substrate) and DCM (realization). + +--- + +## The Layers + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Higher-Order Universal Model (deliberately deferred) │ +│ ───────────────────────────────── │ +│ A more abstract universal model that UDLM is a specialization │ +│ of. Could be derived later if a real second realization │ +│ creates the pressure. NOT formalized today. │ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ +┌─────────────────────────────┴───────────────────────────────────┐ +│ UDLM — github.com/croadfeldt/udlm │ +│ ──────────────────────────────── │ +│ The universal substrate. Owns the *what*: │ +│ • entity types, fields, relationships │ +│ • the four states (intent, requested, realized, discovered) │ +│ • allowed state transitions and lifecycle invariants │ +│ • provenance, lineage, identity │ +│ • wire contracts (provider, policy, event payloads, │ +│ data store, schema-sharing protocol) │ +│ • reference taxonomies (authority tier model, registry │ +│ governance model, layered-topology contract, ...) │ +│ │ +│ UDLM owns wire-level compatibility. Any peer realization that │ +│ conforms produces data that any other conformant peer can │ +│ read, interpret, and exchange. │ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ (this repo realizes) + │ +┌─────────────────────────────┴───────────────────────────────────┐ +│ DCM — this repo │ +│ ──────────────── │ +│ One operational platform built on UDLM. Owns the *how*: │ +│ • control-plane components and their boundaries │ +│ • the convergence engine (the intent → realized loop) │ +│ • policy evaluation at each transition │ +│ • provider invocation, retry, dependency-graph orchestration │ +│ • ingress / egress, APIs, service boundaries │ +│ • drift detection, recovery utilities, expiration │ +│ • persistence (PostgreSQL mandated for this realization) │ +│ • specific 9-layer canonical location hierarchy │ +│ • mTLS + interaction credential as provider callback auth │ +│ • deployment topology, runtime concerns │ +│ │ +│ A different DCM-peer realization could consume the same UDLM │ +│ substrate and realize it differently while remaining wire- │ +│ compatible at the UDLM contract boundary. │ +└─────────────────────────────────────────────────────────────────┘ +``` + +> The **9-layer canonical location hierarchy** named above is DCM's concrete +> location topology — Country → Region → … → Unit, nine levels — DCM's specific +> choice for UDLM's abstract layered-topology contract. It is fully defined in +> [topology/canonical-9-layer-hierarchy.md](topology/canonical-9-layer-hierarchy.md). + +--- + +## The boundary rule + +For each file or section, the test is: + +> *"Could a peer of DCM, built independently, choose to do this differently +> and still be a valid realization of the same data?"* + +- **Yes →** belongs in **DCM**. It's an operational/implementation choice. +- **No, it would break interop or invalidate the data →** belongs in **UDLM**. + It's a substrate invariant. + +| Concern | Layer | Why | +|---|---|---| +| State names (intent, requested, realized, discovered) | UDLM | Vocabulary every realization shares | +| Field shape at each state | UDLM | Same data, regardless of operationalization | +| Allowed transitions between states | UDLM | Invariant of the data, not a runtime choice | +| Provider response shape | UDLM | Wire contract | +| Policy input/output schemas | UDLM | Wire contract | +| Event payloads | UDLM | Wire contract that lets observers exist | +| Schema-sharing protocol | UDLM | Required for federation peers to exchange custom-type schemas | +| **The convergence loop that walks data through states** | **DCM** | Implementation choice — see [convergence-engine/overview.md](convergence-engine/overview.md) | +| **Provider invocation, retry, ordering** | **DCM** | Orchestration — see [convergence-engine/recovery-and-retry.md](convergence-engine/recovery-and-retry.md) | +| **PostgreSQL as the data store** | **DCM** | UDLM requires persistence; DCM mandates PostgreSQL specifically. See [persistence/postgres-mandate.md](persistence/postgres-mandate.md) | +| **mTLS + interaction credential for provider callback** | **DCM** | UDLM defines two-layer auth abstractly; DCM picks the specific mechanism. See [credentials-and-auth/provider-callback.md](credentials-and-auth/provider-callback.md) | +| **Specific 9-layer location hierarchy** | **DCM** | UDLM defines layered-topology contract; DCM picks Country → ... → Unit. See [topology/canonical-9-layer-hierarchy.md](topology/canonical-9-layer-hierarchy.md) | +| **Drift detection / recovery / expiration utilities** | **DCM** | Runtime concerns | +| **Control-plane components, service boundaries, APIs** | **DCM** | Deployment / runtime choices | + +--- + +## Compatibility model (LOCKED) + +**UDLM enforces wire-level compatibility at the data/event/contract boundary; +it does not enforce implementation portability.** + +- Any system conformant to UDLM version X produces data that any other system + conformant to the same major version can read, interpret, and exchange + (versioning rules apply). +- Federation between peers is **literal interop**, not "architecturally similar + systems requiring adapters." +- A peer realization's storage, internal APIs, control-plane components, and + runtime mechanics are NOT constrained by UDLM — those are DCM-layer choices. + +This is the K8s precedent: K8s API + CRDs are wire-compatible across +distributions; controllers are not portable. UDLM and DCM are in the same shape. + +--- + +## Why the Higher-Order Model is deferred + +A more abstract universal model likely exists above UDLM — a "manage-anything- +via-data-and-policy" pattern UDLM is a specialization of. It is intentionally +not formalized today: + +- **Abstraction discipline.** UDLM is allowed to be specific enough to be + realizable, even if that bakes in some assumptions a purer model wouldn't. +- **No pressure yet.** The higher-order model becomes worth formalizing when + a real second realization (a non-DCM peer using UDLM) creates pressure to + identify what is truly shared vs DCM-specific. Until then, drawing the line + is guessing. +- **Cost asymmetry.** Premature abstraction costs more than lifting concepts + up later when the line becomes obvious. + +Treat the higher-order layer as **known to exist** and **named**, but **out of +scope for current documentation**. If a peer of DCM emerges, this is the layer +where the genuinely-shared bits will be lifted from UDLM. + +--- + +## Implications for verification/assessment consumers + +A verification consumer (an assessment realization or test harness — non-normative; +nothing here depends on a specific tool) tests use cases against the architecture. +Per this layering, that's two distinct questions: + +1. **"Does UDLM support this use case?"** — does the substrate accommodate the + entities, states, contracts, lifecycle the UC needs? +2. **"Does DCM operationalize it correctly?"** — does the convergence loop, + provider orchestration, and runtime actually realize it? + +Such a consumer's `spec_refs` use namespaced paths to disambiguate: +- `udlm/contracts/event-catalog.md` — substrate reference +- `dcm/architecture/convergence-engine/policy-evaluation.md` — realization reference + +The two repos are independently fetchable; cross-repo references are resolved +during use-case evaluation. diff --git a/architecture/operator-perspective.md b/architecture/operator-perspective.md new file mode 100644 index 0000000..cb78065 --- /dev/null +++ b/architecture/operator-perspective.md @@ -0,0 +1,249 @@ +--- +Document Status: 📋 Draft — Initial Specification +Document Type: Operator/Implementer Narrative +Established: 2026-05-26 +Maps to: UDLM operationalization +--- + +# DCM Operator Perspective — the DMV Operator's Manual + +> **Implements contracts defined in UDLM**: this is the operator-side +> companion to UDLM's `udlm/docs/consumer-perspective.md` (the consumer's +> driver's handbook). Together the two perspectives cover the system from +> both sides — the consumer who submits intent and waits for realized state, +> and the operator who runs the platform that makes realization happen. + +This document is for people who must **run DCM**. Not consume it from the +outside (that's the consumer perspective in UDLM); **operationalize it**. + +--- + +## 1. Mental model — DCM is UDLM's operationalization + +UDLM tells you what data exists, what states it can be in, what wire contracts +peers must honor, and what the lifecycle invariants are. UDLM does not tell +you: + +- Where the data lives on disk +- Which programming language runs the convergence loop +- How requests are queued and dispatched +- Which database your audit chain is stored in +- Whether a service mesh handles mTLS +- How many control-plane services you deploy + +Those are realization choices. **DCM is one set of answers to those questions** — +a specific, opinionated, operationally proven set. + +The driving analogy: UDLM is the published rules of the road (what cars exist, +what driving means, what licenses are required). DCM is the actual road, the +turn signals, the traffic lights, the cars driving, the DMV that issues licenses. +A peer of DCM in a different jurisdiction could pave the road in concrete instead +of asphalt and still be a valid road — provided cars conformant to the rules of +the road can drive on it. + +--- + +## 2. Where the substrate ends and the realization begins + +When you read a UDLM document like `udlm/lifecycle/operational-models.md`, +you'll see contracts like: + +- *"a realization MUST honor timeout contracts on dispatch"* +- *"cancellation MUST follow the best-effort propagation model"* +- *"orphan detection MUST be triggered after compensation failure"* + +When you read the corresponding DCM document +[`convergence-engine/recovery-and-retry.md`](convergence-engine/recovery-and-retry.md), +you'll see implementation: + +- *"DCM enforces dispatch_timeout via per-step deadlines computed as fractions of assembly_timeout"* +- *"DCM's cancellation execution sends a cancel payload to the provider's declared cancellation endpoint and waits up to PT30S for acknowledgement"* +- *"DCM's orphan detection runs an immediate Mode 1 capacity-and-listing query against the provider after COMPENSATION_FAILED"* + +This pattern repeats across every DCM document. The UDLM contract is the +"what must be true"; the DCM document is "here is how DCM makes it true." + +When you write your own DCM-internal documentation or extend an existing area, +**stay in operationalization voice**: "DCM evaluates X using algorithm A", +"DCM enforces Y at boundary Z", "DCM persists Q in table R". Avoid re-stating +the UDLM contract; link to it. + +--- + +## 3. Where the operational concerns live in this repo + +The DCM repo is organized by **architectural concern**, not by document number. +Use this map to find what you need. + +### 3.1 Running the platform + +| You need to... | Look in | +|---|---| +| Understand what services to deploy | [`control-plane/components.md`](control-plane/components.md) | +| Set up internal mTLS between services | [`control-plane/internal-component-auth.md`](control-plane/internal-component-auth.md) | +| Configure health checks and probes | [`control-plane/self-health.md`](control-plane/self-health.md) | +| Manage API versioning | [`control-plane/api-versioning.md`](control-plane/api-versioning.md) | +| Manage session revocation | [`control-plane/session-revocation.md`](control-plane/session-revocation.md) | +| Choose a deployment topology | [`runtime-features/deployment-redundancy.md`](runtime-features/deployment-redundancy.md) | +| Set up the database | [`persistence/postgres-mandate.md`](persistence/postgres-mandate.md), [`persistence/postgres-implementation.md`](persistence/postgres-implementation.md) | + +### 3.2 The convergence engine — the heart of DCM + +| You need to... | Look in | +|---|---| +| Understand the intent→realized loop | [`convergence-engine/overview.md`](convergence-engine/overview.md) | +| Understand policy evaluation mechanics | [`convergence-engine/policy-evaluation.md`](convergence-engine/policy-evaluation.md) | +| Understand placement scoring | [`convergence-engine/scoring.md`](convergence-engine/scoring.md) | +| Handle timeouts, retries, cancellations | [`convergence-engine/recovery-and-retry.md`](convergence-engine/recovery-and-retry.md) | +| Orchestrate dependent requests | [`convergence-engine/dependency-orchestration.md`](convergence-engine/dependency-orchestration.md) | + +### 3.3 Bringing infrastructure into the platform + +| You need to... | Look in | +|---|---| +| Onboard brownfield infrastructure | [`ingestion/engine.md`](ingestion/engine.md) | +| Analyze workload placement | [`ingestion/workload-analysis.md`](ingestion/workload-analysis.md) | + +### 3.4 Credentials, auth, governance enforcement + +| You need to... | Look in | +|---|---| +| Configure auth providers | [`credentials-and-auth/auth-implementation.md`](credentials-and-auth/auth-implementation.md) | +| Manage credentials lifecycle | [`credentials-and-auth/credentials.md`](credentials-and-auth/credentials.md) | +| Set up provider callback auth (mTLS + interaction credential) | [`credentials-and-auth/provider-callback.md`](credentials-and-auth/provider-callback.md) | +| Enforce authority tier on approvals | [`credentials-and-auth/authority-enforcement.md`](credentials-and-auth/authority-enforcement.md) | +| Enforce the governance matrix | [`convergence-engine/policy-evaluation.md`](convergence-engine/policy-evaluation.md) | +| Monitor accreditation status | [`governance-enforcement/accreditation-monitor.md`](governance-enforcement/accreditation-monitor.md) | +| Govern registry contributions | [`governance-enforcement/registry-enforcement.md`](governance-enforcement/registry-enforcement.md) | +| Run the contribution pipeline (GitOps PRs) | [`governance-enforcement/contribution-pipeline.md`](governance-enforcement/contribution-pipeline.md) | +| Configure policy profiles | [`governance-enforcement/policy-profiles.md`](governance-enforcement/policy-profiles.md) | + +### 3.5 Runtime features + +| You need to... | Look in | +|---|---| +| Set up scheduled requests + maintenance windows | [`runtime-features/scheduling.md`](runtime-features/scheduling.md) | +| Configure notification delivery | [`runtime-features/notifications.md`](runtime-features/notifications.md) | +| Set up webhooks and messaging | [`runtime-features/webhooks-messaging.md`](runtime-features/webhooks-messaging.md) | +| Configure DCM-to-DCM federation runtime | [`runtime-features/federation-runtime.md`](runtime-features/federation-runtime.md) | + +### 3.6 Topology and placement + +| You need to... | Look in | +|---|---| +| Set up the 9-layer location hierarchy | [`topology/canonical-9-layer-hierarchy.md`](topology/canonical-9-layer-hierarchy.md) | +| Configure placement and priority bands | [`topology/placement-and-priority-bands.md`](topology/placement-and-priority-bands.md) | + +### 3.7 Integrations and reference + +| You need to... | Look in | +|---|---| +| Integrate with ITSM (ServiceNow, Jira, Remedy) | [`integrations/itsm.md`](integrations/itsm.md) | +| Read the Kessel integration evaluation | [`integrations/kessel-evaluation.md`](integrations/kessel-evaluation.md) | +| Look up implementation standards (algorithms, RFCs, FIPS levels) | [`../reference/implementation-standards.md`](../reference/implementation-standards.md) | +| Operational runbooks and CLI reference | [`../reference/operational-reference.md`](../reference/operational-reference.md) | +| Implementation specifications | [`../reference/implementation-specifications.md`](../reference/implementation-specifications.md) | +| See orchestration scenarios | [`../examples/orchestration-scenarios.md`](../examples/orchestration-scenarios.md) | + +--- + +## 4. The operational rhythm + +DCM is a convergence-driven control plane. The operational rhythm has four +repeating beats: + +1. **Ingress** — a consumer submits intent (via API, GitOps PR, CLI, message bus, + or scheduled trigger). The API gateway authenticates, the Request Orchestrator + acknowledges. +2. **Assemble** — the Request Processor performs nine-step assembly: layer + resolution, policy evaluation, scoring, placement. A new Requested State is + written. +3. **Dispatch and realize** — the Request Orchestrator dispatches to the selected + provider with a scoped, short-lived interaction credential. The provider + realizes; DCM persists the Realized State. +4. **Reconcile** — the Discovery Service polls providers on schedule, compares + Discovered State to Realized State, fires drift events. The Policy Engine + evaluates each drift through Recovery Policies. The loop continues. + +Most operator work is configuring the policies, profiles, and providers that +shape this loop — not writing the loop itself. + +--- + +## 5. The profile system is your primary configuration lever + +DCM ships with six built-in profiles: `minimal`, `dev`, `standard`, `prod`, +`fsi`, `sovereign`. The profile controls dozens of enforcement and threshold +settings simultaneously: credential lifetimes, TLS requirements, MFA +requirements, scoring thresholds, approval routing tiers, scheduling horizons, +audit retention, and more. + +**Pick the profile that matches your operational risk tolerance and let it +drive the defaults.** Override individual settings only when you have a +specific reason. + +The profile is not a UDLM concept — it's a DCM ease-of-use scaling mechanism. +A peer realization could choose a different scaling axis. UDLM only requires +that the chosen mechanism produce wire-compatible behavior. + +See [`governance-enforcement/policy-profiles.md`](governance-enforcement/policy-profiles.md). + +### 5.1 Profiles govern the control plane, not the managed infrastructure + +Profiles are purely a control-plane lever: they govern *how* the control plane +behaves — strictness, thresholds, automation level, review windows. They do not +shape the infrastructure DCM manages. That is a different set of levers: + +- **Placement / validation / transformation policies** — what may be built, + where, and under what constraints. +- **Catalog and composite-service definitions** — what infrastructure shapes + are offerable in the first place. +- **Provider capability declarations** — what a provider can actually realize. + +Put simply: the **profile** decides how the control plane operates; **policy + +catalog + provider** decide what infrastructure gets built and under what +constraints. When tuning behavior, reach for the profile; when changing what +DCM can provision, reach for policies, the catalog, and provider capabilities. + +--- + +## 6. Common pitfalls + +- **Don't author against UDLM contracts thinking they're DCM.** UDLM defines + the abstract two-layer provider callback auth contract; DCM picks mTLS + + interaction credential. If your peer realization picks JWT + signed + assertion instead, you're still UDLM-conformant — but you're not DCM. + +- **Don't assume PostgreSQL is the only valid persistence.** UDLM requires + persistence; DCM mandates PostgreSQL for *this realization*. A peer + realization could use a different SQL category and still conform. See + [`persistence/postgres-mandate.md`](persistence/postgres-mandate.md) for + why DCM made this specific choice. + +- **Don't treat the 9-layer location hierarchy as substrate.** UDLM defines + the layered-topology contract abstractly. DCM picks Country → Region → + Zone → Site → Data Center → Hall → Cage → Rack → Unit as its canonical + default. A peer could pick a 6-layer hierarchy or a 12-layer hierarchy and + remain UDLM-conformant. + +- **Don't reach for new mechanisms before extending existing ones.** Most + needs can be expressed through the existing profile + policy + provider + capability extension system. New mechanisms are a last resort. + +--- + +## 7. When you contribute back + +If you find yourself wanting to change something: + +1. Does the change affect wire-level compatibility (data shapes, event + payloads, contract surfaces)? **It belongs in UDLM**, not here. +2. Does it change DCM's realization choices (which database, which auth + mechanism, how the convergence loop works)? **It belongs here.** +3. Does it sit on the boundary? Apply the test from + [layering.md](layering.md): *"could a peer of DCM choose differently?"* + If yes → DCM. If no → UDLM. + +The split manifest at [`00-split-manifest.md`](00-split-manifest.md) is the +durable record of how this boundary was originally drawn. Read it before +proposing structural changes. diff --git a/architecture/overview.md b/architecture/overview.md new file mode 100644 index 0000000..4c91262 --- /dev/null +++ b/architecture/overview.md @@ -0,0 +1,75 @@ +--- +Document Status: ✅ Stable — DCM architecture entry point +Document Type: Architecture Overview +Established: 2026-05-26 +--- + +# DCM Architecture Overview + +> **Implements contracts defined in UDLM**: this entire DCM repo realizes the +> substrate specified at [github.com/croadfeldt/udlm](https://github.com/croadfeldt/udlm). +> Every architectural choice documented here is one possible realization of +> a UDLM-conformant peer. + +DCM (Data Center Management) is a concrete operational platform built on +**UDLM (Universal Data Lifecycle Model)**. UDLM owns the substrate — entity +types, the four states (intent / requested / realized / discovered), state +transitions and invariants, wire contracts (provider, policy, events, data +store), provenance, identity, reference taxonomies. UDLM is what peers must +share to interoperate. + +DCM owns the operationalization — the convergence engine, control-plane +components, deployment topology, persistence mandate, runtime features, +governance enforcement, credentials, and integrations with specific external +systems. + +For the deeper conceptual layering and the boundary test ("could a peer of +DCM, built independently, choose to do this differently and still be a valid +realization of the same data?"), see [layering.md](layering.md). For the +narrative operator perspective, see [operator-perspective.md](operator-perspective.md). + +--- + +## Reading order for new contributors + +1. Read this overview. +2. Read [layering.md](layering.md) for the UDLM/DCM boundary. +3. Read [operator-perspective.md](operator-perspective.md) for the operator narrative. +4. Read [convergence-engine/overview.md](convergence-engine/overview.md) for how DCM walks data through the four states. +5. Use the per-concern subdirectory READMEs to dive into specific areas as needed. + +--- + +## How this repo is organized + +DCM groups documentation by **architectural concern**, not by file number. + +| Subdirectory | What lives here | +|---|---| +| [`control-plane/`](control-plane/) | The deployable services — API gateway, components, self-health, internal component auth, session revocation, API versioning | +| [`convergence-engine/`](convergence-engine/) | The intent→realized loop — policy evaluation, scoring, recovery and retry, dependency orchestration, provider matching | +| [`ingestion/`](ingestion/) | Brownfield ingestion engine and workload analysis | +| [`credentials-and-auth/`](credentials-and-auth/) | Auth implementation, credential management, provider callback auth, authority tier enforcement | +| [`governance-enforcement/`](governance-enforcement/) | Matrix evaluator, accreditation monitor, registry enforcement, contribution pipeline, policy profiles | +| [`runtime-features/`](runtime-features/) | Scheduling, notifications, webhooks/messaging, federation runtime, deployment redundancy | +| [`topology/`](topology/) | DCM's canonical 9-layer location hierarchy and placement/priority bands | +| [`persistence/`](persistence/) | The PostgreSQL mandate and its implementation | +| [`integrations/`](integrations/) | ITSM and Kessel evaluation | + +Adjacent top-level directories: + +| Directory | What lives here | +|---|---| +| [`../examples/`](../examples/) | DCM-specific orchestration scenarios that build on UDLM canonical examples | +| [`../reference/`](../reference/) | Implementation standards, operational reference, implementation specifications | +| `deployment/` *(planned — not yet in the repo)* | Deployment topology, Kubernetes manifests | + +--- + +## Permanent split context + +The DCM repo split from a single combined `architecture/data-model/` tree in +2026-05-26. The split planning record is preserved at +[`00-split-manifest.md`](00-split-manifest.md) — kept as a permanent contextual +artifact so future contributors can understand which decisions are load-bearing +and why files live where they do. diff --git a/architecture/persistence/postgres-implementation.md b/architecture/persistence/postgres-implementation.md new file mode 100644 index 0000000..f318e8d --- /dev/null +++ b/architecture/persistence/postgres-implementation.md @@ -0,0 +1,542 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — PostgreSQL Implementation +Established: 2026-05-26 +Maps to: udlm/design-principles/infrastructure-optimization.md +--- + +# PostgreSQL Implementation + +> **Implements contracts defined in UDLM**: +> [udlm/design-principles/infrastructure-optimization.md](https://github.com/croadfeldt/udlm/blob/main/design-principles/infrastructure-optimization.md). +> UDLM requires that the four data domains be persistently queryable with +> declared immutability invariants. This document specifies DCM's +> PostgreSQL realization: schema, enforcement mechanisms, query optimization, +> and retention policies. + +> See [`postgres-mandate.md`](postgres-mandate.md) for the architectural +> decision and rationale. + +--- + +## 1. Schema design + +DCM's database schema enforces the four-domain contracts through PostgreSQL +native features. + +### 1.1 Intent domain + +```sql +-- Append-only. Raw consumer declarations. Never modified after write. + +CREATE TABLE intent_records ( + intent_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + entity_uuid UUID NOT NULL, + tenant_uuid UUID NOT NULL REFERENCES tenants(tenant_uuid), + catalog_item_uuid UUID NOT NULL, + submitted_by UUID NOT NULL, + submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + submitted_via VARCHAR(32) NOT NULL + CHECK (submitted_via IN ('api', 'gitops', 'cli', 'message_bus')), + intent_version INTEGER NOT NULL DEFAULT 1, + fields JSONB NOT NULL DEFAULT '{}', + provenance JSONB NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_intent_entity ON intent_records(entity_uuid, intent_version); +CREATE INDEX idx_intent_tenant ON intent_records(tenant_uuid, submitted_at); + +REVOKE UPDATE, DELETE ON intent_records FROM dcm_app; +``` + +### 1.2 Requested domain + +```sql +-- Append-only. Assembled, policy-evaluated, placed payloads. + +CREATE TABLE requested_records ( + requested_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + entity_uuid UUID NOT NULL, + tenant_uuid UUID NOT NULL REFERENCES tenants(tenant_uuid), + operation_uuid UUID NOT NULL REFERENCES operations(operation_uuid), + intent_uuid UUID NOT NULL REFERENCES intent_records(intent_uuid), + resource_type VARCHAR(256) NOT NULL, + provider_uuid UUID NOT NULL, + assembled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + assembled_payload JSONB NOT NULL DEFAULT '{}', + layer_sources JSONB NOT NULL DEFAULT '[]', + policy_results JSONB NOT NULL DEFAULT '{}', + placement_result JSONB NOT NULL DEFAULT '{}', + provenance JSONB NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_requested_entity ON requested_records(entity_uuid); +CREATE INDEX idx_requested_tenant ON requested_records(tenant_uuid); +CREATE INDEX idx_requested_operation ON requested_records(operation_uuid); + +REVOKE UPDATE, DELETE ON requested_records FROM dcm_app; +``` + +### 1.3 Realized domain + +```sql +-- Versioned rows. is_current flag. Append-on-change semantics. + +CREATE TABLE realized_entities ( + realized_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + entity_uuid UUID NOT NULL, + tenant_uuid UUID NOT NULL REFERENCES tenants(tenant_uuid), + resource_type VARCHAR(256) NOT NULL, + provider_uuid UUID NOT NULL, + requested_uuid UUID NOT NULL REFERENCES requested_records(requested_uuid), + realized_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + version_major INTEGER NOT NULL, + version_minor INTEGER NOT NULL, + version_revision INTEGER NOT NULL, + is_current BOOLEAN NOT NULL, + lifecycle_state VARCHAR(32) NOT NULL, + realized_payload JSONB NOT NULL DEFAULT '{}', + provider_metadata JSONB NOT NULL DEFAULT '{}', + provenance JSONB NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_realized_entity_current ON realized_entities(entity_uuid) + WHERE is_current = true; +CREATE INDEX idx_realized_entity_history ON realized_entities(entity_uuid, realized_at); +CREATE INDEX idx_realized_tenant ON realized_entities(tenant_uuid, realized_at); +CREATE INDEX idx_realized_provider ON realized_entities(provider_uuid, realized_at); + +-- Append-on-change enforced via trigger that updates is_current on previous version +-- when a new version is inserted; the previous row's is_current flips to false. +``` + +### 1.4 Discovered domain + +```sql +-- Ephemeral snapshots from provider discovery runs. + +CREATE TABLE discovered_records ( + discovery_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + entity_uuid UUID, -- null for orphans + tenant_uuid UUID REFERENCES tenants(tenant_uuid), + provider_uuid UUID NOT NULL, + resource_type VARCHAR(256) NOT NULL, + discovered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + discovery_run_uuid UUID NOT NULL, + discovered_fields JSONB NOT NULL DEFAULT '{}', + provider_native_id VARCHAR(512), + match_confidence VARCHAR(16) DEFAULT 'exact' + CHECK (match_confidence IN ('exact', 'high', 'low', 'unmatched')) +); + +CREATE INDEX idx_discovered_entity ON discovered_records(entity_uuid, discovered_at); +CREATE INDEX idx_discovered_run ON discovered_records(discovery_run_uuid); +CREATE INDEX idx_discovered_orphans ON discovered_records(entity_uuid) + WHERE entity_uuid IS NULL; +``` + +### 1.5 Pipeline events + +```sql +-- Append-only event log. Replaces Kafka for pipeline routing in standard deployments. +-- LISTEN/NOTIFY provides real-time notification to pipeline consumers. + +CREATE TABLE pipeline_events ( + event_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + event_type VARCHAR(128) NOT NULL, + entity_uuid UUID, + request_uuid UUID, + tenant_uuid UUID, + actor_uuid UUID, + payload JSONB NOT NULL DEFAULT '{}', + published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + consumed_by JSONB NOT NULL DEFAULT '[]', + consumed_at TIMESTAMPTZ +); + +CREATE INDEX idx_events_type ON pipeline_events(event_type, published_at); +CREATE INDEX idx_events_entity ON pipeline_events(entity_uuid, published_at); +CREATE INDEX idx_events_unconsumed ON pipeline_events(event_type, published_at) + WHERE consumed_at IS NULL; + +REVOKE UPDATE, DELETE ON pipeline_events FROM dcm_app; + +-- Notify function for real-time pipeline routing +CREATE OR REPLACE FUNCTION notify_pipeline_event() RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('dcm_pipeline', json_build_object( + 'event_uuid', NEW.event_uuid, + 'event_type', NEW.event_type, + 'entity_uuid', NEW.entity_uuid + )::text); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER pipeline_event_notify + AFTER INSERT ON pipeline_events + FOR EACH ROW EXECUTE FUNCTION notify_pipeline_event(); +``` + +--- + +## 2. Enforcement mechanisms + +### 2.1 Append-only via REVOKE + +The application role (`dcm_app`) has only INSERT and SELECT permissions on +append-only tables. UPDATE and DELETE are revoked: + +```sql +REVOKE UPDATE, DELETE ON intent_records FROM dcm_app; +REVOKE UPDATE, DELETE ON requested_records FROM dcm_app; +REVOKE UPDATE, DELETE ON pipeline_events FROM dcm_app; +REVOKE UPDATE, DELETE ON audit_records FROM dcm_app; +``` + +Database administration roles retain full access for operational concerns +(point-in-time recovery, planned schema migrations), but the application +cannot mutate append-only data. + +### 2.2 Row-Level Security (tenant isolation) + +Every table with tenant data has RLS enforced: + +```sql +ALTER TABLE intent_records ENABLE ROW LEVEL SECURITY; +ALTER TABLE requested_records ENABLE ROW LEVEL SECURITY; +ALTER TABLE realized_entities ENABLE ROW LEVEL SECURITY; +ALTER TABLE discovered_records ENABLE ROW LEVEL SECURITY; +ALTER TABLE pipeline_events ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenant_isolation_intent ON intent_records + FOR ALL TO dcm_app + USING (tenant_uuid = current_setting('dcm.current_tenant_uuid')::uuid); + +CREATE POLICY tenant_isolation_requested ON requested_records + FOR ALL TO dcm_app + USING (tenant_uuid = current_setting('dcm.current_tenant_uuid')::uuid); + +CREATE POLICY tenant_isolation_realized ON realized_entities + FOR ALL TO dcm_app + USING (tenant_uuid = current_setting('dcm.current_tenant_uuid')::uuid); + +CREATE POLICY tenant_isolation_discovered ON discovered_records + FOR ALL TO dcm_app + USING (tenant_uuid = current_setting('dcm.current_tenant_uuid')::uuid); + +CREATE POLICY tenant_isolation_events ON pipeline_events + FOR SELECT TO dcm_app + USING (tenant_uuid = current_setting('dcm.current_tenant_uuid')::uuid); +``` + +The API Gateway sets `dcm.current_tenant_uuid` at session startup per the +authenticated request's tenant scope. RLS enforces that no query can return +rows from other tenants — even with a buggy WHERE clause (STI-001, STI-002). + +Platform admin queries set `dcm.current_tenant_uuid = '*'` (resolved via a +separate RLS policy that grants cross-tenant access only to platform_admin +role). + +### 2.3 Merkle tree (audit integrity) + +Audit integrity follows the **RFC 9162 Merkle-tree** model (DCM ADR-010; the +normative contract is UDLM `AUD-006` / universal-audit §8) — **not** a linked +hash chain. Each `audit_records` row is a **leaf**: `record_hash` is the +SHA-256 of the record's canonical content (the Merkle leaf hash), and +`leaf_index` is its append-only position in the tree. `previous_leaf_hash` +survives only as the per-request **chain-of-custody** link +(`output_payload_hash[N] == input_payload_hash[N+1]`); it is not the integrity +mechanism. The root of trust is a periodically **signed tree head**. + +```sql +CREATE TABLE audit_records ( + audit_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + leaf_index BIGINT NOT NULL, -- append-only position in the Merkle tree + request_uuid UUID, -- chain-of-custody grouping + entity_uuid UUID, + tenant_uuid UUID, + action VARCHAR(64) NOT NULL, + actor_uuid UUID, + actor_type VARCHAR(32) NOT NULL, + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + payload JSONB NOT NULL DEFAULT '{}', + previous_leaf_hash CHAR(64), -- per-request chain of custody, not the integrity root + record_hash CHAR(64) NOT NULL, -- SHA-256 of canonical content = the Merkle leaf hash + UNIQUE (leaf_index) +); + +-- Signed Tree Heads: the root of trust (AUD-006 §8.3). Recomputed and signed +-- every N leaves or T seconds (profile-governed). +CREATE TABLE audit_signed_tree_heads ( + tree_size BIGINT NOT NULL, -- number of leaves covered + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + sha256_root_hash CHAR(64) NOT NULL, -- Merkle root + signature BYTEA NOT NULL, -- Ed25519, DCM's audit signing key + PRIMARY KEY (tree_size) +); + +CREATE INDEX idx_audit_entity ON audit_records(entity_uuid, timestamp); +CREATE INDEX idx_audit_actor ON audit_records(actor_uuid, timestamp); +CREATE INDEX idx_audit_action ON audit_records(action, timestamp); + +REVOKE UPDATE, DELETE ON audit_records FROM dcm_app; +``` + +Verification is by **inclusion and consistency proofs** against a signed tree +head (AUD-006 §8.4), not by walking a linked chain — so targeted integrity +checks (a single entity's or request's leaves) are O(log n) against a tree head +and need no full-database recompute, and an external auditor verifies with only +the tree head and the proof. + +### 2.4 Append-on-change via trigger (Realized) + +```sql +CREATE OR REPLACE FUNCTION update_realized_current() RETURNS TRIGGER AS $$ +BEGIN + -- Flip previous version's is_current to false + UPDATE realized_entities + SET is_current = false + WHERE entity_uuid = NEW.entity_uuid + AND realized_uuid != NEW.realized_uuid + AND is_current = true; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER realized_version_update + AFTER INSERT ON realized_entities + FOR EACH ROW WHEN (NEW.is_current = true) + EXECUTE FUNCTION update_realized_current(); +``` + +DCM never UPDATEs realized rows in-place — it always INSERTs a new version +with `is_current = true`. The trigger atomically flips the previous version's +flag. + +--- + +## 3. Query optimization and indexing + +### 3.1 Hot paths + +| Query | Index strategy | +|---|---| +| Current state of entity | `idx_realized_entity_current` (partial index WHERE is_current = true) | +| Entity history | `idx_realized_entity_history` (entity_uuid, realized_at) | +| Tenant catalog browse | `idx_realized_tenant` | +| Provider drift comparison | `idx_realized_provider` joined with `idx_discovered_entity` | +| Pipeline event delivery | `idx_events_unconsumed` (partial index WHERE consumed_at IS NULL) | +| Audit chain verification (per entity) | `idx_audit_entity` | +| Policy evaluation context lookup | `idx_policies_active` (partial WHERE status = 'active') | + +### 3.2 JSONB GIN indexes + +For policy evaluation and complex queries on assembled_payload: + +```sql +CREATE INDEX idx_requested_payload_gin ON requested_records USING GIN (assembled_payload); +CREATE INDEX idx_realized_payload_gin ON realized_entities USING GIN (realized_payload); +``` + +These enable fast `assembled_payload @> '{...}'` queries used by policy +evaluation and audit search. + +### 3.3 Materialized views (catalog browse) + +```sql +CREATE MATERIALIZED VIEW catalog_browse_view AS + SELECT + ci.catalog_item_uuid, + ci.handle, + ci.display_name, + ci.resource_type, + ci.allowed_locations, -- pre-joined from layer_reference + ci.allowed_versions, + ci.tenant_visibility, + rts.schema_summary, + pco.cost_estimate + FROM catalog_items ci + LEFT JOIN resource_type_specs rts USING (resource_type) + LEFT JOIN provider_cost_overview pco USING (catalog_item_uuid) + WHERE ci.status = 'active'; + +CREATE UNIQUE INDEX idx_catalog_browse ON catalog_browse_view(catalog_item_uuid); + +-- Refresh on catalog change events via pipeline_events trigger +``` + +Materialized views handle catalog browse and other read-heavy lookups. They +refresh on `catalog.changed` events; staleness window is bounded by the +event delivery latency (PT5S in standard deployments). + +For workloads requiring sub-second refresh, Redis cache is an optional +deployment enhancement (see [`postgres-mandate.md` §3](postgres-mandate.md)). + +--- + +## 4. Connection pooling + +DCM uses PgBouncer in transaction-pooling mode for connection management: + +```ini +[databases] +dcm = host=postgres.dcm.svc port=5432 dbname=dcm_prod + +[pgbouncer] +pool_mode = transaction +default_pool_size = 50 +min_pool_size = 10 +max_client_conn = 10000 +server_idle_timeout = 600 +``` + +Each DCM control plane service instance establishes a connection through +PgBouncer; PgBouncer multiplexes onto a smaller backend pool. This supports +thousands of concurrent API requests without exhausting PostgreSQL backend +connections. + +--- + +## 5. Data retention and archival policies + +DCM applies per-domain retention policies: + +| Domain | Default retention | Notes | +|---|---|---| +| Intent | Indefinite | Audit and reproducibility; small per-record size | +| Requested | Indefinite | Provenance chain for active entities; archived per profile after entity decommission | +| Realized (historical versions) | Per profile: P365D (homelab) → P10Y (fsi/sovereign) | `is_current = false` rows | +| Realized (current) | While entity active; permanent post-decommission for audit | `is_current = true` rows; immutable after decommission | +| Discovered | P30D rolling | Discovery snapshots; not retained as authoritative state | +| Pipeline events | P7D rolling for delivered events; indefinite for replay-eligible | Consumed events purged; never-consumed events retained for replay | +| Audit | Indefinite | Tamper-evident chain; never deleted; archived to cold storage per profile | + +### 5.1 Archival mechanism + +For long-retention data (audit, decommissioned entity records), DCM +supports archival to cold storage: + +```yaml +archival_policy: + archive_to: s3 | gcs | azure_blob | filesystem + archive_after: P1Y # archive after 1 year (configurable) + archive_format: jsonl + sha256 manifest + archive_encryption: AES-256-GCM with archived-data-encryption-key (HSM) + verification_schedule: P30D # periodic random-sample verification + retention_in_archive: P10Y (default; fsi/sovereign extends to P30Y) +``` + +Archived data remains queryable through DCM's audit-archive API, with +multi-second latency vs sub-second for hot data. + +### 5.2 Profile-governed retention defaults + +| Profile | Historical Realized retention | Audit retention | Archival enabled | +|---|---|---|---| +| homelab | P90D | P1Y | No | +| dev | P180D | P1Y | No | +| standard | P365D | P3Y | Optional | +| prod | P3Y | P7Y | Yes | +| fsi | P7Y | P10Y | Yes; PCI DSS, SOX requirements | +| sovereign | P10Y | P30Y | Yes; sovereign data residency in archive | + +--- + +## 6. High availability and disaster recovery + +### 6.1 Standard HA + +DCM standard deployments use PostgreSQL streaming replication or +Patroni-managed clusters: + +- 1 primary + 2 streaming replicas +- Synchronous replication to at least one replica (`synchronous_commit = remote_apply` + for fsi/sovereign) +- Automatic failover via Patroni leader election +- PgBouncer reads from primary; failover transparent to DCM services + +### 6.2 Backup strategy + +- Continuous archiving via WAL streaming to object storage (S3 / GCS / etc.) +- Daily base backups (pg_basebackup or Velero with Crunchy Operator) +- Point-in-time recovery to any second within the WAL retention window +- Encrypted backups (AES-256-GCM with HSM-managed KEK for fsi/sovereign) + +### 6.3 Cross-zone DR + +For sovereign deployments, each sovereignty zone has independent PostgreSQL +HA. Cross-zone DR uses the DCM federation mechanism (signed export bundles) +rather than database-level replication — preserves the sovereignty boundary. + +--- + +## 7. Sovereignty partitioning + +``` +Sovereign deployment: + zone-1 (EU): PostgreSQL HA cluster, DCM control plane, local providers + zone-2 (US): PostgreSQL HA cluster, DCM control plane, local providers + zone-3 (APAC): PostgreSQL HA cluster, DCM control plane, local providers + +Federation between zones: DCM-to-DCM mTLS tunnels per +runtime-features/federation-runtime.md +No cross-zone database replication. +``` + +Each zone's PostgreSQL is independent. The database boundary IS the +sovereignty boundary. RLS still applies within each zone for tenant isolation. + +--- + +## 8. Schema migration + +DCM uses a forward-only migration tool (`golang-migrate` or `dbmate`): + +``` +schemas/sql/ + 001-initial.sql # base schema + 002-add-conformance.sql + 003-add-archival.sql + ... +``` + +Migrations run as part of DCM control plane bootstrap. The application role +does not have schema-change permissions; only the migration tool's +dedicated role (with DDL grants) runs migrations. + +Major schema changes require coordinated DCM version bumps and are +documented in `../reference/implementation-specifications.md`. + +--- + +## 9. Operational queries + +Standard operator queries that should be fast (< 100ms p99): + +| Query | Expected response | +|---|---| +| `GET /api/v1/resources/{entity_uuid}` | Current Realized State for entity | +| `GET /api/v1/resources/{entity_uuid}/audit` | Per-entity audit chain | +| `GET /api/v1/catalog` | Browse-able catalog items for current tenant | +| `GET /api/v1/admin/policies` | Active policies list (platform admin) | +| `GET /api/v1/admin/drift?since=PT1H` | Recent drift events | +| `GET /api/v1/admin/orphans` | Orphan candidate review queue | + +Queries that may take longer (< 5s acceptable): + +| Query | Notes | +|---|---| +| `GET /api/v1/admin/audit/search?q=...` | Full-text audit search; JSONB GIN | +| `GET /api/v1/admin/cost/attribution?range=...` | Aggregated cost analysis | +| `POST /api/v1/admin/audit/verify` | Hash chain verification for a tenant | + +--- + +## 10. Realization note + +The schemas, indexes, and operational patterns above are **DCM's specific +realization choices**. A peer DCM realization using a different storage +technology would have its own equivalent enforcement mechanisms — different +SQL, different indexes, potentially different concurrency models — while +satisfying the same UDLM persistence contract. diff --git a/architecture/persistence/postgres-mandate.md b/architecture/persistence/postgres-mandate.md new file mode 100644 index 0000000..f9cc090 --- /dev/null +++ b/architecture/persistence/postgres-mandate.md @@ -0,0 +1,186 @@ +--- +Document Status: ✅ Stable — DCM architectural decision +Document Type: Architecture Reference — Persistence Decision +Established: 2026-05-26 +Maps to: udlm/design-principles/infrastructure-optimization.md +--- + +# PostgreSQL Mandate + +> **Implements contracts defined in UDLM**: +> [udlm/design-principles/infrastructure-optimization.md](https://github.com/croadfeldt/udlm/blob/main/design-principles/infrastructure-optimization.md). +> UDLM requires that all four data domains (Intent, Requested, Realized, +> Discovered) be persistently queryable. The technology choice is not +> specified by UDLM — it is a realization-layer decision. DCM mandates +> **PostgreSQL** (or any PostgreSQL-compatible database: CockroachDB, Aurora +> PostgreSQL, Crunchy Postgres) as its required persistence infrastructure. + +This is a **DCM-level architectural decision**, not a UDLM contract. A peer +DCM realization could pick a different SQL category (or a non-SQL category +entirely) and still satisfy the UDLM persistence contract, provided it +honors immutability, queryability, and the wire-level data formats. + +--- + +## 1. The decision + +**DCM mandates PostgreSQL as its sole required external infrastructure.** + +All other dependencies — identity, secrets, event streaming, caching, Git +ingress, service mesh — can either be handled internally by DCM or +optionally delegated to external systems. PostgreSQL is the floor. + +Specifically: any PostgreSQL-compatible database satisfies the mandate. + +| Acceptable | Reason | +|---|---| +| PostgreSQL (vanilla) | Reference implementation | +| CockroachDB | Wire-compatible; native HA; sovereignty-friendly partitioning | +| Aurora PostgreSQL | AWS-managed; PostgreSQL wire-compatible | +| Crunchy Postgres | K8s-native operator; PostgreSQL upstream | +| YugabyteDB (PG mode) | Distributed; PostgreSQL wire-compatible | + +What disqualifies a database: missing JSONB, missing `LISTEN/NOTIFY`, missing +RLS, missing append-only tables (REVOKE UPDATE/DELETE), missing `pgcrypto`, +or any subset of these. DCM's contract enforcement assumes all of them. + +--- + +## 2. Why PostgreSQL (the rationale) + +DCM's design principle is: prescribe **data contracts** (schemas, +immutability rules, versioning, the Merkle-tree audit) — not infrastructure products. +Where a contract maps directly to a single well-understood infrastructure +category, DCM prescribes the category and the contract, not an abstraction +layer over it. + +Abstraction layers earn their place when the underlying implementations have +genuinely different interaction contracts — different APIs, different +lifecycle semantics, different operational models. When the implementations +share a standard protocol (SQL, OIDC, AMQP), the protocol is the +abstraction. Adding a DCM-specific abstraction on top of a standard +protocol is unnecessary indirection. + +PostgreSQL satisfies every UDLM persistence contract obligation through +native features: + +| UDLM contract obligation | PostgreSQL native feature | +|---|---| +| Append-only on Intent / Requested / Audit | `REVOKE UPDATE, DELETE` + audit trigger | +| Versioning on Realized | Row versioning with semantic version columns + `is_current` flag | +| Tamper-evident audit | RFC 9162 Merkle tree over the `audit_records` leaves, signed tree heads | +| Tenant isolation | Row-Level Security (RLS) policies | +| Event-driven pipeline routing | `LISTEN/NOTIFY` | +| JSONB document storage | Native JSONB with GIN indexes | +| Strong transactional consistency | ACID transactions across all DCM tables | +| Sovereignty partitioning | One PostgreSQL instance per sovereignty zone | +| Air-gapped deployment | Single dependency to operate offline | + +### 2.1 Why not separate stores per domain + +| Concern | Four-store answer (Git + Kafka + Redis + PostgreSQL) | Single-store answer (PostgreSQL) | +|---|---|---| +| Immutability | Git commits are immutable | Append-only tables + REVOKE UPDATE, DELETE + audit trigger | +| Version history | Git log | Row versioning with semantic version fields | +| Audit trail | Git commit metadata | RFC 9162 Merkle tree (stronger — inclusion/consistency proofs against signed tree heads vs Git's graph integrity) | +| PR-based review | Native Git workflow | DCM's Policy Engine + Scoring Model + Authority Tier routing (more sophisticated) | +| Tamper evidence | Git SHA integrity | Per-leaf Merkle inclusion (per-record, not per-repo) | +| Transactional consistency | Cross-store sync required | Native — intent + audit + operation in same transaction | +| Sovereignty partitioning | Separate Git/Kafka/Redis per zone | Separate PostgreSQL instance per zone (one thing to deploy, not four) | +| Air-gapped deployment | Git + Kafka + Redis + PostgreSQL (4 infra dependencies) | PostgreSQL only (1 dependency) | +| Operations skill set | Git admin + Kafka admin + Redis admin + DBA | DBA only | + +The four-store approach was historically considered but rejected on +operational and integrity grounds. A single well-understood database +provides stronger guarantees with one-fourth the operational surface. + +### 2.2 The "no abstraction layer over standard protocols" principle + +DCM does NOT abstract over SQL. The Catalog Manager, Policy Manager, +Request Orchestrator, and all other services use PostgreSQL directly. The +schema is defined by DCM; the queries are DCM-native. + +This is the same pattern DCM applies elsewhere: + +- OIDC is the abstraction over identity providers; DCM does not add a + DCM-specific identity abstraction +- AMQP/Kafka is the abstraction over message buses; DCM does not add a + DCM-specific bus abstraction +- gRPC/HTTP+JSON is the abstraction over RPC; DCM does not add a + DCM-specific RPC layer + +For persistence, SQL is the abstraction. PostgreSQL is the implementation. +DCM mandates PostgreSQL because writing PostgreSQL-flavored SQL is more +direct, more debuggable, and more operationally stable than writing +SQL-via-an-abstraction. + +--- + +## 3. Optional infrastructure (deployment enhancements) + +PostgreSQL is required. Everything else is optional: + +| Infrastructure | When to add | What it provides | +|---|---|---| +| OIDC IdP | Enterprise auth; multi-tenant federation | External authentication via registered auth_provider | +| Vault | Existing Vault infrastructure; dynamic secrets; full HSM seal | External secrets backend | +| Kafka | >1000 events/sec; multiple consumer groups with independent replay | Replaces pipeline_events + LISTEN/NOTIFY | +| Redis | Read-heavy catalog/placement workloads; geo-distributed reads | Replaces materialized views | +| Git repository | CI/CD integration; PR-based ingress | Adds Git as ingress path alongside API/CLI | +| Service mesh | Production mTLS between services | Replaces application-level TLS config | + +Each can be added per-deployment without changing the architectural mandate. +None are required. + +--- + +## 4. Deployment profiles + +| Profile | Required | Optional | +|---|---|---| +| **Minimal** (homelab/dev) | PostgreSQL (single instance) | — | +| **Standard** (production) | PostgreSQL (HA) | Keycloak, Vault, Service mesh, Redis | +| **Enterprise** (large scale) | PostgreSQL (HA + read replicas) | Keycloak (HA), Vault (HA + HSM), Service mesh, Kafka, Redis, Git | +| **Sovereign** (air-gapped) | PostgreSQL (per-zone) | Keycloak (per-zone), Vault (per-zone + HSM seal), Service mesh | + +--- + +## 5. Sovereignty partitioning + +DCM supports sovereignty zones by deploying separate PostgreSQL instances +per zone (one DCM control plane plus database per sovereignty zone). The +zones do not share a database; federation between zones uses the +DCM-to-DCM federation mechanism (see +[`../runtime-features/federation-runtime.md`](../runtime-features/federation-runtime.md)), +not database replication. + +This means a `sovereign` deployment has N PostgreSQL instances for N +sovereignty zones — never one database serving multiple zones. The single +database per zone simplifies sovereignty enforcement (the database +boundary IS the sovereignty boundary). + +--- + +## 6. Realization note + +PostgreSQL is **DCM's choice**, not a UDLM requirement. UDLM requires: + +- All four data domains are persistently queryable +- Wire-level data formats are honored +- Immutability invariants are maintained for Intent / Requested / Audit +- Versioning is supported for Realized +- Schema-sharing protocol permits federation peers to exchange schemas + +A peer DCM realization could pick: + +- A purpose-built database (e.g., a wide-column store + audit chain) +- A multi-engine architecture (e.g., separate ledger + queryable cache) +- A different SQL category (MySQL, MariaDB, SQL Server) + +...and remain UDLM-conformant, provided the four-domain queryability and +immutability invariants are honored. This DCM realization deliberately +picks a single well-understood SQL category and avoids abstraction-over-SQL. + +For implementation details — table structures, schema, query optimization, +indexing, data retention — see +[`postgres-implementation.md`](postgres-implementation.md). diff --git a/architecture/runtime-features/deployment-redundancy.md b/architecture/runtime-features/deployment-redundancy.md new file mode 100644 index 0000000..5d5baf8 --- /dev/null +++ b/architecture/runtime-features/deployment-redundancy.md @@ -0,0 +1,797 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — Deployment and Redundancy +--- + +# DCM Data Model — Deployment and Redundancy Model + +> **DCM-native runtime feature; no single UDLM contract counterpart.** +> Deployment topology and redundancy are realization-layer concerns. UDLM +> does not mandate a deployment model — a peer DCM realization could choose a +> different redundancy strategy and still satisfy every UDLM contract. This +> document specifies DCM's own deployment and redundancy approach. + + +**Document Status:** ✅ Complete +**Related Documents:** [Context and Purpose](https://github.com/croadfeldt/udlm/blob/main/foundations/context-and-purpose.md) | [data stores](https://github.com/croadfeldt/udlm/blob/main/contracts/storage-providers.md) | [Universal Audit Model](https://github.com/croadfeldt/udlm/blob/main/observability/universal-audit.md) | [Policy Organization](../governance-enforcement/policy-profiles.md) + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [udlm/foundations/foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: DATA + PROVIDER** +> +> Data: deployment specification. Provider: data store redundancy + + + +--- + +> **Operational guidance:** GitOps the disaster recovery runbook and RTO/RPO tables are in [Operational Reference](../../reference/operational-reference.md) Section 3. + +## 1. Purpose + +Every DCM component, every data store, and every capability is designed for redundancy by default. Redundancy is not an add-on or an advanced configuration — it is the baseline operational posture for all profiles above `minimal`. + +**The minimal profile** provides single-instance deployment for home lab and evaluation simplicity. All other profiles assume redundancy as the floor. The transition from `minimal` to `dev` is the transition from "works on my laptop" to "survives a node failure." + +**Everything in DCM runs as a container in a pod.** No bare-metal DCM components. No special-case deployment paths. Every DCM component follows the same container lifecycle, the same health check model, the same rolling update pattern. DCM runs on Kubernetes — and manages Kubernetes. + +**DCM is self-hosting.** DCM's own deployment is expressible as DCM resources. DCM can manage its own lifecycle, detect drift in its own components, and rehydrate its own deployment from Git. This is the ultimate expression of the data center repave use case — DCM restoring itself. + +--- + +## 2. Design Principles + +**Redundant by default.** Every component, store, and capability has a redundancy model. The `minimal` profile sets `replicas: 1`. Every other profile sets `replicas: >= 3` with quorum writes and anti-affinity scheduling. + +**Everything containerized.** All DCM components run as containers in Kubernetes pods. No exceptions. This gives a consistent deployment model, rolling updates, health checks, and self-healing as first-class properties. + +**Profile-governed redundancy.** Replica counts, quorum thresholds, geo-replication, and anti-affinity requirements are declared by the active Profile — not by per-component configuration. Activating a Profile configures redundancy for the entire deployment. + +**Self-hosting.** DCM's own deployment is a DCM resource. DCM manages itself through the same model it uses to manage customer infrastructure. + +**Stateless control plane.** All DCM control plane components are stateless — all state lives in external stores. Any component instance can fail and be replaced without data loss. State recovery means restarting a pod — not restoring a database. + +**Quorum writes for durability.** All durable stores use quorum writes — a write is confirmed only when a majority of replicas acknowledge it. This ensures durability even if a minority of replicas fail simultaneously. + +--- + +## 3. Component Redundancy Model + +### 3.1 Control Plane Components + +All control plane components are stateless, horizontally scalable, and deployed as Kubernetes Deployments with configurable replica counts. + +```yaml +component_redundancy: + component: request_payload_processor # same model for all components + deployment_type: kubernetes_deployment + replicas: 3 # set by active Profile + affinity: + anti_affinity: required # pods spread across nodes + zone_spread: preferred # prefer spreading across availability zones + disruption_budget: + min_available: 2 # always keep 2 running during rolling updates + health_check: + liveness: + path: /healthz + interval_seconds: 10 + failure_threshold: 3 + readiness: + path: /readyz + interval_seconds: 5 + failure_threshold: 2 + rolling_update: + strategy: RollingUpdate + max_unavailable: 0 # never take a pod down before replacement is ready + max_surge: 1 +``` + +**Control plane components:** + +| Component | Stateless? | Replica Model | +|-----------|-----------|--------------| +| API Gateway | Yes | Deployment + HorizontalPodAutoscaler | +| Request Payload Processor | Yes | Deployment | +| Policy Engine (OPA) | Yes | Deployment + PolicyBundle sidecar | +| Placement Engine | Yes | Deployment | +| Service Catalog | Yes | Deployment | +| IDM / IAM | Yes | Deployment (external IdP recommended) | +| Audit Forward Service | Yes | Deployment (1 active + 1 standby) | +| Lifecycle Constraint Enforcer | Yes | Deployment (leader election) | +| Drift Detection | Yes | Deployment (leader election for scheduling) | +| Resource Discovery | Yes | Deployment (leader election) | +| Message Bus Router | Yes | Deployment | + +**Leader election** for scheduler-type components (Lifecycle Constraint Enforcer, Drift Detection, Resource Discovery): multiple replicas run but only one holds the leader lease at a time. On leader failure, a replica acquires the lease within seconds. + +### 3.2 Data Store Redundancy + +All DCM data stores run as containers. Each store type has a declared replication and quorum model. + +#### Commit Log + +```yaml +commit_log: + replicas: 3 + write_quorum: 2 # confirmed durable when 2/3 replicas acknowledge + read_quorum: 1 # any replica can serve reads + affinity: + zone_spread: required # replicas MUST span availability zones + implementation: etcd # or equivalent consensus store + # etcd is purpose-built for this pattern: Raft consensus, quorum writes, + # sub-millisecond local writes, proven in Kubernetes itself +``` + +The Commit Log uses consensus protocol (Raft/equivalent). A write is confirmed when the quorum acknowledges — ensuring durability even if a minority of replicas fail simultaneously. + +#### DCM database (Intent, Requested, Layers, Policies) + +```yaml +gitops_store: + implementation: gitea # or equivalent self-hosted Git + replicas: 3 + replication_mode: active_active # any node can accept writes + write_quorum: 2 + backup: + enabled: true + schedule: "0 */6 * * *" # every 6 hours + retention: 30d +``` + +#### pipeline_events table (Realized, Discovered, Audit Events) + +```yaml +event_stream: + implementation: kafka # or equivalent + brokers: 3 + replication_factor: 3 + min_insync_replicas: 2 # minimum replicas that must acknowledge a write + partitions: 12 # enables parallel consumption + retention: + bytes: -1 # unlimited — retention governed by policy + ms: -1 # unlimited +``` + +#### Audit Store + +```yaml +audit_store: + implementation: elasticsearch # or equivalent — optimized for queryable retention + replicas: 3 + primary_shards: 5 + replica_shards: 1 # each shard has 1 replica = 2 copies total + geo_replicated: true # in prod/fsi/sovereign profiles + append_only_enforced: true # storage layer enforces immutability +``` + +#### Search Index (Non-Authoritative) + +```yaml +search_index: + implementation: elasticsearch + replicas: 2 # lower redundancy — can rebuild from Git + rebuild_from_git: true # on data loss, rebuild from authoritative stores +``` + +### 3.3 Container Specification + +Every DCM component pod follows a common security and resource model: + +```yaml +pod_spec: + security_context: + run_as_non_root: true + run_as_user: 65534 # nobody + run_as_group: 65534 + fs_group: 65534 + seccomp_profile: + type: RuntimeDefault + capabilities: + drop: [ALL] + + containers: + - name: + image: ghcr.io/dcm-project/: + image_pull_policy: IfNotPresent + security_context: + read_only_root_filesystem: true + allow_privilege_escalation: false + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: 2000m + memory: 2Gi + liveness_probe: + readiness_probe: + volume_mounts: + - name: tmp + mount_path: /tmp # writable temp — no other writable paths + + volumes: + - name: tmp + empty_dir: {} +``` + +--- + +## 4. Redundancy by Profile + +Profile activation configures redundancy for the entire deployment. Organizations do not configure replica counts individually — they activate a profile. + +```yaml +# Redundancy matrix per profile +redundancy_matrix: + minimal: + control_plane_replicas: 1 + store_replicas: 1 + write_quorum: false + zone_spread: false + geo_replication: false + anti_affinity: false + note: "Single-instance. No redundancy. Home lab and evaluation only." + + dev: + control_plane_replicas: 1 + store_replicas: 1 + write_quorum: false + zone_spread: false + geo_replication: false + anti_affinity: false + note: "Single-instance with backup. Basic resilience for dev environments." + + standard: + control_plane_replicas: 3 + store_replicas: 3 + write_quorum: 2 # 2 of 3 + zone_spread: preferred + geo_replication: false + anti_affinity: required + note: "Production baseline. Survives single node or zone failure." + + prod: + control_plane_replicas: 3 + store_replicas: 3 + write_quorum: 2 + zone_spread: required + geo_replication: true + anti_affinity: required + sla_disruption_budget: "always 2 replicas available" + note: "Production with SLA. Geo-replicated stores." + + fsi: + control_plane_replicas: 5 + store_replicas: 5 + write_quorum: 3 # 3 of 5 + zone_spread: required + geo_replication: true + anti_affinity: required + audit_store_replicas: 5 + note: "FSI-grade. Higher quorum threshold. Compliance-grade audit." + + sovereign: + control_plane_replicas: 5 + store_replicas: 5 + write_quorum: 3 + zone_spread: required + geo_replication: true # within sovereignty boundary only + anti_affinity: required + air_gap_backup: true + sovereignty_boundary_enforced: true + note: "Maximum. All geo-replication within sovereignty boundary." +``` + +--- + +## 5. The DCM Deployment Specification + +The DCM deployment itself is a DCM resource — declared in YAML, stored in Git, governed by Policy, subject to the same four-state lifecycle as any other resource. + +```yaml +dcm_deployment: + artifact_metadata: + uuid: + handle: "deployments/primary/dcm-control-plane" + version: "1.0.0" + status: active + + profile: system/profile/standard + kubernetes_namespace: dcm-system + + # Redundancy — set by active Profile, overridable per component + redundancy: + control_plane: + replicas: 3 + affinity: + anti_affinity: required + zone_spread: preferred + disruption_budget: + min_available: 2 + + stores: + commit_log: + replicas: 3 + write_quorum: 2 + implementation: etcd + gitops_store: + replicas: 3 + write_quorum: 2 + implementation: gitea + event_stream: + brokers: 3 + replication_factor: 3 + min_insync_replicas: 2 + implementation: kafka + audit_store: + replicas: 3 + geo_replicated: false # standard profile default + implementation: elasticsearch + search_index: + replicas: 2 + implementation: elasticsearch + + # Provider health configuration + providers: + health_check_interval_seconds: 30 + unhealthy_threshold: 3 + automatic_failover: true + + # Self-hosting: DCM manages its own deployment + self_managed: true + drift_detection_enabled: true + rehydration_enabled: true + # DCM detects if its own components drift from declared spec + # and can rehydrate (redeploy) from this declaration +``` + +--- + +## 6. Self-Hosting — DCM Managing Itself + +DCM's own deployment is managed through the same model it uses to manage customer infrastructure. This is the **self-hosting principle** — DCM eats its own cooking. + +### 6.1 What Self-Hosting Means + +- DCM control plane components are defined as Resource Entities in DCM +- DCM data stores are defined as data store resources in DCM +- DCM's own Policy Groups govern DCM's own deployment constraints +- DCM runs drift detection on its own components — a component running the wrong image version is drift +- DCM can rehydrate its own deployment from the `dcm_deployment` declaration in Git + +### 6.2 The Bootstrap Problem + +DCM cannot manage itself before it exists. The bootstrap sequence: + +``` +1. Bootstrap installer deploys minimal DCM (single instance, no redundancy) + from a declarative bootstrap manifest + │ +2. Bootstrap DCM reads the target dcm_deployment declaration from Git + │ +3. Bootstrap DCM provisions itself to the target state: + - Scales from 1 to N replicas + - Provisions redundant stores + - Configures quorum + │ +4. Bootstrap instance hands off to the now-redundant DCM + │ +5. DCM manages its own lifecycle from this point forward +``` + +The bootstrap manifest is the only thing that exists outside DCM's management scope. It is minimal by design — just enough to get DCM running. + +### 6.3 Self-Hosted Drift Detection + +DCM continuously compares its own running state against the `dcm_deployment` declaration: + +| Drift Type | Example | Response | +|-----------|---------|---------| +| Wrong image version | Component running v1.1.0, declared v1.2.0 | Rolling update triggered | +| Wrong replica count | 2 replicas running, declared 3 | Scale-up triggered | +| Wrong resource limits | Component using more than declared limits | Alert + potential eviction | +| Store replication mismatch | Store has 2 replicas, declared 3 | Replication repair triggered | + +### 6.4 The Repave Scenario + +The ultimate test of self-hosting: DCM is lost entirely (ransomware, catastrophic failure). Recovery: + +``` +1. Deploy bootstrap installer to new Kubernetes cluster + │ +2. Bootstrap DCM reads dcm_deployment declaration from Git backup + │ +3. DCM provisions itself — full redundant deployment + │ +4. DCM reads all resource declarations from Git + │ +5. DCM rehydrates customer workloads in dependency order + │ +6. Drift detection validates recovered state matches declared state +``` + +The recovery time is bounded by infrastructure provisioning speed — not by backup restoration or manual configuration. Everything is code. Everything is declarative. Everything is in Git. + +--- + +## 7. Commit Log Redundancy — Two-Stage Audit Integration + +The Commit Log is the synchronous component of the two-stage audit model. In a distributed deployment, "synchronous durable write" means quorum acknowledgment: + +``` +DCM component initiates change + │ + ▼ +Write to Commit Log (Raft consensus) + │ Propose to leader + │ Leader replicates to followers + │ Write confirmed when quorum (2/3 or 3/5) acknowledge + │ + ├── Replica 1 (local node) → ACK ─┐ + ├── Replica 2 (different node) → ACK ─┤ quorum reached + └── Replica 3 (different zone) → ACK ─┘ + │ + ▼ Operation returns success (< 1ms typical with NVMe) + │ + ▼ [async — Audit Forward Service] +Read from any surviving Commit Log replica + │ Enrich → write to Audit Store + └── Clear Commit Log entry after Audit Store confirms +``` + +**Failure scenarios and recovery:** + +| Failure | During Stage 1 | Effect | +|---------|---------------|--------| +| Single replica fails before quorum | Quorum still achievable | No impact | +| Majority fail before quorum | Commit Log unavailable | Operation aborted — no silent change | +| Leader fails after quorum | New leader elected (seconds) | In-flight writes complete on new leader | +| All replicas fail after quorum | Audit Forward Service reads from backup | Full recovery on restart | +| Audit Store unavailable | Commit Log accumulates | Forward resumes when Audit Store recovers | + +--- + +## 8. Network Architecture + +> **Full internal auth specification:** See [Internal Component Authentication](../control-plane/internal-component-auth.md) for component identity model, Internal CA, bootstrap protocol, and ICOM-001–ICOM-009 system policies. + +### 8.1 Service Mesh + +All DCM component-to-component communication uses a service mesh (Istio or equivalent): +- mTLS everywhere (RFC 8446 TLS 1.3 + RFC 5280 X.509) — no plaintext internal communication +- Traffic policies enforced at mesh level +- Observability: traces, metrics, logs for all inter-component calls +- Circuit breaking: prevent cascade failures + +### 8.2 Ingress + +External traffic enters through a redundant Ingress layer: + +``` +External clients + │ + ▼ +Load Balancer (external — cloud or on-premises) + │ + ▼ +Ingress Controller (replicated — 2+ instances) + │ + ▼ +API Gateway pods (3+ instances, anti-affinity) + │ + ▼ +Internal service mesh +``` + +### 8.3 DNS and Service Discovery + +All DCM components address each other via Kubernetes Service DNS. No hardcoded IPs. Service discovery is automatic — a new pod replica is immediately addressable. + +--- + +## 9. DCM System Policies — Redundancy + +| Policy | Rule | +|--------|------| +| `RED-001` | All DCM control plane components must run as containers in Kubernetes pods | +| `RED-002` | All control plane components must be stateless — all persistent state in external stores | +| `RED-003` | In profiles above `minimal`, all control plane components must have `replicas >= 3` with anti-affinity | +| `RED-004` | All durable stores in profiles above `minimal` must use quorum writes with `write_quorum >= 2` | +| `RED-005` | The Commit Log must use consensus protocol (Raft or equivalent) with quorum writes | +| `RED-006` | The DCM deployment must be declared as a DCM resource in Git — self-hosting required | +| `RED-007` | DCM must run drift detection on its own components — version drift is treated as resource drift | +| `RED-008` | A rolling update of any DCM component must not reduce available replicas below `min_available` | +| `RED-009` | All DCM component communication must use mTLS — no plaintext internal communication | +| `RED-010` | The bootstrap manifest is the only DCM configuration outside DCM's management scope | + +--- + +## 10. Open Questions + +| # | Question | Impact | Status | +|---|----------|--------|--------| +| 1 | Should the bootstrap manifest be version-controlled and verifiable? | Bootstrap integrity | ✅ Resolved — GitOps store + hash verification at every startup; tampering prevents start; operator-signed (RED-011) | +| 2 | How does DCM handle Kubernetes cluster upgrades in sovereign deployments? | Operational | ✅ Resolved — pre-staged images via signed bundles; maintenance mode during upgrade; startup verification before resume (RED-012) | +| 3 | Should DCM support non-Kubernetes container runtimes? | Portability | ✅ Resolved — Kubernetes primary/required for production; Podman/Docker Compose for dev/community only (RED-013) | +| 4 | What is the minimum hardware specification per profile? | Implementation | ✅ Resolved — declared as DCM Resource definitions; enforced by placement engine; table documented (RED-014) | +| 5 | How does DCM's self-hosted drift detection handle DCM drifting from its own state? | Self-hosting | ✅ Resolved — DCM is DCM-managed resource; Operator reconciles; bootstrap hash provides independent check; audit Merkle tree externally verifiable via inclusion/consistency proofs (RED-015) | + +--- + +## 11. Related Concepts + +- **Universal Audit Model** ([universal-audit.md](https://github.com/croadfeldt/udlm/blob/main/observability/universal-audit.md)) — two-stage audit; Commit Log quorum model +- **Policy Organization** (now the policy-contract / policy-groups model) — Profile-governed redundancy configuration +- **data stores** ([data-store-contracts.md](https://github.com/croadfeldt/udlm/blob/main/contracts/data-store-contracts.md)) — Store contracts include replication requirements +- **Four States** ([four-states.md](https://github.com/croadfeldt/udlm/blob/main/foundations/four-states.md)) — all state stores are redundant per this model +- **Ingestion Model** ([ingestion-model.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/ingestion-model.md)) — DCM's own deployment recovery uses the repave/rehydration pattern + + +## 8. Deployment Redundancy Gap Resolutions + +### 8.1 Bootstrap Manifest Verification (Q1) + +The bootstrap manifest is stored in the GitOps store, hash-verified at DCM startup and every restart. Tampering prevents DCM from starting. + +```yaml +bootstrap_manifest: + version: "1.0.0" + manifest_uuid: + manifest_hash: # computed at creation; verified at every startup + signed_by: # signed by deploying operator +``` + +DCM startup sequence: verify manifest hash → verify manifest signature → proceed with initialization. On failure: refuse to start; emit security alert; notify platform admin. + +### 8.2 Kubernetes Cluster Upgrades in Sovereign Deployments (Q2) + +Sovereign DCM (air-gapped) uses pre-staged container images and the maintenance mode pattern: + +``` +Pre-upgrade: + 1. Pre-stage all DCM container images in local registry (signed bundles) + 2. DCM enters maintenance mode — new requests queued; in-flight complete + +During upgrade: + 3. Kubernetes upgrade proceeds using pre-staged images + 4. DCM components restart against new cluster version + +Post-upgrade: + 5. Startup verification (bootstrap manifest hash check) + 6. DCM exits maintenance mode — queued requests resume +``` + +Same signed bundle model as registry updates — no new pattern needed. + +### 8.3 Non-Kubernetes Container Runtime Support (Q3) + +Kubernetes is DCM's primary and recommended container runtime. Non-Kubernetes runtimes (Podman, Docker Compose) are supported for development and community extension only. The DCM Operator is Kubernetes-native and not supported on other runtimes. Production deployments must use Kubernetes. + +### 8.4 Minimum Hardware Specifications (Q4) + +Expressed as DCM Resource definitions per profile — machine-readable and enforced by the placement engine during self-deployment. + +| Profile | CPU | Memory | Storage | Replicas | +|---------|-----|--------|---------|---------| +| homelab | 2 cores | 4 Gi | 20 Gi | 1 | +| dev | 4 cores | 8 Gi | 50 Gi | 1 | +| standard | 8 cores | 16 Gi | 100 Gi | 3 | +| prod | 16 cores | 32 Gi | 200 Gi | 3 | +| fsi | 32 cores | 64 Gi | 500 Gi | 5 | +| sovereign | 32 cores | 64 Gi | 500 Gi | 5 | + +These are minimums. Production workloads may require significantly more based on managed resource count. + +### 8.5 DCM Self-Hosted Drift Detection (Q5) + +DCM's own deployment is a DCM-managed resource subject to the same drift detection as any other resource. The DCM Operator continuously reconciles running components against the declared deployment manifest. + +**The bootstrap paradox — who watches the watchman:** +- DCM Operator drifts → bootstrap manifest hash verification (RED-011) detects independently +- Audit component compromised → Audit Store Merkle-tree break is detectable by external verification (failed inclusion/consistency proof) +- Full DCM compromise → signed bundle verification at import time provides external trust anchor + +### 8.6 System Policies — Deployment Redundancy Gaps + +| Policy | Rule | +|--------|------| +| `RED-011` | The bootstrap manifest is version-controlled in the GitOps store, hash-verified at DCM startup and every restart. Bootstrap manifest tampering prevents DCM from starting and triggers a security alert. | +| `RED-012` | Kubernetes cluster upgrades in Sovereign DCM deployments use pre-staged container images from the local signed bundle registry. DCM enters maintenance mode during upgrade. In-flight operations complete before upgrade. DCM exits maintenance mode on successful startup verification. | +| `RED-013` | Kubernetes is DCM's primary and recommended container runtime. Non-Kubernetes runtimes are supported for development and community extension purposes only. Production deployments must use Kubernetes. | +| `RED-014` | Minimum hardware specifications are expressed as DCM Resource definitions per profile and enforced by the placement engine during DCM self-deployment. | +| `RED-015` | DCM's own deployment is a DCM-managed resource subject to the same drift detection as any other resource. Bootstrap manifest hash verification (RED-011) provides independent verification. Audit Store Merkle-tree breaks are detectable externally via inclusion/consistency proofs. | + + + +--- + +## 9. Bootstrap Tenant Creation Sequence + +### 9.1 The Bootstrap Problem + +DCM requires every entity to belong to exactly one Tenant. But during initial deployment, no Tenants exist. The bootstrap sequence resolves this by creating the foundational Tenants as part of DCM startup, declared in the bootstrap manifest. + +### 9.2 The Three Foundation Tenants + +The bootstrap manifest declares three system Tenants that are created before any consumer can submit requests: + +```yaml +bootstrap_tenants: + - handle: "__platform__" + display_name: "DCM Platform" + purpose: "Owns DCM's own control plane resources (components, stores, providers)" + automatically_created: true + cannot_be_decommissioned: true + + - handle: "__transitional__" + display_name: "Transitional" + purpose: "Holds brownfield entities during ingestion before promotion to a real Tenant" + automatically_created: true + cannot_be_decommissioned: true + + - handle: "__system__" + display_name: "System" + purpose: "Owns system-level artifacts (system layers, system policies, system workflows)" + automatically_created: true + cannot_be_decommissioned: true +``` + +### 9.3 Bootstrap Startup Sequence + +``` +DCM starts + │ + ▼ Step 1: Verify bootstrap manifest hash and signature + │ + ▼ Step 2: Initialize storage providers + │ GitOps stores initialized + │ Audit Store initialized + │ Commit Log initialized + │ + ▼ Step 3: Create foundation Tenants (if not already existing) + │ __platform__, __transitional__, __system__ + │ + ▼ Step 4: Create initial Platform Admin actor + │ Declared in bootstrap manifest + │ Assigned to __platform__ Tenant + │ Given platform_admin role + │ + ▼ Step 5: Activate system domain layers and policies + │ System layers loaded from GitOps store + │ System policies activated + │ Built-in recovery profiles activated + │ + ▼ Step 6: Register built-in providers + │ Built-in Auth Provider + │ Search Index data store + │ Audit Store data store + │ (All owned by __platform__ Tenant) + │ + ▼ Step 7: DCM ready + Consumer API, Provider API, Admin API accepting requests + Platform Admin can now create organization Tenants + Organization Tenants can request resources +``` + +### 9.4 System Policy + +| Policy | Rule | +|--------|------| +| `RED-016` | The three foundation Tenants (__platform__, __transitional__, __system__) are created during bootstrap and cannot be decommissioned. All DCM control plane resources are owned by __platform__. All brownfield ingested entities enter __transitional__ before promotion. | + + + +--- + +## 9. Bootstrap Sequence and Initial Tenant Creation + +### 9.1 The Bootstrap Problem + +The DCM data model requires every entity to be owned by a Tenant. But Tenants are themselves DCM entities. The bootstrap sequence defines how the initial Tenants and platform admin actor are created before DCM can accept external requests. + +### 9.2 Bootstrap Manifest + +The bootstrap manifest (RED-011) declares the initial state required for DCM to start. It includes: + +```yaml +bootstrap_manifest: + version: "1.0.0" + signed_by: + + # Initial system Tenants (created before any external requests) + system_tenants: + - uuid: + handle: "__platform__" + display_name: "DCM Platform" + description: "System Tenant owning DCM's own control plane resources" + immutable: true # cannot be decommissioned or modified by regular operators + + - uuid: + handle: "__transitional__" + display_name: "Transitional" + description: "Holding Tenant for brownfield ingestion (INGEST phase)" + immutable: true + + # Initial platform admin actor + bootstrap_admin: + uuid: + username: "dcm-bootstrap-admin" + auth_provider: builtin + roles: [platform_admin] + credential_ref: + # This credential is rotated on first login + + # Active profile for initial deployment + initial_profile: + deployment_posture: minimal # or as declared; can be changed post-bootstrap + compliance_domains: [] + + # Bootstrap admin's initial Tenant + initial_tenant: + uuid: + handle: "org-default" + display_name: "Default Organization Tenant" + owned_by: bootstrap_admin +``` + +### 9.3 Bootstrap Sequence + +``` +DCM starts → bootstrap manifest hash verified (RED-011) + │ + ▼ System Tenants created (before Policy Engine active): + │ __platform__ Tenant — owns DCM control plane resources + │ __transitional__ Tenant — brownfield ingestion holding + │ These are created by the bootstrap process itself, not through the request pipeline + │ + ▼ Bootstrap admin actor created + │ Auth Provider initialized with bootstrap credential + │ Platform Admin role assigned + │ + ▼ Initial profile activated + │ Deployment posture policies loaded + │ Compliance domain policies loaded (if declared) + │ + ▼ Policy Engine comes online + │ All subsequent operations go through the standard request pipeline + │ + ▼ Bootstrap admin creates the initial organization Tenant (optional) + │ First real request through the pipeline + │ Creates the initial production Tenant for organizational resources + │ + ▼ Bootstrap admin credential rotation notification sent + │ Bootstrap credential must be rotated on first login + │ After rotation, bootstrap_admin becomes a standard platform admin actor + │ + ▼ DCM accepts external requests +``` + +### 9.4 System Tenants + +The `__platform__` and `__transitional__` Tenants are created by the bootstrap process and are immutable: + +| System Tenant | Purpose | Who can modify | +|--------------|---------|---------------| +| `__platform__` | Owns DCM's own control plane resources | Platform Admin (restricted operations only) | +| `__transitional__` | Brownfield ingestion holding area | Ingestion pipeline only | + +These Tenants are exempt from the normal Tenant decommission workflow — they cannot be decommissioned while DCM is operational. + +### 9.5 System Policy + +| Policy | Rule | +|--------|------| +| `BOOT-001` | The __platform__ and __transitional__ system Tenants are created by the bootstrap process before the Policy Engine comes online. They are immutable and cannot be decommissioned while DCM is running. | +| `BOOT-002` | The bootstrap admin credential must be rotated on first login. The bootstrap manifest declares the initial credential reference only; the credential itself is managed by the credential management service. | +| `BOOT-003` | After bootstrap, all Tenant creation and modification goes through the standard request pipeline. The bootstrap process is a one-time operation. | + + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/runtime-features/federation-runtime.md b/architecture/runtime-features/federation-runtime.md new file mode 100644 index 0000000..0e15b15 --- /dev/null +++ b/architecture/runtime-features/federation-runtime.md @@ -0,0 +1,761 @@ +--- +Document Status: ✅ Complete +Document Type: Architecture Reference — Federation Runtime +Maps to: udlm/governance/federated-contribution-model.md +--- + +# DCM Data Model — DCM Federation, Peering, and Cross-Instance Coordination + +> **Implements contracts defined in UDLM**: +> [udlm/governance/federated-contribution-model.md](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md). +> UDLM defines the federated contribution model — how peer DCM instances +> contribute, share, and govern data across instance boundaries, including +> the contribution authority and cross-instance trust contracts. DCM +> operationalizes peer DCMs as typed Providers, the peering and tunnel +> runtime, and the cross-instance coordination of contributed data. + + +**Document Status:** ✅ Complete +**Related Documents:** [Federated Contribution Model](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md) | [Universal Group Model](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md) | [data stores](https://github.com/croadfeldt/udlm/blob/main/contracts/storage-providers.md) | [Auth Providers](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [Information Providers Advanced](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md) + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [udlm/foundations/foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: PROVIDER + POLICY** +> +> Provider: Peer DCM as typed Provider. Policy: federation governance rules + + + +--- + +> **Federated Contribution:** Federation contribution follows the [Federated Contribution Model](https://github.com/croadfeldt/udlm/blob/main/governance/federated-contribution-model.md) — peer DCMs are contributors to each other's artifact stores, scoped by their federation trust posture. + +## 1. Purpose + +DCM instances do not operate in isolation. Organizations with multiple data centers, regions, or organizational boundaries may run multiple DCM instances that need to coordinate, share resources, and maintain consistent governance. This document defines how DCM instances relate to each other — as peers, in parent-child hierarchies, or as hub-and-spoke configurations — and the mechanisms for cross-instance resource sharing, data export/import, and provider federation eligibility. + +--- + +## 2. DCM-to-DCM Relationship Types + +### 2.1 Three Relationship Types + +**Peer DCM** — two DCM instances at the same organizational level that share resources or information. Regional DCM instances sharing VLAN allocations. Campus DCM instances sharing compute capacity across departments. + +**Parent-Child DCM** — hierarchical relationship where the parent has governance overlay authority over child instances. Corporate DCM with regional children. Service provider DCM with customer children. Parent does not own child resources — it has governance visibility and policy overlay authority (same model as nested Tenants in the Universal Group Model). + +**Hub DCM** — specialized Parent DCM acting as a clearinghouse for resource allocation across multiple children. The Hub holds the master resource inventory; children request allocations from the Hub. + +### 2.2 Relationship Mapping to Universal Group Model + +DCM-to-DCM relationships use the existing Universal Group Model: + +```yaml +# Peer relationship — federation group +dcm_group: + group_class: federation + name: "EU-US Regional Federation" + members: + - member_uuid: + member_type: dcm_peer + member_role: eu_region_dcm + - member_uuid: + member_type: dcm_peer + member_role: us_region_dcm + federation_config: + shared_policy_inheritance: opt_in + cross_member_visibility: audit_only + +# Parent-Child relationship — nesting +dcm_group: + group_class: tenant_boundary + name: "Corporate DCM" + child_groups: + - + - + policy_inheritance: opt_out # parent policies cascade unless child excludes +``` + +--- + +## 3. Provider Federation Eligibility + +### 3.1 Concept + +Every provider registration carries a `federation_eligibility` declaration — whether the provider can participate in cross-DCM federation, with whom, and under what conditions. This is **layer-defined** (static organizational knowledge) and **policy-enforced** (runtime governance). + +### 3.2 Federation Eligibility on Provider Registration + +```yaml +provider_registration: + handle: "providers/service/eu-compute-primary" + + federation_eligibility: + mode: + # none: Provider cannot participate in any DCM federation + # (sovereign, classified, or compliance-restricted providers) + # selective: Federation permitted only with explicitly declared partners + # open: Federation permitted with any trusted DCM peer + # (sovereignty checks always apply regardless) + + permitted_partners: + - partner_type: + dcm_instance_uuids: [] # specific instances + dcm_instance_tags: [region-eu, internal] # tag-based matching + dcm_certification_required: [ISO-27001, GDPR-compliant] + relationship_requires_approval: true # bilateral approval required + + federation_scope: + permitted_resource_types: + - resource_type: Compute.VirtualMachine + operations: [allocate, query_capacity] + # NOT: decommission — remote DCMs cannot decommission local resources + - resource_type: Network.VLAN + operations: [allocate, query_capacity, release] + data_sharing: + capacity_data: true + realized_state: false # do not share realized state details + pricing_data: true + sovereignty_declaration: true # always share — required for federation + max_allocations_per_partner: 100 + max_concurrent_allocations: 500 + + override_reason: null # populated when overriding layer default +``` + +### 3.3 Layer-Defined Federation Defaults + +Federation eligibility defaults live in a `platform` domain layer — static organizational knowledge inherited by all providers unless overridden. Individual provider registrations may be **more restrictive** than the layer default (always permitted); **less restrictive** requires compliance Validation Policy approval. + +```yaml +layer: + handle: "platform/federation/provider-federation-defaults" + domain: platform + priority: 600.0.0 + fields: + provider_federation_defaults: + compute_providers: + federation_eligibility: + mode: selective + permitted_partners: + - partner_type: dcm_peer + dcm_instance_tags: [internal, eu-region] + dcm_certification_required: [ISO-27001] + federation_scope: + permitted_resource_types: + - resource_type: Compute.VirtualMachine + operations: [allocate, query_capacity] + (prescribed infrastructure)s: + federation_eligibility: + mode: none # storage never federated — data sovereignty + network_providers: + federation_eligibility: + mode: selective + permitted_partners: + - partner_type: dcm_peer + dcm_instance_tags: [internal] + information_providers: + federation_eligibility: + mode: selective + data_sharing_restrictions: + max_classification: internal # never share confidential/restricted +``` + +### 3.4 Policy Enforcement on Federation + +Policies act on federation eligibility at three enforcement points: + +**At tunnel establishment:** +```yaml +policy: + type: gating + target: dcm_tunnel_establishment + rule: > + If provider.federation_eligibility.mode == none + THEN gate: "Provider is not eligible for federation" + +policy: + type: gating + target: dcm_tunnel_establishment + rule: > + If remote_dcm.sovereignty_zone NOT IN permitted_sovereignty_zones + THEN gate: "Remote DCM sovereignty zone incompatible with local requirements" + +policy: + type: gating + target: dcm_tunnel_establishment + rule: > + If remote_dcm.certifications NOT CONTAINS + provider.federation_eligibility.permitted_partners.dcm_certification_required + THEN gate: "Remote DCM does not hold required certifications" +``` + +**At allocation time:** +```yaml +policy: + type: gating + target: cross_dcm_allocation + rule: > + If resource_type NOT IN provider.federation_eligibility.federation_scope.permitted_resource_types + THEN gate: "Resource type not permitted through this federation tunnel" + +policy: + type: gating + target: cross_dcm_allocation + rule: > + If cross_dcm_allocations_active > provider.federation_eligibility.max_concurrent_allocations + THEN gate: "Maximum concurrent federation allocations exceeded" +``` + +**At data egress:** +```yaml +policy: + type: gating + target: dcm_tunnel_data_egress + rule: > + If data.classification > remote_dcm.max_data_classification_receivable + THEN gate: "Data classification exceeds remote DCM authorization" +``` + +--- + +## 4. The DCM Provider — Cross-Instance Tunneling + +### 4.1 Concept + +A **DCM Provider** is a ninth provider type that wraps another DCM instance's API, enabling one DCM to consume resources managed by another DCM as if they were local providers. + +| # | Type | Purpose | +|---|------|---------| +| 1 | Service Provider | Realizes resources | +| 2 | Information Provider | Serves authoritative external data | +| 3 | composite service definition | Composes multiple providers | +| 4 | data store | Persists DCM state | +| 5 | External Policy Evaluator | Supplies and evaluates policies | +| 6 | event routing service | Bridges internal/external event streams | +| 7 | credential management service | Resolves secrets | +| 8 | Auth Provider | Authenticates identities | +| 9 | **DCM Provider** | Wraps another DCM instance's API | + +### 4.2 DCM Provider Registration + +```yaml +dcm_provider_registration: + artifact_metadata: + uuid: + handle: "providers/dcm/region-eu-dcm" + status: active + + provider_type: dcm_provider + relation: + + remote_dcm: + instance_uuid: + endpoint: https://dcm.region-eu.corp.example.com + dcm_version: "2.1.0" # minimum compatible version + sovereignty_declaration_ref: # verified at registration time + + # Authentication — always mTLS for DCM-to-DCM + auth: + mode: mtls + client_cert_ref: + service_provider_uuid: + path: "dcm/dcm-providers/region-eu/client-cert" + server_ca_ref: + service_provider_uuid: + path: "dcm/dcm-providers/region-eu/server-ca" + + # Tunnel configuration + tunnel_config: + encrypted: true # always — not configurable + sovereignty_boundary_check: true # always — not configurable + permitted_resource_types: [Compute.VirtualMachine, Network.VLAN] + max_allocation_per_request: 10 + audit_forwarding: true # forward audit records to local Audit Store + observability_forwarding: true + + # Sovereignty — must be compatible with local requirements. NOTE: these are the peer's CLAIMS. + # The tunnel-establishment gate reads Accreditation-Monitor-VERIFIED certification/sovereignty + # status (sovereignty_declaration_ref verified at registration; certifications_current in the + # Federation Trust Score), never the raw self-declared values below (ADR-022). + sovereignty_declaration: + remote_jurisdiction: eu-west + data_residency_guarantee: true + certifications: [ISO-27001, GDPR-compliant] # claimed; gate uses the verified status, not this + + # Health check + health_check: + endpoint: /api/v1/health + interval_seconds: 60 + on_unhealthy: suspend_allocations +``` + +### 4.3 Primary Concerns on All DCM Tunnels + +These are non-negotiable on every DCM-to-DCM connection: + +| Concern | Enforcement | +|---------|-----------| +| **Sovereignty** | Sovereignty_declaration verified before tunnel establishment; data classification checked before every egress | +| **Authentication** | Always mTLS — no API key, no bearer token — mutual certificate authentication between DCM instances | +| **Authorization** | Local DCM policies govern ALL resources obtained through a tunnel; remote DCM's policies do not override local | +| **Audit** | All cross-DCM operations produce audit records in BOTH DCM instances; shared `correlation_id` links the two trails | +| **Observability** | Cross-DCM resource allocation visible in both instances' observability stores | +| **Governance** | Local compliance validation policies apply to all resources from any tunnel source | + +--- + +## 5. Cross-DCM Confidence Scoring + +Resources obtained through a DCM tunnel carry compound confidence scores — the resource's confidence in its source DCM, degraded by the tunnel trust score: + +``` +cross_dcm_confidence = source_resource_confidence × (tunnel_trust_score / 100) +``` + +A resource with confidence 90 in source DCM, through a tunnel with trust score 85: `90 × 85/100 = 76.5` → **77** + +### 5.1 Federation Trust Score + +```yaml +dcm_federation_trust_score: + remote_dcm_uuid: + score: 84 # 0-100 + scored_at: + decay_rate: per_30_days + factors: + identity_verified: true # mTLS certificate chain verified + sovereignty_compatible: true + certifications_current: true + audit_trail_integrity: true # audit Merkle proofs verified on sample + uptime_score: 0.98 + compliance_score: 0.90 # policy compliance in recent operations + data_completeness: 0.92 + action_on_score_below: + threshold: 60 + action: +``` + +--- + +## 6. DCM Export and Import + +### 6.1 Export Package + +DCM state is fully exportable as a signed package — for disaster recovery, migration, cross-DCM sharing, and Hub DCM onboarding. + +```yaml +dcm_export_package: + package_uuid: + exported_at: + exported_by: + dcm_version: + signed_by: + + scope: + tenants: [, ...] + resource_types: all + layers: [platform, tenant] + policies: [platform, tenant] + providers: registrations_only # not credentials — never export credentials + entities: [intent_state, requested_state] # not realized (that's provider state) + groups: all + audit_records: + date_range: [, ] + include_merkle_proof: true # inclusion/consistency proofs for audit verification on import + + sovereignty: + classification: internal + permitted_import_jurisdictions: [eu-west] + signed: true + encryption: aes256_gcm +``` + +### 6.2 Import Trust Score + +When importing from another DCM instance, each imported resource carries a trust score: + +```yaml +import_trust_score: + score: 78 # 0-100 + factors: + source_dcm_verified: true # source DCM identity verified + sovereignty_compatible: true + data_completeness: 0.92 + schema_compatibility: 1.00 # source schema matches current version + audit_trail_complete: true # audit records included and Merkle proofs valid + certifications_current: true + action_on_low_score: + threshold: 70 # reject if below +``` + +### 6.3 Scoring in Resource Definition and Allocation + +Resources imported from or allocated through peer DCMs carry their compound confidence score throughout their lifecycle in the importing DCM. The score is visible to the placement engine, Cost Analysis, and the Policy Engine — enabling policies that prefer locally-managed resources over federated resources when scores are comparable. + +--- + +## 7. Profile-Appropriate Federation Policy Groups + +DCM ships built-in federation policy groups activated by default per profile: + +| Group | Profile | Behavior | +|-------|---------|---------| +| `system/group/federation-minimal` | minimal | No federation — single instance only | +| `system/group/federation-dev` | dev | Peer federation permitted; advisory only | +| `system/group/federation-standard` | standard | Peer federation with certification requirements | +| `system/group/federation-prod` | prod | Selective federation; bilateral approval; audit forwarding | +| `system/group/federation-fsi` | fsi | Strict federation; within-jurisdiction only; full audit; no storage federation | +| `system/group/federation-sovereign` | sovereign | No external federation; internal peer federation within sovereignty boundary only | + +--- + +## 8. DCM System Policies — Federation + +| Policy | Rule | +|--------|------| +| `DCM-001` | DCM instances may establish peer, parent-child, or hub relationships using the Universal Group Model federation and nesting constructs. | +| `DCM-002` | All DCM-to-DCM communication uses mTLS. No API key or bearer token. Sovereignty checks are mandatory before tunnel establishment. These requirements are non-configurable. | +| `DCM-003` | Local DCM policies govern all resources obtained through DCM tunnels. Cross-DCM operations produce audit records in both DCM instances with a shared correlation_id. | +| `DCM-004` | DCM state is exportable as a signed package. Imported packages carry a trust score (0-100) computed from source verification, sovereignty compatibility, data completeness, schema compatibility, and audit trail integrity. | +| `DCM-005` | Resources obtained through DCM tunnels carry compound confidence scores: source_resource_confidence × (tunnel_trust_score / 100). | +| `DCM-006` | Every provider registration must declare federation_eligibility (mode: none, selective, or open). Federation eligibility defaults are declared in platform domain layers. Individual provider registrations may be more restrictive — never more permissive without compliance Validation Policy approval. | +| `DCM-007` | Provider federation scope declares: permitted resource types, permitted operations per type, data sharing permissions, and allocation limits. Remote DCMs cannot decommission local resources through a federation tunnel. | +| `DCM-008` | Storage providers default to federation_eligibility.mode: none. Data sovereignty constraints prohibit storage federation unless explicitly authorized by sovereign policy with full justification. | + +--- + +## 9. Open Questions + +| # | Question | Impact | Status | +|---|----------|--------|--------| +| 1 | How are DCM-to-DCM certificate rotation and renewal handled — coordinated or independent? | Operations | ✅ Resolved — independent rotation with P30D overlap; peer notification 60 days before expiry via Message Bus; auto-renewal at 90 days (DCM-009) | +| 2 | Should Hub DCM relationships support automatic load balancing across child DCMs? | Architecture | ✅ Resolved — full placement engine logic at DCM instance level; sovereignty as hard pre-filter; tie-breaking hierarchy same as provider selection; sub-regional routing recursive (DCM-010) | +| 3 | How does drift detection work for resources allocated from a peer DCM — who is responsible for discovery? | Operational | ✅ Resolved — provider-side DCM discovers; consumer-side DCM compares; events via federation Message Bus; peer unavailable = alert-and-hold (DCM-011) | +| 4 | Should cross-DCM audit records be synchronized — so each DCM has the other's audit records? | Compliance | ✅ Resolved — correlation_id reference model; no full sync; on-demand pull with platform admin auth + sovereignty check (DCM-012) | +| 5 | What is the maximum supported federation depth (peer of peer of peer)? | Architecture | ✅ Resolved — profile-governed max depth: homelab/dev=5, standard/prod=3, fsi/sovereign=2; measured as hops from deepest to Hub (DCM-013) | + + +## 11. Federation Gap Resolutions + +### 11.1 Certificate Rotation and Renewal (Q1) + +DCM-to-DCM mTLS certificates rotate independently per instance with a coordinated notification model. Coordinated simultaneous rotation would create a single point of failure. + +```yaml +dcm_federation_cert_rotation: + rotation_model: independent_with_overlap + overlap_period: P30D # old cert valid for 30 days after new cert issued + notification: + notify_peers_at: P60D_before_expiry + notification_channel: message_bus + notification_payload: + new_cert_public_key: + new_cert_valid_from: + old_cert_expires_at: + automatic_renewal: + trigger_at: P90D_before_expiry + requires_approval: false # renewal is automatic; elevation requires approval +``` + +The P30D overlap allows peers to update their trust stores at their own pace without service interruption. + +### 11.2 Federation Routing — Placement Engine at the DCM Level (Q2) + +**Hub DCM federation routing follows the same placement engine logic as provider selection.** Regional DCMs are treated as DCM Provider instances in the placement engine. + +**Sovereignty is a hard pre-filter — not a preference:** + +``` +Before placement loop: + Filter eligible Regional DCMs where: + sovereignty_declaration satisfies request constraints + operating_jurisdictions includes required jurisdictions + sovereignty_zone matches tenant.sovereignty_zone + + If no eligible Regional DCMs → Reject with clear error + Only eligible DCMs enter the placement loop +``` + +**The full federation routing flow:** + +``` +Request arrives at Hub DCM + │ + ▼ Steps 1-5: Standard nine-step assembly (layers, policies, placement constraints) + │ Pre-placement policies may declare federation routing constraints: + │ "This resource must be in a Regional DCM with EU sovereignty" + │ "This Tenant's resources must stay in Regional DCM-EU-West" + │ + ▼ Step 6: Placement loop — at the DCM instance level + │ Reserve query to eligible Regional DCMs: + │ capacity available? sovereignty compatible? trust score adequate? + │ + ▼ Tie-breaking (same hierarchy as provider selection): + │ 1. Policy preference (policy declares preferred Regional DCM) + │ 2. Federation priority (numeric priority on DCM Provider registration) + │ 3. Tenant affinity (Tenant's resources prefer a specific Regional DCM) + │ 4. Sovereignty match quality (exact match over partial match) + │ 5. Geographic affinity (closest regional to consumer) + │ 6. Least loaded (capacity utilization across instances) + │ 7. Consistent hash (deterministic tiebreaker) + │ + ▼ Selected Regional DCM receives assembled request payload + │ Runs its own local assembly and placement (regional layers, regional providers) + │ Returns realization result to Hub DCM → forwarded to consumer + │ + ▼ Sub-regional routing: Regional DCM acts as Hub for its children + Same logic applies recursively within federation depth limit (DCM-013) +``` + +**Load balancing is the least-loaded step in the hierarchy** — not a primary strategy. Sovereignty, policy, and tenant affinity all take precedence. Optional `hub_dcm_load_balancing` configuration: + +```yaml +hub_dcm_load_balancing: + enabled: true # default: true + sovereignty_override: true # always — sovereignty is a hard pre-filter + fallback_on_regional_unavailable: route_to_next_eligible +``` + +### 11.3 Federated Drift Detection Ownership (Q3) + +Provider-side DCM is responsible for discovery; consumer-side DCM is responsible for drift comparison. + +```yaml +federated_drift_detection: + discovery_responsibility: provider_side_dcm + comparison_responsibility: consumer_side_dcm + mechanism: + provider_dcm: + - Run standard discovery against its providers + - Publish Discovered State events to federation Message Bus + - Tagged with: entity_uuid + consumer_dcm_uuid + correlation_id + consumer_dcm: + - Subscribe to Discovered State events for its federated entities + - Compare against its Requested State + - Trigger drift response policy if drift detected + on_peer_dcm_unavailable: + action: alert_and_hold # not assumed drift + max_hold_period: PT24H + on_hold_exceeded: escalate_to_platform_admin +``` + +### 11.4 Cross-DCM Audit Record Correlation (Q4) + +No full synchronization. Each DCM keeps its own authoritative audit trail. Cross-DCM correlation uses correlation_id references and on-demand pull. + +```yaml +cross_dcm_audit_correlation: + model: correlation_id_reference + local_audit_record: + action: ALLOCATE_FROM_PEER + correlation_id: + peer_dcm_uuid: + peer_audit_record_uuid: # reference — not a copy + on_demand_pull: + endpoint: GET /api/v1/audit/cross-dcm/{correlation_id} + requires: platform_admin + peer_dcm_authorization + sovereignty_check +``` + +Full synchronization is not required — auditors follow correlation_id to the peer DCM on demand. + +### 11.5 Maximum Federation Depth (Q5) + +```yaml +federation_depth_policy: + max_depth: 3 # profile-governed + on_max_exceeded: reject_federation_establishment + profile_defaults: + minimal: 5 + dev: 5 + standard: 3 + prod: 3 + fsi: 2 + sovereign: 2 +``` + +Depth is measured as hops from the deepest instance to the Hub DCM. Depth 3 covers Hub → Regional → Sub-Regional → Edge — sufficient for most real-world architectures. + +### 11.6 System Policies — Federation Gaps + +| Policy | Rule | +|--------|------| +| `DCM-009` | DCM-to-DCM mTLS certificates rotate independently per instance with a P30D overlap period. Peers are notified 60 days before expiry via Message Bus. Automatic renewal triggers 90 days before expiry. The overlap period allows peers to update trust stores without coordinated downtime. | +| `DCM-010` | Hub DCM federation routing follows the same placement engine logic as provider selection. Sovereignty is a hard pre-filter — only Regional DCMs satisfying all sovereignty constraints enter the placement loop. The tie-breaking hierarchy applies at the DCM instance level: policy preference → federation priority → tenant affinity → sovereignty match quality → geographic affinity → least loaded → consistent hash. Regional DCMs are treated as DCM Provider instances. Sub-regional routing applies the same logic recursively within the federation depth limit. | +| `DCM-011` | For resources allocated from peer DCMs, the provider-side DCM is responsible for discovery. The consumer-side DCM is responsible for drift comparison. Discovered State events are published via federation Message Bus with correlation_id. Peer DCM unavailability triggers alert-and-hold — not assumed drift. | +| `DCM-012` | Cross-DCM audit records are referenced via correlation_id — not fully synchronized. Each DCM keeps its own authoritative audit trail. Cross-DCM correlation uses on-demand pull with platform admin authorization and sovereignty check. | +| `DCM-013` | Federation depth is limited to a profile-governed maximum (default: 3 for standard/prod; 2 for fsi/sovereign; 5 for homelab/dev). Requests to establish federation beyond the maximum depth are rejected. Depth is measured as hops from the deepest instance to the Hub DCM. | + + +--- + +## 10. Related Concepts + +- **Universal Group Model** ([universal-groups.md](https://github.com/croadfeldt/udlm/blob/main/observability/universal-groups.md)) — federation and nesting group classes +- **data stores** ([data-store-contracts.md](https://github.com/croadfeldt/udlm/blob/main/contracts/data-store-contracts.md)) — storage never federated by default +- **Auth Providers** ([auth-providers.md](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md)) — mTLS for DCM-to-DCM authentication +- **Universal Audit Model** ([universal-audit.md](https://github.com/croadfeldt/udlm/blob/main/observability/universal-audit.md)) — audit records in both DCM instances; correlation_id +- **Registry Governance** ([registry-governance.md](https://github.com/croadfeldt/udlm/blob/main/governance/registry-governance.md)) — signed bundles for air-gapped registry updates +- **Information Providers Advanced** ([information-providers-advanced.md](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md)) — confidence scoring used in cross-DCM context + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* + +--- + +## 12. Federation Tunnel Establishment and Maintenance + +> **Implements contracts defined in UDLM**: +> [udlm/governance/accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). +> UDLM defines the Federation Tunnel Model as the contract for secure +> inter-DCM channels. This section operationalizes the establishment and +> maintenance of those tunnels. + +### 12.1 Tunnel as zero-trust boundary + +A federation tunnel is a mutually authenticated, encrypted, scoped channel — +**not a VPN**. It establishes secure transport, not perimeter trust. Every +message crossing the tunnel is authenticated, authorized, and subject to the +five-check boundary model defined in +[udlm/governance/accreditation-and-authorization-matrix.md](https://github.com/croadfeldt/udlm/blob/main/governance/accreditation-and-authorization-matrix.md). + +### 12.2 Tunnel structure + +```yaml +federation_tunnel: + uuid: + local_dcm_uuid: + remote_dcm_uuid: + tunnel_type: peer | parent_child | hub_spoke + trust_model: zero_trust # always; non-negotiable + + # Mutual authentication + authentication: + protocol: mtls + local_certificate_ref: + remote_certificate_pin: # pinned; not just chain-valid + trust_anchor: + certificate_rotation_interval: P90D + revocation_check: ocsp_stapling + + # Per-message signing + message_integrity: + signing_algorithm: ed25519 + local_signing_key_ref: + remote_verification_key_ref: + replay_protection: true # nonce + timestamp window PT5M + + # Inbound authorization — what remote may request from this DCM + inbound_authorization: + - operation: catalog_query + permitted_resource_types: [Compute.VirtualMachine, Network.VLAN] + requires_cross_tenant_authorization: true + - operation: allocation_request + permitted_resource_types: [Network.IPAddress] + max_allocations_per_request: 10 + requires_cross_tenant_authorization: true + + # Outbound authorization — what this DCM may request from remote + outbound_authorization: + - operation: placement_query + permitted_resource_types: [Compute.VirtualMachine] + - operation: realized_state_query + permitted_entity_uuids: [] # scoped to specific entities + + # Data classification boundary (hard constraints) + data_boundary: + max_outbound_classification: restricted # never send sovereign/classified + max_inbound_classification: restricted + # sovereign profile: max_*_classification: internal + # classified profile: no federation permitted + + # Sovereignty scope + sovereignty_scope: + local_jurisdiction: EU + remote_jurisdiction: EU + cross_jurisdiction_permitted: false # fsi/sovereign: always false +``` + +### 12.3 Federation credential scoping + +Federation credentials are scoped to specific tunnel operations: + +```yaml +federation_credential: + credential_uuid: + issued_by_dcm_uuid: + issued_to_dcm_uuid: + expires_at: # PT15M for fsi/sovereign + operation_scope: catalog_query + scoped_resource_types: [Compute.VirtualMachine] + non_transferable: true + tunnel_uuid: # bound to specific tunnel +``` + +A federation credential issued for `catalog_query` cannot be used for +`allocation_request`. + +### 12.4 Establishment flow + +``` +Local DCM initiates establishment with Remote DCM: + ▼ Mutual mTLS handshake + │ Validate remote certificate against trust_anchor and pin + │ OCSP stapling for real-time revocation check + ▼ Sovereignty compatibility check (hard pre-filter) + │ Local + remote sovereignty zones must satisfy declared requirements + │ Cross-jurisdiction blocked in fsi/sovereign + ▼ Accreditation verification + │ Remote DCM's accreditations checked via Accreditation Monitor + │ Required accreditations per profile + ▼ Tunnel record created (status: establishing) + ▼ Initial federation credential issued (scoped to limited ops) + ▼ Health probe exchange to verify bidirectional connectivity + ▼ Tunnel transitions to active + ▼ Audit record written: federation.tunnel_established +``` + +### 12.5 Tunnel maintenance + +DCM maintains tunnel health via: + +- **Health probes** every PT60S (configurable) +- **Certificate rotation** independent per side with P30D overlap (DCM-009) +- **Federation trust score** updated continuously per + [udlm/contracts/information-providers-advanced.md](https://github.com/croadfeldt/udlm/blob/main/contracts/information-providers-advanced.md) +- **Sovereignty re-verification** on configurable cadence; tunnel suspended + if sovereignty becomes incompatible +- **Federation depth enforcement** per DCM-013 (profile-governed max depth) + +On tunnel degradation: +- Trust score drops below 60 → action per `action_on_score_below` config +- Sovereignty incompatibility → tunnel suspended immediately; admin notified +- Certificate revocation → tunnel suspended; new credential required + +### 12.6 Hub-spoke zero trust + +In hub-spoke federation, the Hub DCM coordinates Regional DCMs. Zero trust +means: + +- The Hub DCM does not have root-level access to Regional DCMs — only + explicitly scoped federation credentials +- A Regional DCM cannot impersonate the Hub to another Regional DCM +- Cross-Regional-DCM operations route through the Hub with the **Hub's + authorization**, not the originating Regional DCM's authorization +- The Hub DCM's accreditation is visible to Regional DCMs — they can verify + the Hub before accepting federation messages + +``` +RegionalDCM-A → HubDCM: authenticated; scoped to allocation_request +HubDCM → RegionalDCM-B: authenticated; scoped to realization_request + Hub presents its own credential to RegionalDCM-B + Not RegionalDCM-A's credential +RegionalDCM-B verifies: Hub certificate; Hub accreditation; data boundary +``` diff --git a/architecture/runtime-features/notifications.md b/architecture/runtime-features/notifications.md new file mode 100644 index 0000000..726ad5f --- /dev/null +++ b/architecture/runtime-features/notifications.md @@ -0,0 +1,601 @@ +# DCM Data Model — Notification Model + + +**Document Status:** ✅ Complete +**Document Type:** Architecture Reference + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [udlm/foundations/foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: PROVIDER + POLICY** +> +> Provider: notification service. Policy: audience resolution and subscription rules + + +**Related Documents:** [Webhooks, Messaging, and External Integration](webhooks-messaging.md) | [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) | [Resource/Service Entities](https://github.com/croadfeldt/udlm/blob/main/entities/resource-service-entities.md) | [Auth Providers](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [Universal Audit](https://github.com/croadfeldt/udlm/blob/main/observability/universal-audit.md) + +--- + + +> **See [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md)** — authoritative source for all DCM event types and payload schemas. + +## 1. Purpose + +The DCM Notification Model defines a **unified, configurable notification pipeline** that routes event notifications to all parties with a stake in a changed resource — not just the original requestor. The audience for any notification is derived from the **entity relationship graph**, not from who submitted the original request. + +This document defines: +- The notification service — the ninth DCM provider type +- The event taxonomy — a closed vocabulary of notification-worthy events +- The audience resolution model — how the relationship graph determines who gets notified +- The subscription model — how actors declare their notification preferences +- The notification payload structure — the unified envelope all notification services receive +- The delivery pipeline — from event trigger through audience resolution through provider delivery + +Outbound webhooks are one delivery channel within this model, implemented via the notification service. + +--- + +## 2. Design Principles + +**Relationship graph determines audience.** When a resource changes, DCM traverses the entity relationship graph to find all stakeholders. A VLAN decommission notifies every VM attached to that VLAN, regardless of which Tenant owns each VM. The graph is the source of truth for notification scope. + +**Delivery mechanism is configurable, not prescribed.** DCM generates and routes notifications. How they are delivered — email, Slack, PagerDuty, ServiceNow, webhook, SMS — is the concern of a notification service. Organizations register the notification service(s) that fit their operations. + +**Three notification tiers.** Some notifications are mandatory and non-suppressable (security, sovereignty violations, audit chain breaks). Some are Tenant-default (all resource lifecycle events in a Tenant). Some are actor-subscription (specific events on specific resources). All three compose without conflict. + +**Audience role shapes the notification.** The same event produces different notifications for an owner ("your resource changed") versus a stakeholder ("a resource you depend on changed") versus an approver ("your approval is required"). The audience role is part of the notification envelope. + +**Delivery is audited.** Every notification dispatch is an audit record. Delivery failures are tracked and escalated per policy. + +--- + +## 3. The notification service + +The notification service is the ninth formal DCM provider type. It handles the translation from DCM's unified notification envelope to the delivery channel's native format, and it handles delivery, retry, and delivery confirmation. + +### 3.1 Provider Types Table Update + +| # | Type | Purpose | +|---|------|---------| +| 1 | Service Provider | Realizes resources | +| 2 | Information Provider | Serves authoritative external data | +| 3 | composite service definition | Composes multiple providers | +| 4 | data store | Persists DCM state | +| 5 | event routing service | Event streaming and messaging | +| 6 | External Policy Evaluator | External policy logic | +| 7 | credential management service | Resolves secrets | +| 8 | Auth Provider | Authenticates identities | +| **9** | **notification service** | **Delivers notifications via configured channels** | + +### 3.2 notification service Registration + +```yaml +service_provider_registration: + artifact_metadata: + uuid: + handle: "org/notifications/slack-provider" + version: "1.0.0" + status: active + owned_by: { display_name: "Platform Engineering" } + + provider_type: notification + display_name: "Slack notification service" + description: "Delivers DCM notifications to configured Slack channels" + + # Delivery channels this provider supports + delivery_channels: + - channel_type: slack + config_schema_ref: # JSON Schema for channel config + supports_threading: true + supports_urgency_routing: true # different channels per urgency level + - channel_type: webhook + config_schema_ref: + + # Sovereignty declaration — same model as all providers + sovereignty_declaration: + data_residency_guarantee: EU + operating_jurisdictions: [DE, FR, NL] + + # Delivery guarantees this provider offers + delivery_guarantees: + at_least_once: true + idempotency_key: notification_uuid + max_delivery_latency: PT30S # for critical urgency + retry_policy: + max_attempts: 7 + backoff: exponential + initial_interval: PT5S + max_interval: PT1H + on_exhaustion: dead_letter + + # Health check endpoint (on the provider) + health_endpoint: https://notif-provider.corp.example.com/health + + # Callback endpoint (DCM calls this to submit notifications) + delivery_endpoint: https://notif-provider.corp.example.com/deliver +``` + +### 3.3 Multiple notification services + +Organizations may register multiple notification services — one for Slack, one for PagerDuty, one for ServiceNow tickets. Notification subscriptions declare which provider to use for delivery. The Notification Router in DCM routes each notification to the correct provider based on the subscription's `service_provider_uuid`. + +--- + +## 4. The Event Taxonomy + +The notification event taxonomy is a **closed vocabulary** — a finite, versioned set of event types that DCM can generate notifications for. Events are grouped by category. Subscriptions reference these event types by name. + +### 4.1 Request Lifecycle Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `request.acknowledged` | Request received, Intent State created | Owner | +| `request.requires_approval` | Policy requires human review before dispatch | Owner, Approvers | +| `request.approved` | Intent State PR merged; proceeding to assembly | Owner | +| `request.dispatched` | Requested State committed; dispatched to provider | Owner | +| `request.completed` | Provider confirmed realization; Realized State written | Owner | +| `request.failed` | Request failed at any stage | Owner | +| `request.cancelled` | Consumer cancelled; request terminated | Owner | +| `request.gating_rejected` | A compliance Validation Policy rejected the request | Owner, Policy Owner | + +### 4.2 Resource Lifecycle Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `entity.realized` | Entity first realized by provider | Owner, Stakeholders (depth 1) | +| `entity.state_changed` | Entity lifecycle state transition | Owner, Stakeholders (required) | +| `entity.ttl_warning` | TTL expires within declared warning window | Owner | +| `entity.ttl_expired` | TTL reached; expiry action triggered | Owner, Stakeholders (required) | +| `entity.suspended` | Entity entered SUSPENDED state | Owner, Stakeholders (required) | +| `entity.resumed` | Entity exited SUSPENDED state | Owner, Stakeholders (required) | +| `entity.decommissioning` | Decommission initiated | Owner, Stakeholders (all) | +| `entity.decommissioned` | Entity fully decommissioned | Owner, Stakeholders (all) | +| `entity.decommission_deferred` | Decommission blocked by active stakes | Owner, Stakeholders (required) | +| `entity.ownership_transferred` | Ownership moved to a different Tenant | Previous Owner, New Owner | +| `entity.pending_review` | Entity entered PENDING_REVIEW state | Owner, Platform Admin | + +### 4.3 Drift and Discovery Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `drift.detected` | Discovered State differs from Realized State | Owner | +| `drift.severity_escalated` | Drift severity increased | Owner, Platform Admin | +| `drift.resolved` | Drift resolved (REVERT or UPDATE_DEFINITION) | Owner | +| `drift.escalated` | Drift escalated to human review | Owner, Platform Admin, SRE | +| `unsanctioned_change.detected` | Change detected with no corresponding Requested State record | Owner, Security Team, Platform Admin | + +### 4.4 Provider Update Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `provider_update.submitted` | Provider submitted an update notification | Owner | +| `provider_update.requires_approval` | Provider update requires consumer approval | Owner (approval required) | +| `provider_update.approved` | Provider update approved; Realized State updated | Owner | +| `provider_update.rejected` | Provider update rejected; becomes drift | Owner, Provider Team | +| `provider_update.auto_approved` | Provider update auto-approved by pre-authorization policy | Owner (informational) | + +### 4.5 Dependency and Relationship Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `dependency.state_changed` | A required dependency's state changed | Owner of dependent entity | +| `stakeholder.resource_decommissioning` | A shared resource the actor stakes is decommissioning | All stakeholders | +| `allocation.pool_capacity_low` | Allocation pool capacity below threshold | Pool Owner, Platform Admin | +| `allocation.released` | Allocation decommissioned; capacity returned to pool | Pool Owner | +| `cross_tenant_auth.expiring` | Cross-tenant authorization expiring | Both Tenant Admins | +| `cross_tenant_auth.revoked` | Cross-tenant authorization revoked while allocation active | Both Tenant Admins, Affected Resource Owners | + +### 4.6 Governance Events + +| Event Type | Trigger | Default Audience | +|-----------|---------|-----------------| +| `policy.activated` | Policy moved to active status | Platform Admin, Policy Owner | +| `policy.deactivated` | Policy deactivated | Platform Admin, Policy Owner | +| `external_policy_evaluation.trust_elevated` | External Policy Evaluator mode level elevated | Platform Admin, Security Team | +| `profile.changed` | Active deployment profile changed | Platform Admin, All Tenant Admins | +| `catalog_item.deprecated` | Catalog item deprecated | All consumers with active resources of that type | + +### 4.7 Security and System Events (Mandatory — Non-Suppressable) + +| Event Type | Trigger | Audience | +|-----------|---------|---------| +| `audit.integrity_alert` | Merkle verification failure detected | Security Team, Platform Admin | +| `sovereignty.violation` | Resource in violation of sovereignty constraints | Platform Admin, Security Team, Resource Owner | +| `sovereignty.migration_required` | Provider sovereignty change requires entity migration | Platform Admin, Resource Owner | +| `federation.tunnel_degraded` | DCM-to-DCM federation tunnel health degraded | Platform Admin, SRE | +| `auth.provider_failover` | Auth Provider failed over to secondary | Platform Admin | +| `rehydration.blocked` | Concurrent rehydration attempt rejected | Requesting Actor, Platform Admin | +| `security.unsanctioned_provider_write` | Attempted write to Realized Store without Requested State ref | Security Team, Platform Admin | + +--- + +## 5. Audience Resolution — The Relationship Graph Model + +### 5.1 The Fundamental Rule + +**The audience for a notification is every entity with a stake in the changed resource, resolved by traversing the relationship graph from the changed entity.** + +The notification system does not maintain a separate subscriber list per entity. It derives the audience at event time by traversing the relationship graph. This means the audience is always current — adding a new VM attachment to a VLAN automatically includes that VM's owner in future VLAN notifications, without any subscription update required. + +### 5.2 Audience Resolution Algorithm + +``` +Event fires on entity E (e.g., VLAN-100 decommissioning) + │ + ▼ Step 1: Resolve direct owner + │ entity.owned_by_tenant_uuid → Tenant Admin and resource owner actors + │ Audience role: owner + │ + ▼ Step 2: Traverse relationship graph + │ For each relationship on entity E: + │ Check: is this relationship type notification-relevant for this event type? + │ Check: does the relationship's stake_strength meet the minimum for this event? + │ If yes: resolve the related entity's owner → add to audience + │ Audience role: stakeholder + │ + ▼ Step 3: Check for approval requirements + │ Does this event require approval from a specific actor? + │ If yes: add approver to audience + │ Audience role: approver + │ + ▼ Step 4: Apply mandatory system audiences + │ Security events: always include Security Team and Platform Admin + │ Governance events: always include Policy Owner and Platform Admin + │ (These cannot be filtered out by subscription preferences) + │ + ▼ Step 5: Apply actor subscription overrides + │ Actors with explicit subscriptions to this event type → include/exclude per subscription + │ (Subscriptions can add additional audience; they cannot remove mandatory audiences) + │ + ▼ Step 6: Deduplicate and resolve contact details + │ Same actor via multiple paths → one notification with all audience_roles listed + │ Resolve each actor to their configured notification channels + │ + ▼ Step 7: Route to notification service(s) + One notification per actor per configured channel + Notification envelope includes audience_role +``` + +### 5.3 Relationship Notification Relevance + +The Resource Type Specification declares which relationship types are notification-relevant and for which events: + +```yaml +resource_type_spec: + fqn: Network.VLAN + notification_rules: + - event_type: entity.decommissioning + notify_relationships: + - relation: attached_to # source direction (VMs attached to this VLAN) + min_stake_strength: required # only required stakes get notified + traversal_depth: 1 # direct relationships only + audience_role: stakeholder + - relation: attached_to + min_stake_strength: optional # optional stakes get informational notice + traversal_depth: 1 + audience_role: observer + + - event_type: entity.state_changed + notify_relationships: + - relation: attached_to + min_stake_strength: required + traversal_depth: 1 + audience_role: stakeholder +``` + +**Traversal depth:** `1` means direct relationships only. `2` means relationships of related entities. In most cases `1` is correct — deeper traversal is reserved for critical security events that affect the entire graph. + +### 5.4 Cross-Tenant Notification + +Notification traversal follows relationship graphs across Tenant boundaries. If a VM in AppTeam Tenant has a `required` stake in a VLAN owned by NetworkOps Tenant, and the VLAN is decommissioned, AppTeam receives a stakeholder notification — even though the VLAN belongs to a different Tenant. + +Cross-tenant notifications are governed by the same sovereignty rules as cross-tenant data access: +- Notification content is limited to what the receiving Tenant is authorized to know +- The notification identifies the changed resource but does not expose the owning Tenant's configuration details +- Sovereignty checks apply to notification delivery (a notification about an EU-sovereign resource cannot be delivered to a US-based endpoint) + +```yaml +notification_sovereignty_check: + # Before delivering cross-tenant notification: + check: + - receiver_tenant_sovereignty_compatible: true + - notification_content_authorized_for_receiver: true + - delivery_endpoint_jurisdiction_compatible: true + on_failure: redact_and_deliver # or: suppress_with_audit | block_with_alert +``` + +--- + +## 6. Notification Subscriptions + +### 6.1 Three Subscription Tiers + +**Tier 1 — Mandatory System Notifications (non-suppressable):** +Security events, sovereignty violations, audit chain breaks. Always delivered to the declared system audiences (Security Team, Platform Admin) regardless of any subscription configuration. No actor or policy can suppress these. + +**Tier 2 — Tenant Default Notifications:** +Configured by Tenant admins for all resources in their Tenant. Establishes the baseline notification behavior — which events trigger notifications, which channels to use, and which urgency mapping to apply. + +```yaml +tenant_notification_defaults: + tenant_uuid: + service_provider_uuid: + + default_channel_config: + channel_type: slack + workspace: "corp" + urgency_routing: + critical: "#platform-incidents" + high: "#platform-alerts" + medium: "#platform-notifications" + low: "#platform-digest" # batched hourly + + # Which event categories are enabled by default for all resources in this Tenant + enabled_event_categories: + request_lifecycle: [request.completed, request.failed, request.gating_rejected] + resource_lifecycle: [entity.state_changed, entity.ttl_warning, entity.decommissioning] + drift: [drift.detected, unsanctioned_change.detected] + provider_update: [provider_update.requires_approval, provider_update.rejected] + dependency: [stakeholder.resource_decommissioning, cross_tenant_auth.revoked] + + # Urgency defaults per event type + urgency_overrides: + unsanctioned_change.detected: critical + drift.detected: high + entity.ttl_warning: medium + request.completed: low +``` + +**Tier 3 — Actor-Level Subscriptions:** +Individual actors subscribe to specific events on specific resources or resource types. Most useful for service accounts (CI/CD pipelines, monitoring tools) that need targeted event feeds. + +```yaml +actor_notification_subscription: + subscription_uuid: + actor_uuid: + service_provider_uuid: + + channel_config: + channel_type: pagerduty + service_id: "payments-api-on-call" + escalation_policy_id: "payments-prod" + + subscriptions: + # Subscribe to all drift events on VMs in AppTeam Tenant + - scope: + tenant_uuid: + resource_type: Compute.VirtualMachine + events: [drift.detected, unsanctioned_change.detected] + urgency_override: high + + # Subscribe to decommission of a specific VLAN I depend on + - scope: + entity_uuid: + events: [entity.decommissioning, entity.decommissioned] + urgency_override: critical +``` + +### 6.2 Subscription Composition Rules + +When multiple subscription tiers match for the same actor and event: +- Mandatory system notifications always fire (cannot be suppressed) +- Tenant defaults fire unless the actor's subscription explicitly opts out for that event type +- Actor subscriptions can add additional channels or override urgency — they do not suppress Tenant defaults unless the subscription explicitly declares `suppress_tenant_default: true` +- Deduplication: if the same notification would be delivered to the same actor via two channels from two subscription matches, deliver once per channel (not once per subscription match) + +--- + +## 7. Notification Payload — The Unified Envelope + +Every notification delivered to a notification service uses this unified envelope. The notification service translates it to the delivery channel's native format. + +```yaml +notification: + # Identity + notification_uuid: # idempotency key + correlation_id: # links to the audit record for the triggering event + generated_at: + + # The event + event_type: entity.decommissioning # from the closed taxonomy + event_uuid: # the triggering event's UUID + urgency: + + # The subject entity + entity: + uuid: + handle: + resource_type: Network.VLAN + display_name: "VLAN-100 (EU-WEST Production)" + tenant_uuid: + tenant_display_name: "NetworkOps" + + # Audience context + audience: + actor_uuid: + actor_display_name: "Jane Smith" + audience_role: + # stakeholder: explains WHY this actor is in the audience + stakeholder_reason: + via_entity_uuid: # "because your VM-A is attached to this VLAN" + via_relation: attached_to + via_entity_display_name: "VM-A (payments-api-server-01)" + + # What changed + context: + previous_state: OPERATIONAL + new_state: DECOMMISSIONING + change_summary: "VLAN-100 decommission initiated by NetworkOps team" + changed_fields: [] + changed_by: + actor_uuid: + actor_display_name: "Bob Jones (NetworkOps)" + effective_at: + + # Action required (if any) + requires_action: false + action: + type: null # approve | acknowledge | migrate | release_stake + description: null + action_url: null + deadline: null + + # Deep links + links: + entity_url: "https://dcm.corp.example.com/resources/" + event_url: "https://dcm.corp.example.com/audit/" + related_entities: + - uuid: + display_name: "VM-A (payments-api-server-01)" + url: "https://dcm.corp.example.com/resources/" +``` + +### 7.1 Urgency Mapping + +| Urgency | Meaning | Typical delivery target | +|---------|---------|------------------------| +| `critical` | Immediate action required; outage or security risk imminent | On-call pager, incident channel | +| `high` | Action required; significant impact if not addressed | Alert channel, SRE queue | +| `medium` | Action recommended; non-urgent but should not be ignored | Notification channel, daily digest | +| `low` | Informational; no action required | Digest, async channel | + +Default urgency per event type is declared in the event taxonomy. Tenant defaults and actor subscriptions may override upward or downward. + +--- + +## 8. The Delivery Pipeline + +``` +Event fires (e.g., VLAN-100 enters DECOMMISSIONING state) + │ + ▼ Stage 1: Audit record written (Stage 1 Commit Log — synchronous) + │ ENTITY_STATE_CHANGED audit record committed + │ event_uuid assigned + │ + ▼ Stage 2: Notification Router evaluates + │ Load entity relationship graph for VLAN-100 + │ Run audience resolution algorithm (Section 5.2) + │ Result: [AppTeam, DevTeam, OpsTeam] as stakeholders; [NetworkOps] as owner + │ + ▼ Stage 3: Subscription resolution + │ For each audience member: + │ Resolve Tier 1 mandatory notifications + │ Apply Tier 2 Tenant defaults + │ Apply Tier 3 actor subscriptions + │ Determine: which notification service(s); which channel config; urgency + │ + ▼ Stage 4: Notification envelope generation + │ One envelope per audience member per delivery + │ Audience role set correctly (owner / stakeholder / approver / observer) + │ Stakeholder reason populated for non-owners + │ + ▼ Stage 5: Route to notification service(s) + │ POST to provider delivery endpoint with notification envelope + │ Provider translates to delivery channel (Slack, PagerDuty, email, etc.) + │ Provider returns delivery_uuid and status + │ + ▼ Stage 6: Delivery confirmation + │ Provider reports: delivered | failed | queued + │ Delivery record written to Notification Delivery Store + │ NOTIFICATION_DISPATCHED audit record written (async) + │ + ▼ Stage 7: Failure handling + On provider delivery failure: + Retry per provider's declared retry policy + On exhaustion: dead_letter to platform admin + On critical urgency exhaustion: escalate immediately + NOTIFICATION_DELIVERY_FAILED audit record written +``` + +### 8.1 Notification Delivery Store + +A lightweight store (not the Audit Store) tracking delivery status per notification: + +```yaml +notification_delivery_record: + delivery_uuid: + notification_uuid: + actor_uuid: + service_provider_uuid: + channel_type: slack + status: + dispatched_at: + delivered_at: + failure_reason: + retry_count: 2 +``` + +--- + +## 9. Provider Update Notification Integration + +Provider Update Notifications ([provider-contract.md](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md), §7a) integrate with the notification model at two points: + +**When provider submits update notification:** +- `provider_update.submitted` fires → Owner notified (informational) + +**When provider update requires consumer approval:** +- `provider_update.requires_approval` fires → Owner notified (action required) +- `action.type: approve` +- `action.action_url` points to `/api/v1/resources/{uuid}/provider-notifications/{uuid}:approve` +- `action.deadline` set per policy (default PT24H — if no response, escalate) + +**On resolution:** +- Approved → `provider_update.approved` fires → Owner notified; Stakeholders notified of state change via `entity.state_changed` +- Rejected → `provider_update.rejected` fires → Owner notified; becomes drift event → `drift.detected` fires + +--- + +## 10. Relationship to Webhooks and Message Bus + +### 10.1 Webhooks as a Notification Channel + +Outbound webhooks ([webhooks-messaging.md](webhooks-messaging.md)) are now **one delivery channel type within the notification service model** rather than a parallel mechanism. A notification service with `channel_type: webhook` delivers notifications to configured HTTP endpoints using the unified notification envelope. + +The webhook registration model ([webhooks-messaging.md](webhooks-messaging.md), §3.2) is implemented as actor-level subscriptions (Section 6.1, Tier 3) with a webhook-type notification service. + +### 10.2 Message Bus as Notification Infrastructure + +The event routing service ([webhooks-messaging.md](webhooks-messaging.md), §5) is the **internal transport** for the notification pipeline. The Notification Router publishes notification events to the Message Bus. notification services subscribe to their assigned topics. This decouples event generation from delivery and enables high-throughput notification processing. + +``` +DCM Event → Notification Router → Message Bus → notification service subscription +``` + +The Message Bus is infrastructure — not a notification channel. Consumers do not subscribe to the Message Bus directly for notifications; they use the subscription model (Section 6.1). + +--- + +## 11. System Policies + +| Policy | Rule | +|--------|------| +| `NOT-001` | The audience for every notification is derived from the entity relationship graph at event time. DCM does not maintain static subscriber lists per entity. | +| `NOT-002` | Mandatory system notifications (Tier 1: security, sovereignty, audit chain) are never suppressable by any subscription configuration or policy. | +| `NOT-003` | Cross-tenant notifications carry only information the receiving Tenant is authorized to see. Sovereignty checks apply to notification delivery endpoints. | +| `NOT-004` | Every notification dispatch is an audit record. Delivery failures are tracked. Critical urgency delivery exhaustion triggers immediate escalation to Platform Admin. | +| `NOT-005` | Provider Update Notifications that require consumer approval carry `action.type: approve` and `action.deadline`. If the deadline passes without resolution, the notification escalates per policy. | +| `NOT-006` | Notification traversal depth is bounded. Resource Type Specifications declare the maximum traversal depth for each event type. Default: depth 1 (direct relationships only). | +| `NOT-007` | A notification service must be registered and active before notifications can be delivered. DCM does not have a built-in delivery channel — at minimum a webhook-type notification service must be configured for external delivery. | +| `NOT-008` | The notification event taxonomy is a closed vocabulary. Custom event types are not supported. New event types require a DCM registry proposal following standard governance. | + +--- + +## 12. Related Concepts + +- **notification service** — the ninth DCM provider type; handles translation and delivery +- **Notification Router** — DCM control plane component that resolves audiences and routes to providers +- **Audience Resolution** — deriving notification recipients from the entity relationship graph +- **Notification Subscription** — actor or Tenant declaration of notification preferences +- **Notification Delivery Store** — lightweight store tracking delivery status +- **Provider Update Notification** — formal provider mechanism for reporting authorized state changes (see [provider-contract.md](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md), Section 7a) +- **Outbound Webhook** — one delivery channel type within the notification service model + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/runtime-features/scheduling.md b/architecture/runtime-features/scheduling.md new file mode 100644 index 0000000..609ad50 --- /dev/null +++ b/architecture/runtime-features/scheduling.md @@ -0,0 +1,337 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Request Scheduling +Established: 2026-05-26 +Maps to: udlm/lifecycle/scheduled-requests.md +--- + +# Request Scheduling + +> **Implements contracts defined in UDLM**: +> [udlm/lifecycle/scheduled-requests.md](https://github.com/croadfeldt/udlm/blob/main/lifecycle/scheduled-requests.md). +> UDLM defines the scheduling model (immediate / at / window / recurring), +> the SCHEDULED request state contract, the maintenance window coordination +> contract, and the deadline enforcement contract. DCM operationalizes the +> Request Scheduler component, deferred request lifecycle management, +> maintenance window scheduling logic, deadline evaluation and timeout +> enforcement, consumer API additions, new events, and profile-governed +> scheduling constraints. + +--- + +## 1. Request Scheduler component + +The Request Scheduler is a DCM control plane component (an internal function +of the Request Orchestrator per +[`../control-plane/components.md`](../control-plane/components.md), Section +5.3) responsible for: + +- Maintaining a priority queue of SCHEDULED requests ordered by `not_before` +- Polling the queue; dispatching requests when `not_before` is reached +- Checking `not_after` deadlines; cancelling expired requests +- Listening for `maintenance_window` events to trigger window-scheduled + requests +- On dispatch: handing off to Request Orchestrator (same path as immediate + requests) +- Writing SCHEDULED status updates to Intent State +- Publishing `request.scheduled` and `request.schedule_cancelled` events + +### 1.1 Implementation choice + +DCM implements the Request Scheduler as a PostgreSQL-backed cron worker per +the +[`../persistence/postgres-implementation.md`](../persistence/postgres-implementation.md) +infrastructure. A `scheduled_requests` table maintains the queue; a worker +polls every PT15S for ripe requests; HA via leader election (PostgreSQL +advisory locks or a simple lease pattern). + +```sql +CREATE TABLE scheduled_requests ( + request_uuid UUID PRIMARY KEY, + entity_uuid UUID NOT NULL, + tenant_uuid UUID NOT NULL REFERENCES tenants(tenant_uuid), + schedule_dispatch VARCHAR(16) NOT NULL, -- at | window | recurring + not_before TIMESTAMPTZ NOT NULL, + not_after TIMESTAMPTZ, + window_uuid UUID, + cron_expression TEXT, + max_occurrences INTEGER, + occurrences_so_far INTEGER DEFAULT 0, + status VARCHAR(16) NOT NULL DEFAULT 'SCHEDULED', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_scheduled_ripe ON scheduled_requests(not_before, status) + WHERE status = 'SCHEDULED'; +``` + +--- + +## 2. Deferred request lifecycle management + +A scheduled request moves through the four states with one additional +intermediate status. DCM implements: + +``` +Submit with schedule.dispatch: at + ▼ ACKNOWLEDGED (Intent State created; entity_uuid assigned) + ▼ Policy evaluation at declaration time + │ Compliance validation policies run immediately (fail-fast) + │ If rejected: request fails before entering queue + │ If approved: request enters scheduled queue + ▼ SCHEDULED (new status within Intent State) + │ Stored in Request Scheduler queue + │ Visible via GET /api/v1/requests/{uuid}/status + │ Cancellable: DELETE /api/v1/requests/{uuid} + ▼ [at not_before] → Policy re-evaluation at dispatch + │ Transformation policies re-run (data may have changed) + │ Compliance Validation Policy re-evaluation with current data + │ If still approved: proceed to LAYERS_ASSEMBLED → dispatch + │ If rejected at dispatch: FAILED with failure_reason: schedule_policy_rejection + ▼ DISPATCHED → REALIZED (normal pipeline) +``` + +### 2.1 Dual policy evaluation (SCH-001) + +Policies evaluate twice intentionally: + +- **At declaration time** — catches obvious rejections early (fail fast) +- **At dispatch time** — catches changes since declaration (quota exhausted, + new compliance policy activated, actor role changed, etc.) + +Dispatch-time evaluation uses the **current** policy set, not the one in +effect at declaration. SCH-003: requests that fail dispatch-time policy +re-evaluation enter FAILED state with `failure_reason: schedule_policy_rejection`. +The consumer receives a `request.failed` event with the rejection detail. + +--- + +## 3. Maintenance window scheduling logic + +Maintenance Windows are reusable schedule artifacts referenced by scheduled +requests. DCM implements: + +```yaml +maintenance_window: + window_uuid: + window_handle: "weekly-sunday-0200-utc" + description: "Weekly maintenance window — low traffic period" + + cron: "0 2 * * 0" # every Sunday at 02:00 UTC + duration: PT2H # window is 2 hours long + + tenant_uuid: # null = platform-wide + resource_types: [] # empty = all resource types + + status: active | suspended + approved_by: + effective_from: + + created_at: + created_by: +``` + +### 3.1 Window-scheduled request flow + +``` +Consumer submits request with schedule.dispatch: window, window_id: + ▼ Request validates window exists, is active, is in scope (tenant + resource type) + ▼ Request enters SCHEDULED with not_before = next window start + ▼ Window scheduler periodically computes upcoming window starts via cron + ▼ At window start: all queued requests for the window dispatch in batch + ▼ Within the window's duration, requests dispatch normally + ▼ Outside window duration, queued requests wait for next window +``` + +### 3.2 Maintenance Window API + +``` +# Platform admin operations +POST /api/v1/admin/maintenance-windows +GET /api/v1/admin/maintenance-windows +GET /api/v1/admin/maintenance-windows/{window_uuid} +PATCH /api/v1/admin/maintenance-windows/{window_uuid} +DELETE /api/v1/admin/maintenance-windows/{window_uuid} + +# Consumer operations +GET /api/v1/maintenance-windows # list visible to consumer +GET /api/v1/maintenance-windows/{uuid} # describe a specific window +``` + +Window approval tier varies by profile: + +| Profile | Window approval tier | +|---|---| +| homelab | auto | +| dev | auto | +| standard | reviewed | +| prod | reviewed | +| fsi | verified | +| sovereign | authorized | + +--- + +## 4. Deadline evaluation and timeout enforcement + +### 4.1 not_before enforcement + +DCM validates `not_before` is a future timestamp at submission. Past +timestamps are rejected with 422 (SCH-002). + +### 4.2 not_after deadline enforcement + +If `not_after` is set and the deadline passes before dispatch: + +``` +not_after reached without dispatch + ▼ Request status → FAILED + │ failure_reason: schedule_deadline_missed + ▼ request.failed event published (urgency: medium) + ▼ Consumer notified + ▼ Intent State marked terminal — no further retries (SCH-005) +``` + +The deadline worker scans every PT15S: + +```sql +SELECT request_uuid FROM scheduled_requests +WHERE status = 'SCHEDULED' + AND not_after IS NOT NULL + AND not_after < NOW() +``` + +For each match: transition to FAILED, emit `request.failed`, remove from +queue. + +### 4.3 Recurring schedule enforcement + +```yaml +schedule: + dispatch: recurring + cron: "0 2 * * 0" + max_occurrences: 4 + not_after: "2026-12-31T00:00:00Z" +``` + +For recurring schedules, DCM: + +1. Computes next ripe time via cron +2. Dispatches; increments `occurrences_so_far` +3. If `occurrences_so_far == max_occurrences`: marks terminal +4. If `not_after` exceeded: marks terminal +5. Otherwise: re-queues with next ripe time + +--- + +## 5. Consumer API additions + +### 5.1 Submit scheduled request + +Scheduling is an optional `schedule` field on the existing request endpoint: + +``` +POST /api/v1/requests +{ + "catalog_item_uuid": "", + "fields": { ... }, + "schedule": { + "dispatch": "at", + "not_before": "2026-04-01T02:00:00Z", + "not_after": "2026-04-01T06:00:00Z" + } +} + +Response 202: +{ + "request_uuid": "", + "entity_uuid": "", + "status": "SCHEDULED", + "scheduled_dispatch_at": "2026-04-01T02:00:00Z", + "schedule_deadline": "2026-04-01T06:00:00Z" +} +``` + +### 5.2 List scheduled requests + +``` +GET /api/v1/requests?status=SCHEDULED +``` + +### 5.3 Cancel scheduled request + +Existing endpoint — no new endpoint required: + +``` +DELETE /api/v1/requests/{request_uuid} + +# Works on SCHEDULED requests; moves status to CANCELLED +# Returns 409 if request is already dispatched (past SCHEDULED) + +Response 204 No Content +``` + +--- + +## 6. New events + +| Event | Urgency | Trigger | +|---|---|---| +| `request.scheduled` | info | Request entered SCHEDULED queue | +| `request.schedule_cancelled` | low | Scheduled request cancelled before dispatch | +| `request.schedule_deadline_missed` | medium | not_after passed without dispatch | + +These add to the `request.*` domain in +[udlm/contracts/event-catalog.md](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md). + +--- + +## 7. Profile-governed scheduling constraints + +DCM enforces scheduling constraints per profile to reflect operational risk +tolerance: + +| Profile | Max scheduling horizon | Max concurrent scheduled/actor | Recurring max frequency | Window approval tier | +|---|---|---|---|---| +| `homelab` | P365D | unlimited | PT1H | auto | +| `dev` | P365D | 50 | PT1H | auto | +| `standard` | P90D | 20 | PT4H | reviewed | +| `prod` | P30D | 10 | PT12H | reviewed | +| `fsi` | P14D | 5 | PT24H | verified | +| `sovereign` | P7D | 3 | PT24H | authorized | + +- **Max scheduling horizon:** how far in the future `not_before` may be set; + beyond → reject with 422 +- **Max concurrent scheduled/actor:** how many SCHEDULED requests per actor; + exceeded → 429 +- **Recurring max frequency:** minimum interval between recurring dispatches; + cron more frequent than this → reject +- **Window approval tier:** authority tier required to create or modify a + Maintenance Window + +--- + +## 8. What operations support scheduling + +| Operation | Scheduling supported | Notes | +|---|---|---| +| Resource creation | ✅ | Full scheduling model | +| Resource update (PATCH) | ✅ | Full scheduling model | +| Suspend | ✅ | Full scheduling model | +| Resume | ✅ | Full scheduling model | +| Decommission | ✅ | Full scheduling model; `not_after` recommended | +| Rehydration | ✅ | Full scheduling model | +| TTL extension | ✅ | Full scheduling model | +| Discovery trigger | ❌ | Handled by Discovery Scheduler (see [`../convergence-engine/recovery-and-retry.md` §4](../convergence-engine/recovery-and-retry.md)) | + +--- + +## 9. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `SCH-001-DCM` | DCM evaluates compliance validation policies twice on scheduled requests: at declaration (fail-fast) and at dispatch (current state). Both must pass | +| `SCH-002-DCM` | DCM rejects scheduled requests with a past not_before (422) | +| `SCH-003-DCM` | DCM transitions requests failing dispatch-time policy re-evaluation to FAILED with failure_reason: schedule_policy_rejection | +| `SCH-004-DCM` | DCM permits cancellation of SCHEDULED requests at any time before dispatch via DELETE /api/v1/requests/{uuid} | +| `SCH-005-DCM` | DCM transitions requests with exceeded not_after to FAILED with failure_reason: schedule_deadline_missed; no retry | +| `SCH-006-DCM` | DCM treats Maintenance Windows as platform-level or tenant-scoped artifacts subject to standard DCM artifact lifecycle | diff --git a/architecture/runtime-features/webhooks-messaging.md b/architecture/runtime-features/webhooks-messaging.md new file mode 100644 index 0000000..51e2fb6 --- /dev/null +++ b/architecture/runtime-features/webhooks-messaging.md @@ -0,0 +1,825 @@ +# DCM Data Model — Webhooks, Messaging, and External Integration + + +**Document Status:** ✅ Complete +**Related Documents (updated):** [Notification Model](notifications.md) | [Entity Relationships](https://github.com/croadfeldt/udlm/blob/main/entities/entity-relationships.md) + +> **Foundation Document Reference** +> +> This document is a detailed reference for a specific domain of the DCM architecture. +> The three foundational abstractions — Data, Provider, and Policy — are defined in +> [udlm/foundations/foundations.md](https://github.com/croadfeldt/udlm/blob/main/foundations/foundations.md). All concepts in this document map to one or +> more of those three abstractions. +> See also: [Provider Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/provider-contract.md) | [Policy Contract](https://github.com/croadfeldt/udlm/blob/main/contracts/policy-contract.md) +> +> **This document maps to: PROVIDER** +> +> The Provider abstraction — Message Bus and webhook delivery channels + + +**Related Documents:** [Universal Audit Model](https://github.com/croadfeldt/udlm/blob/main/observability/universal-audit.md) | [Deployment and Redundancy](deployment-redundancy.md) | [Authentication and Authorization](https://github.com/croadfeldt/udlm/blob/main/governance/auth-providers.md) | [Policy Organization](../governance-enforcement/policy-profiles.md) + +--- + + +> **See [Event Catalog](https://github.com/croadfeldt/udlm/blob/main/contracts/event-catalog.md)** — authoritative source for all DCM event types and payload schemas. + +## 1. Purpose + +DCM communicates with the outside world through three complementary mechanisms: + +- **Outbound Webhooks** — DCM pushes event notifications to external HTTP endpoints +- **Inbound Webhooks** — External systems push requests, queries, and events to DCM HTTP endpoints +- **event routing services** — DCM integrates with external message buses for persistent, high-throughput bidirectional event streaming + +All three mechanisms are authenticated, authorized, and audited identically to any other DCM API call. There is no privileged back-channel. Every integration is a registered DCM actor subject to full Policy Engine evaluation. + +**Webhooks are optional and policy-governed.** The active Profile sets defaults — `fsi` and `sovereign` profiles may require webhook or message bus coverage for audit events. `minimal` and `dev` profiles make them fully optional. + +--- + +## 2. Ingress and Egress — The Universal Actor Model + +### 2.1 The `ingress` Block + +Every request entering DCM carries an immutable `ingress` block set by the DCM ingress layer before Policy Engine evaluation. It is never consumer-declarable and never modifiable by policies — policies may only read it. + +```yaml +ingress: + # HOW it arrived + surface: + protocol: + authenticated_via: + authorized_via: policy_engine + + # WHO sent it — fully resolved actor context + actor: + uuid: + type: + display_name: "Jane Smith" + identity_source: + auth_provider_uuid: # which Auth Provider authenticated + auth_provider_type: + + # Resolved DCM roles and scope (at authentication time) + roles: [sre] + tenant_scope: [] + groups: [] + permissions: [request.submit, query.entity_state] + + # Authorization chain + authorized_by: + method: + authorizing_entity_uuid: + authorization_timestamp: + expiry: + + # Session context (human actors) + session_uuid: + session_started_at: + mfa_verified: + + # External identity (federated actors) + external_identity: + provider: + subject: "uid=jsmith,cn=users,cn=accounts,dc=corp,dc=example,dc=com" + claims: + email: jsmith@corp.example.com + display_name: "Jane Smith" + department: Engineering + cost_center: CC-1234 + ldap_groups: [cn=dcm-sre,...] + sid: "S-1-5-21-..." # AD Security Identifier + + # Rate limit tracking + rate_limit_bucket: + + # Surface-specific detail + webhook_registration_uuid: # if surface: webhook_inbound + (optional infrastructure)_uuid: # if surface: message_bus_inbound + message_offset: # if surface: message_bus_inbound + scheduler_job_uuid: # if surface: scheduler + parent_request_uuid: # if surface: policy_engine|rehydration + source_ip: +``` + +### 2.2 The `egress` Block + +All outbound calls from DCM carry DCM's authenticated identity: + +```yaml +egress: + surface: + protocol: + actor: + uuid: + type: dcm_internal + component: + authenticated_via: + credential_ref: + service_provider_uuid: + secret_path: + originating_request_uuid: + originating_actor_uuid: +``` + +### 2.3 Policy Engine Use Cases — Ingress/Egress Fields + +The ingress block enables a rich class of governance rules: + +```yaml +# Require specific auth for sensitive operations +policy: "If action == decommission AND ingress.actor.mfa_verified == false THEN gate" + +# Block legacy API keys from production Tenants +policy: "If tenant.profile == prod AND ingress.actor.identity_source == static_api_key THEN gate" + +# Require enterprise auth for security resources +policy: "If resource_type IN [Network.FirewallRule] AND ingress.actor.auth_provider_type NOT IN [oidc, freeipa, active_directory] THEN gate" + +# Enrich from OIDC claims +policy: "If ingress.actor.external_identity.claims.department EXISTS THEN inject: business_context.department" + +# Block message bus inbound from non-service-accounts +policy: "If ingress.surface == message_bus_inbound AND ingress.actor.type != webhook_service_account THEN gate" + +# Sovereignty check on inbound message bus +policy: "If ingress.surface == message_bus_inbound AND (optional infrastructure).jurisdiction != tenant.sovereignty_zone THEN gate" +``` + + +### 2.5 Ingress API vs Consumer API — Relationship Clarification + +These two terms refer to different architectural layers: + +**Ingress API (infrastructure layer):** +The network-level entry point for all inbound requests to the DCM control plane. It handles: +- TLS termination +- Authentication token validation +- Setting the immutable `ingress` block on every request (surface, actor, timestamp, mfa_verified) +- Rate limiting at the network level +- Routing to the appropriate internal component (Consumer API handlers, Provider API handlers, Admin API handlers) + +The Ingress API is infrastructure — it is not directly defined in any consumer-facing specification. + +**Consumer API (application layer):** +The logical REST API surface that consumers interact with, as defined in the [Consumer API Specification](../../docs/specifications/consumer-api-spec.md). The Consumer API is *served through* the Ingress API. When a consumer calls `POST /api/v1/requests`, that request enters through the Ingress API (which sets the ingress block) and is then handled by the Consumer API component. + +**Other APIs served through the Ingress API:** +- **Provider API** — the callback and notification endpoints that Service Providers call (`/api/v1/provider/...`) +- **Admin API** — platform administration operations (`/api/v1/admin/...`) +- **Webhook Inbound** — external systems calling DCM (`/api/v1/webhooks/...`) + +**The Ingress API is one, the Consumer API is one of several logical surfaces routed through it.** + +--- + +### 2.6 Consumer Rate Limiting and Quota Model + +Consumer-side rate limiting and resource quotas are enforced by compliance validation policies — not hardcoded limits. This keeps quota enforcement consistent with DCM's policy-driven model. + +**Request rate limiting (per actor):** + +```yaml +rate_limit_policy: + type: gating + handle: "system/quotas/api-rate-limit" + trigger: request.initiated + conditions: + - field: ingress.actor_uuid + rate_window: PT1M + max_requests: 60 # configurable per Tenant policy + action: reject + rejection_code: 429 + rejection_message: "Rate limit exceeded. Retry after PT1M." +``` + +**Resource quotas (per Tenant per resource type):** + +```yaml +quota_policy: + type: gating + handle: "tenant/payments/vm-quota" + trigger: request.initiated + conditions: + - field: request.resource_type + equals: Compute.VirtualMachine + - field: tenant.active_entity_count + resource_type: Compute.VirtualMachine + operator: gte + value: 100 # max 100 concurrent VMs for this Tenant + action: reject + rejection_message: "VM quota exceeded (100). Request a quota increase via the Admin API." +``` + +**Quota increase process:** Tenants request quota increases through the standard request process. A quota change request produces a Requested State record, goes through policy evaluation, and requires Platform Admin approval for significant increases. + +**Profile-governed defaults:** + +| Profile | Default API rate limit | Default resource quota | +|---------|----------------------|----------------------| +| homelab | 10 req/min | Unlimited | +| dev | 60 req/min | Unlimited | +| standard | 60 req/min | Policy-declared | +| prod | 120 req/min | Policy-declared | +| fsi | 60 req/min | Strict policy-declared | +| sovereign | 30 req/min | Strict policy-declared | + + +### 2.4 System Policies + +| Policy | Rule | +|--------|------| +| `ING-008` | All DCM requests must carry an ingress block. The ingress block is set by the DCM ingress layer and is immutable — policies may read but not modify it. | +| `ING-009` | The ingress block must include a fully resolved actor context: uuid, type, identity_source, roles, tenant_scope, auth_provider_uuid, and authorized_by chain. | +| `ING-010` | All egress calls from DCM must carry DCM's authenticated identity. Unauthenticated egress is rejected. | +| `ING-011` | Authentication enforcement is profile-governed. Standard and above reject unauthenticated requests. Minimal and dev support lightweight authenticated modes. There is no anonymous access in any profile. | +| `ING-012` | Webhook and message bus inbound surfaces always require authentication regardless of active Profile. | +| `ING-013` | Rate limiting is enforced per registered actor. Exceeding rate limits returns 429 Too Many Requests. | + +--- + +## 3. Outbound Webhooks + +> **⚠️ Architecture Update — Notification Model Supersedes Outbound Webhooks** +> +> The outbound webhook model described in Section 3 has been one delivery channel within the Notification Model. Outbound webhooks are now one delivery channel type within the notification service model rather than a parallel mechanism. +> +> **For new implementations:** Use the notification service subscription model ([notifications.md](notifications.md), §6) with a webhook-type notification service. +> +> +> The key improvement in the new model: audience is derived from the **entity relationship graph**, not from a manually maintained subscriber list. A webhook subscription for VLAN drift events will now automatically include all VMs attached to that VLAN as audience context. + +### 3.1 Concept + +DCM pushes event notifications to registered external HTTP endpoints. Outbound webhooks are **optional and policy-governed** — the active Profile and Policy Groups determine which events require external notification. + +### 3.2 Webhook Registration + +```yaml +webhook_registration: + artifact_metadata: + uuid: + handle: "org/webhooks/payments-drift-alerts" + version: "1.0.0" + status: active + owned_by: + display_name: "Payments Platform Team" + notification_endpoint: + + name: "Payments Platform Drift Alerts" + description: "Notifies payments team of drift detection events" + + # SCOPE + scope: + type: + tenant_uuid: + cross_tenant_authorization_uuid: # if cross_tenant + + # EVENT SUBSCRIPTIONS + event_subscriptions: + - event_type: drift.detected + schema_version: "1.0" + adapter: true # DCM transforms newer schemas to 1.0 + filter: + tenant_uuid: + resource_types: [Compute.VirtualMachine, Storage.Block] + - event_type: request.realized + schema_version: "1.0" + - event_type: entity.state_transition + schema_version: "1.0" + filter: + to_states: [DEGRADED, FAILED] + + # ENDPOINT + endpoint: + url: https://alerts.payments.corp.example.com/dcm/events + sovereignty_check: true # verify endpoint jurisdiction before delivery + + # AUTHENTICATION + authentication: + mode: + secret_ref: + service_provider_uuid: + secret_path: "dcm/webhooks/payments-drift/hmac-secret" + rotation_policy: + automatic: true + interval: P90D + transition_window: P7D + notify_before: P14D + + # RELIABILITY + retry_policy: + max_attempts: 7 + backoff: exponential + initial_interval: PT5S + max_interval: PT1H + timeout_per_attempt: PT10S + on_exhaustion: dead_letter # dead_letter | discard | escalate + + # HEALTH + health: + failure_threshold: 10 + suspension_notification: true + auto_deactivate_after: P30D + status: active + + # SCHEMA COMPATIBILITY + schema_adapter: + enabled: true + # DCM maintains forward-compatibility adapters per schema version + # Consumer stays on declared schema_version indefinitely + deprecation_notice_days: 90 +``` + +### 3.3 Event Taxonomy + +> The table below is the event taxonomy for webhook subscriptions. + +The event taxonomy maps onto the Universal Audit action vocabulary. All are versioned registry entries: + +| Category | Events | +|----------|--------| +| Entity lifecycle | `entity.created`, `entity.modified`, `entity.state_transition`, `entity.deleted`, `entity.expired`, `entity.rehydrated` | +| Group | `group.member_added`, `group.member_removed`, `group.created`, `group.deleted` | +| Relationship | `relationship.created`, `relationship.released` | +| Policy | `policy.activated`, `policy.deactivated`, `policy.evaluated` (fail/gate only), `policy.shadow_result` | +| Provider | `provider.healthy`, `provider.degraded`, `provider.unhealthy`, `provider.registered`, `provider.deregistered` | +| Audit/security | `audit.integrity_break`, `audit.forward_failed` | +| Drift | `drift.detected`, `drift.resolved`, `drift.escalated` | +| Request | `request.submitted`, `request.approved`, `request.rejected`, `request.realized`, `request.failed` | +| Rehydration | `rehydration.started`, `rehydration.completed`, `rehydration.paused`, `rehydration.interrupted` | +| Authorization | `authorization.granted`, `authorization.revoked` | +| Webhook | `webhook.secret_rotated`, `webhook.suspended`, `webhook.schema_deprecated` | + +### 3.4 Payload Format + +```yaml +webhook_payload: + # Envelope + event_uuid: # idempotency key + event_type: drift.detected + event_schema_version: "1.0" + timestamp: # from Stage 1 Commit Log — authoritative + dcm_version: + + # Subject + subject: + entity_uuid: + entity_type: infrastructure_resource + entity_handle: + tenant_uuid: + + # Delta + delta: + drifted_fields: + - field: cpu_count + realized_value: 4 + discovered_value: 8 + drift_severity: significant + + # Links + links: + self: + audit_record: +``` + +### 3.5 Delivery Guarantees + +- **At-least-once** — not exactly-once; consumers must be idempotent using `event_uuid` +- **Per-entity ordering** — events for a given `entity_uuid` delivered in Commit Log sequence order +- **Cross-entity ordering** — not guaranteed; use `timestamp` for actual occurrence time +- **Sovereignty-aware** — delivery blocked if endpoint jurisdiction incompatible with Tenant sovereignty (WHK-004) + +--- + +## 4. Inbound Webhooks + +### 4.1 Concept + +DCM exposes authenticated HTTP endpoints that external systems call to submit requests, queries, and events. Inbound webhooks are subject to full Policy Engine evaluation — identical to any other API call. + +### 4.2 Inbound Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `POST /webhooks/inbound/request` | Submit a service request | +| `POST /webhooks/inbound/query` | Query entity state or catalog | +| `POST /webhooks/inbound/event` | Push an event (provider state change, CI/CD signal) | +| `POST /webhooks/inbound/ingestion` | Push brownfield ingestion data | +| `POST /webhooks/inbound/data` | Push enrichment or information data | + +### 4.3 Webhook Actor Registration + +Every inbound webhook caller must be registered as a **webhook actor** — a service account in the DCM identity model: + +```yaml +webhook_actor: + artifact_metadata: + uuid: + handle: "actors/webhook/cicd-pipeline-prod" + status: active + + name: "CI/CD Pipeline Production" + actor_type: webhook_service_account + + # Authentication + authentication: + mode: hmac_sha256 + secret_ref: + service_provider_uuid: + secret_path: "dcm/webhooks/inbound/cicd-pipeline/hmac" + + # Authorization + role: consumer + tenant_scope: [] + permitted_operations: + - request.submit + - query.entity_state + - query.catalog + + # Rate limiting + rate_limit: + requests_per_minute: 60 + burst: 10 + + # Audit identity + audit_identity: + display_name: "CI/CD Pipeline (Production)" + system: "jenkins-prod-01" +``` + +### 4.4 Response Model + +- **Queries** — synchronous response with result +- **Requests and events** — `202 Accepted` + `request_uuid`; caller polls status or registers outbound webhook for completion notification + +--- + +## 5. event routing service + +### 5.1 Concept + +A **event routing service** is the sixth DCM provider type — a persistent, high-throughput integration with an external message bus for bidirectional event streaming. Where webhooks are point-to-point HTTP calls, a event routing service is a durable pub/sub connection. + +### 5.2 Registration + +```yaml +(optional infrastructure)_registration: + artifact_metadata: + uuid: + handle: "providers/messagebus/corporate-kafka" + version: "1.0.0" + status: active + + name: "Corporate Kafka Cluster" + provider_type: message_bus + + # Direction + direction: + + # Protocol + protocol: + + # Connection + connection: + brokers: [kafka-1.corp:9093, kafka-2.corp:9093, kafka-3.corp:9093] + credentials_ref: + service_provider_uuid: + secret_path: "dcm/providers/messagebus/corporate-kafka/credentials" + tls: + mode: mtls + ca_cert_ref: + service_provider_uuid: + secret_path: "dcm/providers/messagebus/corporate-kafka/ca-cert" + + # Outbound — DCM publishes to external bus + outbound: + topic_mapping: + entity.state_transition: "dcm.entities.state" + drift.detected: "dcm.drift.alerts" + request.realized: "dcm.requests.completed" + audit.integrity_break: "dcm.security.alerts" + schema_version: "1.0" + delivery_guarantee: at_least_once + + # Inbound — DCM consumes from external bus + inbound: + consumer_group: "dcm-inbound-prod" + topic_mapping: + "cicd.deployment.completed": request.submit + "cmdb.discovery.update": ingestion.push + "itsm.change.approved": request.approve + # Inbound messages processed as authenticated API calls + actor_identity_uuid: + # Same Policy Engine evaluation as inbound webhooks + + # Sovereignty + operational_sovereignty: + jurisdiction: eu-west + certifications: [ISO-27001, GDPR-compliant] + + # Health + health_check: + interval_seconds: 30 + on_unhealthy: alert +``` + +### 5.3 Architecture + +``` +DCM internal Message Bus (internal pub/sub backbone) + │ + ├── Webhook Delivery Service ──────→ External HTTP endpoints (outbound webhooks) + │ + └── Message Bus Bridge Service ────→ External message bus (event routing service) + ←─── External message bus (inbound) +``` + +The internal Message Bus is never exposed directly. All external event integration goes through either the Webhook Delivery Service or the Message Bus Bridge Service — both of which handle authentication, authorization, sovereignty checks, and schema transformation. + +--- + +## 6. Git PR Ingress — Distributed Git Request Mechanism + +### 6.1 Concept + +DCM supports **git_pr_merge** as a twelfth ingress surface — enabling teams to submit DCM resource definitions as Pull Requests to a DCM-watched Git repository. This is the native workflow for infrastructure-as-code teams: open a PR, get it reviewed by humans and DCM's policy engine simultaneously, merge to execute. + +**Why Git PR ingress matters:** +- GitOps teams work in Git — their deployment workflow is already PR-based +- Security and compliance teams review infrastructure changes the same way they review code +- The PR itself is the human review record; DCM's audit trail captures the automated processing +- Rollback is a Git revert — natural and familiar +- Multi-team approval workflows use existing Git branch protection rules +- The PR diff shows exactly what changes — field-level visibility + +### 6.2 The Git Request Watcher + +A dedicated control plane component — the **Git Request Watcher** — monitors designated repositories via webhooks (preferred) or polling. It is policy-governed: which repositories it watches, which branches trigger processing, and which resource types may be submitted via Git PR. + +### 6.3 Request Repository Structure + +``` +dcm-requests/ ← DCM-watched request repository + {tenant-uuid}/ + pending/ + {resource-handle}/ + request.yaml ← Standard DCM resource definition + realized/ + {resource-handle}/ + realized.yaml ← DCM writes realized state here on success + failed/ + {resource-handle}/ + request.yaml ← Moved here on failure with error detail +``` + +### 6.4 Git Actor Identity Resolution + +**Authentication is always required.** Git PR ingress actors must be resolved to DCM actors through the registered Auth Provider — DCM trusts the Git server's authentication assertion, not user-declared Git configuration. Anyone can set their local `git config user.email` to anything; DCM ignores self-declared identity. + +**The trust chain:** +``` +Git server authenticates user (SSH key, OAuth token, password) + │ Git server's authentication is trusted — not user's claimed identity + ▼ +DCM Git Request Watcher receives PR merge webhook + │ Webhook payload contains: actor.login, actor.auth_method, actor.external_id + │ All verified by the Git server + ▼ +Auth Provider resolution (same path as web UI login for the same user): + ├── OIDC/OAuth: Git server OAuth subject → OIDC Auth Provider userinfo lookup + ├── LDAP/AD: Git server username → LDAP lookup → DCM actor + ├── SSH key: Git server key fingerprint → DCM SSH key registry → DCM actor + └── Service account: Git service account → registered webhook actor + ▼ +Fully resolved DCM actor — same roles, groups, tenant scope as any other user +``` + +**Resolution methods:** + +```yaml +git_actor_resolution: + # Method 1: OIDC/OAuth (recommended — Git server uses same IdP as DCM) + method: oidc_subject_lookup + auth_provider_uuid: + + # Method 2: LDAP/AD (enterprise — Git server authenticates via corporate directory) + method: ldap_username_lookup + auth_provider_uuid: + + # Method 3: SSH key fingerprint + method: ssh_key_fingerprint + # Keys registered in DCM SSH key registry, linked to actor UUIDs + + # Method 4: Service account (automated workflows) + method: webhook_service_account + # Git service account mapped to registered webhook actor +``` + +**Identity resolution failure — explicit rejection:** + +When DCM cannot map the merge actor to a DCM actor, the PR is rejected with an actionable comment. Never silently ignored. + +``` +❌ DCM Identity Resolution Failed + +DCM could not map the merge actor "jsmith" to a DCM actor identity. + +Possible causes: + • Your Git account is not linked to a DCM actor via the corporate Auth Provider + • Your DCM actor account has been suspended or deactivated + +To resolve: + • Contact your platform administrator: https://dcm.corp.example.com/actors/git-identity-setup + +This PR will not be processed until identity resolution succeeds. +``` + +### 6.5 The ingress Block for Git PR + +```yaml +ingress: + surface: git_pr_merge # or: git_pr_open (for shadow validation) + protocol: https + authenticated_via: oidc # or: ldap_direct_bind, active_directory, ssh_key + actor: + uuid: + type: human + display_name: "Jane Smith" + identity_source: oidc + auth_provider_uuid: + roles: [consumer] + tenant_scope: [] + groups: [] + # Groups and tenant scope: SAME mappings as web UI login for this user + external_identity: + provider: github # or: gitlab, gitea, freeipa, active_directory + subject: # verified by Git server + git_username: jsmith + git_verified_email: jsmith@corp.com # from Git server record — not git config + mfa_verified: true # from Auth Provider session record + git_context: + repository: https://git.corp.example.com/dcm-requests/payments + pr_number: 142 + pr_url: https://git.corp.example.com/payments/pulls/142 + merge_commit: + base_branch: main + pr_author: jsmith + pr_reviewers: [platform-team, security-team] + pr_approved_by: [, ] + # Approved by: DCM resolves Git reviewer identities via same Auth Provider +``` + +### 6.6 PR Lifecycle — DCM Processing Flow + +``` +1. Developer creates resource definition YAML (standard DCM request format) + │ +2. Opens PR against dcm-requests/{tenant-uuid}/pending/ + │ +3. DCM Git Watcher detects PR (git_pr_open event) + │ +4. DCM resolves PR author → DCM actor via Auth Provider + │ Failure → post rejection comment; stop processing + │ +5. DCM validates actor tenant scope against target Tenant + │ Failure → post rejection comment; stop processing + │ +6. Shadow policy evaluation (same nine-step assembly — dry run) + │ Results posted as PR review comments: + │ "✅ Schema valid" + │ "✅ All policies pass" + │ "⚠️ Will be placed in eu-west-1 per sovereignty policy" + │ "❌ Validation Policy (compliance): VM size exceeds quota — reduce cpu_count" + │ +7. Human review and approval (standard Git PR workflow) + │ Branch protection enforces required reviewers + │ Policy may declare: require DCM-defined approvers in git_context.pr_approved_by + │ +8. PR merged to main (git_pr_merge event) + │ ingress_surface: git_pr_merge + │ Actor re-verified at merge time (not assumed from PR open time) + │ +9. DCM processes as standard nine-step assembly (real — not shadow) + │ +10. DCM posts realization result as PR comment + status check + │ "✅ VM payments-prod-01 realized — UUID: " + │ "❌ Realization failed — audit record: " + │ +11. DCM commits realized state to realized/ directory (optional) + Git history = full lifecycle record +``` + +### 6.7 Policy Engine Use Cases — Git PR Ingress + +```yaml +# Require PR approval before processing production resources +policy: + type: gating + rule: > + If ingress.surface == git_pr_merge + AND tenant.profile IN [prod, fsi, sovereign] + AND ingress.git_context.pr_approved_by NOT CONTAINS required_approvers + THEN gate: "PR requires approval from platform admin and security owner" + +# Require MFA for Git PR merges in production Tenants +policy: + type: gating + rule: > + If ingress.surface == git_pr_merge + AND tenant.profile IN [prod, fsi, sovereign] + AND ingress.actor.mfa_verified == false + THEN gate: "MFA required for Git PR merges in production Tenants" + +# Restrict resource types submittable via Git PR +policy: + type: gating + rule: > + If ingress.surface == git_pr_merge + AND resource_type NOT IN [Compute.VirtualMachine, Storage.Block] + THEN gate: "Only compute and storage resources may be submitted via Git PR" + +# Require actor to be in authorized Git team for target Tenant +policy: + type: gating + rule: > + If ingress.surface == git_pr_merge + AND ingress.actor.groups NOT CONTAINS tenant.authorized_git_groups + THEN gate: "Git PR author is not in an authorized group for this Tenant" + +# Post shadow results as PR comments (transformation) +policy: + type: transformation + placement_phase: pre + rule: > + If ingress.surface IN [git_pr_merge, git_pr_open] + THEN inject: git_feedback.post_as_pr_comment = true +``` + +### 6.8 Updated Ingress Surface Taxonomy + +```yaml +ingress_surface_taxonomy: + - web_ui # DCM's own web interface + - consumer_api # Direct REST API call + - webhook_inbound # Inbound webhook call + - message_bus_inbound # Inbound message bus + - provider_callback # Provider reporting back + - policy_engine # Policy-generated sub-request + - scheduler # Scheduled/timed trigger + - rehydration # Rehydration-generated + - ingestion # Ingestion pipeline + - dcm_internal # DCM system-generated + - operator_cli # Command line interface + - git_pr_merge # Git PR merge → execute ← new + - git_pr_open # Git PR open → shadow validation only ← new +``` + +### 6.9 System Policies — Git PR Ingress + +| Policy | Rule | +|--------|------| +| `GIT-001` | DCM supports git_pr_merge and git_pr_open as ingress surfaces. Git PR ingress is subject to full Policy Engine evaluation identical to API ingress. | +| `GIT-002` | DCM trusts the Git server's authentication assertion — not user-declared Git configuration. DCM resolves the Git server's verified identity through the registered Auth Provider to produce a fully-resolved DCM actor with the same role, group, and tenant scope mappings as any other user authenticated via the same Auth Provider. | +| `GIT-003` | Unresolvable Git actor identities are rejected with an actionable PR comment. PRs are never silently ignored. | +| `GIT-004` | The resolved Git PR actor must have the target Tenant UUID in their tenant_scope. PRs targeting Tenants outside the actor's scope are rejected — same enforcement as API tenant scope checks. | +| `GIT-005` | DCM posts shadow policy evaluation results as PR review comments on git_pr_open. Compliance Validation Policy failures should be surfaced via repository branch protection integration. | +| `GIT-006` | PR approval status may be declared as an authorization requirement by policy. DCM checks declared reviewer approvals against the PR's actual approval record before processing a merged PR. | +| `GIT-007` | The Git Request Watcher component is policy-governed: which repositories it monitors, which branches trigger processing, and which resource types may be submitted via Git PR are declared via Policy Group. | +| `GIT-008` | Actor identity is re-verified at merge time — not assumed from PR open time. A user whose DCM actor is suspended between PR open and merge will be rejected at merge. | + +--- + + + +| Policy | Rule | +|--------|------| +| `WHK-001` | Events for a given entity_uuid are delivered in Commit Log sequence order. Cross-entity ordering is not guaranteed. | +| `WHK-002` | Webhook delivery uses at-least-once semantics. Consumers must be idempotent using event_uuid as the deduplication key. | +| `WHK-003` | Outbound webhook authentication must be declared at registration. Supported: hmac_sha256 (default), mtls, bearer_token. Unauthenticated webhooks are rejected. | +| `WHK-004` | Webhook endpoints that would deliver data outside a Tenant's sovereignty boundary are subject to sovereignty checks. | +| `WHK-005` | Webhook governance is policy-driven. Profiles set defaults — fsi/sovereign profiles may require webhook coverage for audit events via Policy Group. | +| `WHK-006` | Webhook registrations must declare the event schema version expected. DCM supports current and N-1 schema versions simultaneously. | +| `WHK-007` | Platform-scoped webhooks require Platform Admin role and are audit-logged as CONFIG_CHANGE. | +| `WHK-008` | Cross-tenant webhooks require a valid cross_tenant_authorization record (XTA-001). | +| `WHK-009` | Inbound webhook callers must be registered as webhook actors with explicit role, tenant scope, and permitted operations. Unregistered callers are rejected with 401. | +| `WHK-010` | Inbound webhook calls are subject to full Policy Engine evaluation — identical to any other API call. No bypass. | +| `WHK-011` | All inbound webhook calls are recorded in the audit trail with the webhook actor as the immediate actor. | +| `WHK-012` | Inbound webhook endpoints return 202 Accepted + request_uuid for async operations. | +| `WHK-013` | Rate limiting is enforced per registered webhook actor. Exceeding rate limits returns 429 Too Many Requests. | +| `WHK-014` | All credential references in webhook and message bus configurations must resolve through a registered credential management service. | + +--- + +*Document maintained by the DCM Project. For questions or contributions see [GitHub](https://github.com/dcm-project).* diff --git a/architecture/topology/canonical-9-layer-hierarchy.md b/architecture/topology/canonical-9-layer-hierarchy.md new file mode 100644 index 0000000..893643e --- /dev/null +++ b/architecture/topology/canonical-9-layer-hierarchy.md @@ -0,0 +1,346 @@ +--- +Document Status: ✅ Stable — DCM canonical default +Document Type: Architecture Reference — Topology Hierarchy +Established: 2026-05-26 +Maps to: udlm/topology/location-topology-layers.md +--- + +# Canonical 9-Layer Location Hierarchy + +> **Implements contracts defined in UDLM**: +> [udlm/topology/location-topology-layers.md](https://github.com/croadfeldt/udlm/blob/main/topology/location-topology-layers.md) +> realizes the layered-topology contract in UDLM with DCM's canonical 9-layer +> scheme. UDLM defines the abstract layered-topology contract (layers have +> parent/child relationships, carry typed fields, follow lifecycle states). +> DCM picks the specific Country → Region → Zone → Site → Data Center → +> Hall → Cage → Rack → Unit hierarchy as its canonical default. A peer +> DCM realization could pick a different hierarchy and remain +> UDLM-conformant. + +--- + +## 1. The DCM canonical hierarchy + +DCM ships with nine standard location types. The hierarchy is ordered from +broadest to most specific. Each type has a standard name, a short code for +handles and references, and a set of standard data fields. + +``` +Level 1 — Country (CTY) +Level 2 — Region (RGN) +Level 3 — Zone / Availability Zone (AZ) +Level 4 — Campus / Site (SITE) +Level 5 — Data Center / Facility (DC) +Level 6 — Hall / Pod / Row (HALL) [optional] +Level 7 — Cage / Enclosure (CAGE) [optional] +Level 8 — Rack (RACK) +Level 9 — Unit / Slot (UNIT) [optional — typically provider-managed] +``` + +Levels marked `[optional]` may be omitted if not relevant to an organization's +estate. The hierarchy is still valid when levels are skipped — a Rack can be +a direct child of a Data Center if Halls and Cages are not used. + +**Custom types** may be inserted at any level via decimal-numbered insertion +(e.g., `level: 3.5` between Zone and Site). Custom types follow the same +format as standard types and are registered in the Location Type Registry. +A Navy deployment, for example, might insert Fleet (3.5) and Ship (4.5) +between Zone and Site, and Site and Data Center respectively. + +--- + +## 2. Standard type definitions + +Each standard type has a defined schema of fields. These fields become the +data carried by location layer instances of that type. + +### 2.1 Country (CTY) + +```yaml +location_type: country +code: CTY +level: 1 + +standard_fields: + country_name: { type: string, required: true, example: "Germany" } + iso_3166_1_alpha2: { type: string, pattern: '^[A-Z]{2}$', required: true, example: "DE" } + iso_3166_1_alpha3: { type: string, pattern: '^[A-Z]{3}$', required: true, example: "DEU" } + data_sovereignty_jurisdiction: { type: string, required: true, example: "EU/GDPR" } + regulatory_frameworks: { type: array, items: string, example: [GDPR, NIS2, eIDAS] } + primary_currency: { type: string, format: ISO-4217, example: "EUR" } + utc_offsets: { type: array, items: string, example: ["UTC+1", "UTC+2"] } + +owning_authority_default: Platform Governance Team +``` + +### 2.2 Region (RGN) + +```yaml +location_type: region +code: RGN +level: 2 +parent_type: country + +standard_fields: + region_name: { type: string, required: true, example: "EU West" } + region_code: { type: string, required: true, example: "eu-west" } + geographic_bounds: { type: object, properties: { lat_min, lat_max, lon_min, lon_max } } + primary_interconnect: { type: string, example: "DE-CIX Frankfurt" } + failover_region: { type: string, format: location-handle, example: "regions/eu-north" } + latency_profile: + intra_region_ms: 2 + to_regions: { "eu-north": 15, "us-east": 85 } + +owning_authority_default: Network Operations +``` + +### 2.3 Zone / Availability Zone (AZ) + +```yaml +location_type: zone +code: AZ +level: 3 +parent_type: region + +standard_fields: + zone_name: { type: string, required: true, example: "EU West Zone A" } + zone_code: { type: string, required: true, example: "eu-west-1a" } + isolation_boundary: { enum: [independent_power, independent_cooling, independent_network, full], required: true } + high_availability_peer_zones: { type: array, items: location-handle } + target_rpo_minutes: { type: integer } + target_rto_minutes: { type: integer } + +owning_authority_default: Data Center Operations +``` + +### 2.4 Campus / Site (SITE) + +```yaml +location_type: site +code: SITE +level: 4 +parent_type: zone + +standard_fields: + site_name: { type: string, required: true, example: "Frankfurt Campus" } + site_code: { type: string, required: true, example: "FRA-CAMPUS-01" } + physical_address: { required: true, properties: { street, city, postal_code, country } } + owned_or_leased: { enum: [owned, leased, colocation, shared], required: true } + security_tier: { enum: [1, 2, 3, 4] } + noc_contact: { properties: { email, phone, escalation_url } } + +owning_authority_default: Facilities Management +``` + +### 2.5 Data Center / Facility (DC) + +```yaml +location_type: data_center +code: DC +level: 5 +parent_type: site + +standard_fields: + dc_name: { type: string, required: true, example: "DC1 — Frankfurt Alpha" } + dc_code: { type: string, required: true, example: "FRA-DC1" } + tier_classification: { enum: [tier_1, tier_2, tier_3, tier_4] } + power_capacity_kw: { type: number } + cooling_capacity_kw: { type: number } + pue_rating: { type: number, example: 1.35 } + redundancy_model: { enum: [N, N+1, 2N, 2N+1] } + network_uplinks: { type: array, items: { carrier, bandwidth_gbps, redundant } } + on_site_contact: { properties: { role, email, phone } } + dc_operations_team: { type: string, format: group-handle, required: true } + certifications: { type: array, items: { standard, expires_at } } + +owning_authority_default: Data Center Operations +``` + +### 2.6 Hall / Pod / Row (HALL) — optional + +```yaml +location_type: hall +code: HALL +level: 6 +parent_type: data_center +optional: true + +standard_fields: + hall_name: { type: string, required: true, example: "Hall A — High Density" } + hall_code: { type: string, required: true, example: "FRA-DC1-HALL-A" } + network_segment: { type: string } + power_phase: { type: string } + cooling_type: { enum: [air, liquid, rear_door, immersion] } + max_rack_units: { type: integer } + +owning_authority_default: Data Center Operations +``` + +### 2.7 Cage / Enclosure (CAGE) — optional + +```yaml +location_type: cage +code: CAGE +level: 7 +parent_type: hall +optional: true + +standard_fields: + cage_name: { type: string, required: true, example: "Cage 12 — Payments Isolated Zone" } + cage_code: { type: string, required: true, example: "FRA-DC1-HALL-A-CAGE-12" } + tenant_uuid: { type: string, format: uuid } + security_classification: { type: string, example: "restricted" } + access_control_system: { type: string, example: "Lenel S2" } + +owning_authority_default: Data Center Operations +``` + +### 2.8 Rack (RACK) + +```yaml +location_type: rack +code: RACK +level: 8 +parent_type: cage # or hall or data_center if cage/hall levels are omitted + +standard_fields: + rack_name: { type: string, required: true, example: "Rack A-12-03" } + rack_code: { type: string, required: true, example: "FRA-DC1-A-12-03" } + rack_units: { type: integer, required: true, example: 42 } + rack_units_available: { type: integer } + power_circuits: { type: array, items: { circuit_id, amperage, phase, redundant } } + max_power_kw: { type: number } + network_top_of_rack: { properties: { switch_model, uplink_gbps, port_count, vlan_range } } + patch_panel_id: { type: string } + +owning_authority_default: Data Center Operations +``` + +### 2.9 Unit / Slot (UNIT) — optional + +The Unit/Slot level is typically provider-managed; DCM does not prescribe +fields. A blade chassis slot, an HSM slot, or similar fine-grained +positioning may be tracked at this level by providers that need it. + +--- + +## 3. Hierarchy assembly + +When a consumer selects a location, DCM resolves the full ancestor chain +and assembles all location layers into the request payload in hierarchy +order (lowest precedence first): + +``` +Layer resolution (Core Layer phase of nine-step assembly): + + 1. Country layer: locations/country/de + 2. Region layer: locations/region/eu-west + 3. Zone layer: locations/az/eu-west-1a + 4. Site layer: locations/site/fra-campus-01 + 5. Data Center: locations/dc/fra-dc1 + 6. Hall layer: locations/hall/fra-dc1-hall-a + 7. Rack layer: locations/rack/fra-dc1-a-12-03 + +Assembled location context in payload: + location.country_code: DE + location.jurisdiction: EU/GDPR + location.regulatory_frameworks: [GDPR, NIS2] + location.region_code: eu-west + location.zone_code: eu-west-1a + location.dc_code: FRA-DC1 + location.rack_code: FRA-DC1-A-12-03 + location.sovereignty_zone: eu-west-sovereign + location.max_data_classification: restricted + location.certifications: [ISO 27001, SOC 2 Type II] + ... (all ancestor fields available to policies and providers) +``` + +Higher-precedence (more specific) location layers override lower-precedence +ones for the same field. A Rack layer declaring +`max_data_classification: internal` overrides the DC layer's `restricted` — +the most specific declaration wins. + +--- + +## 4. Priority band allocation + +Location layers occupy a dedicated 100–900 band in the Core Layer priority space, one +hundred-band per level (Country lowest, Unit/Slot highest), so precedence always follows +specificity. The band table and allocation rules are owned by +[`placement-and-priority-bands.md`](placement-and-priority-bands.md) §2. + +--- + +## 5. Custom location types + +Organizations may insert custom types at any level via decimal-numbered +insertion. Custom types follow the same format as standard types. + +Example: Navy deployment with Fleet (level 3.5) and Ship (level 4.5): + +```yaml +custom_location_type: + type_name: fleet + code: FLEET + display_name: "Fleet" + level: 3.5 # inserted between Zone (3) and Site (4) + parent_type: zone + child_type: ship + + standard_fields: + fleet_name: { type: string, required: true } + fleet_code: { type: string, required: true } + command_node: { type: string } + operating_area: { type: string } + + owning_authority: Fleet Operations Command + +--- +custom_location_type: + type_name: ship + code: SHIP + display_name: "Ship / Vessel" + level: 4.5 # inserted between Site (4) and Data Center (5) + parent_type: fleet + child_type: data_center + + standard_fields: + vessel_name: { type: string, required: true } + hull_number: { type: string, required: true } + vessel_class: { type: string } + home_port: { type: string } + current_location_lat: { type: number } + current_location_lon: { type: number } + connectivity_profile: { enum: [satcom, fiber_pier, disconnected], required: true } + + owning_authority: Fleet Data Center Operations +``` + +Custom type instances are created and managed exactly like standard type +instances — GitOps PRs, owned by the designated authority, versioned and +immutable. + +--- + +## 6. Layer instance format + +Each location node is a Core Layer artifact stored in GitOps and registered +in DCM. See [`placement-and-priority-bands.md`](placement-and-priority-bands.md) +for the standard layer instance format, the database/query interface, +priority band allocation in detail, and lifecycle management. + +--- + +## 7. Realization note + +The specific 9-layer hierarchy (Country, Region, Zone, Site, DC, Hall, +Cage, Rack, Unit) is **DCM's canonical default**. A peer DCM realization +operating in a different domain (a Navy fleet, a hyperscale cloud, a +mining operation, a satellite network) could pick a different hierarchy: +- A satellite network might use Constellation → Orbital Plane → Satellite → Module +- A mining operation might use Region → Site → Pit → Bench → Machine + +The layered-topology contract in +[udlm/topology/location-topology-layers.md](https://github.com/croadfeldt/udlm/blob/main/topology/location-topology-layers.md) +remains the wire contract; the specific layers and field definitions +above are this realization's choice. diff --git a/architecture/topology/placement-and-priority-bands.md b/architecture/topology/placement-and-priority-bands.md new file mode 100644 index 0000000..c85f6a0 --- /dev/null +++ b/architecture/topology/placement-and-priority-bands.md @@ -0,0 +1,396 @@ +--- +Document Status: ✅ Stable — DCM implementation +Document Type: Architecture Reference — Placement and Priority Bands +Established: 2026-05-26 +Maps to: udlm/topology/location-topology-layers.md +--- + +# Placement and Priority Bands + +> **Implements contracts defined in UDLM**: +> [udlm/topology/location-topology-layers.md](https://github.com/croadfeldt/udlm/blob/main/topology/location-topology-layers.md). +> UDLM defines the layered-topology contract (layers, parent/child +> relationships, typed fields, hierarchy assembly rules, lifecycle states, +> custom/extension mechanism). DCM operationalizes the location topology +> database and query interface, priority band allocation (premium / +> standard / budget), consumer selection model, authority and ownership +> model, relationship to the placement engine, location layer lifecycle +> management, and profile-governed topology constraints. + +> See [`canonical-9-layer-hierarchy.md`](canonical-9-layer-hierarchy.md) for +> DCM's specific 9-layer hierarchy (Country → Region → Zone → Site → DC → +> Hall → Cage → Rack → Unit). + +--- + +## 1. Location topology database and query interface + +DCM stores location layers in the PostgreSQL persistence layer: + +```sql +CREATE TABLE location_layers ( + layer_uuid UUID PRIMARY KEY, + handle VARCHAR(256) NOT NULL UNIQUE, -- locations/{type}/{code} + location_type VARCHAR(32) NOT NULL, -- country, region, zone, site, ... + level NUMERIC(4,1) NOT NULL, -- 1, 2, 3, ..., 9 (or 3.5 for custom) + parent_uuid UUID REFERENCES location_layers(layer_uuid), + priority VARCHAR(32) NOT NULL, -- "200.10.0" + status VARCHAR(16) NOT NULL, -- active | deprecated | retired + version VARCHAR(16) NOT NULL, -- semver "1.2.0" + data JSONB NOT NULL DEFAULT '{}', -- type-specific field data + sovereignty JSONB NOT NULL DEFAULT '{}', -- zone_handle, jurisdiction, residency + placement JSONB NOT NULL DEFAULT '{}', -- eligible_resource_types, max_class + concern_tags TEXT[] NOT NULL DEFAULT '{}', + owned_by JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID NOT NULL +); + +CREATE INDEX idx_location_handle ON location_layers(handle); +CREATE INDEX idx_location_type ON location_layers(location_type, status); +CREATE INDEX idx_location_parent ON location_layers(parent_uuid); +CREATE INDEX idx_location_sovereignty ON location_layers((sovereignty->>'zone_handle')); +``` + +### 1.1 Query interface + +``` +# Admin API +GET /api/v1/admin/locations +GET /api/v1/admin/locations/{location_uuid} +POST /api/v1/admin/locations # via GitOps PR — not direct write +PATCH /api/v1/admin/locations/{location_uuid} # version bump +DELETE /api/v1/admin/locations/{location_uuid} # deprecation start + +# Consumer-facing (read-only) +GET /api/v1/catalog/locations # filtered by entitlement + catalog item +``` + +### 1.2 Sample layer instance + +```yaml +layer: + artifact_metadata: + uuid: + handle: "locations/dc/fra-dc1" # locations/{type}/{code} + version: "1.2.0" + status: active + owned_by: + display_name: "Data Center Operations — Frankfurt" + group_handle: "groups/dc-operations-fra" + created_via: pr + created_at: + + layer_type: core + location_type: data_center + scope: type_agnostic + + priority: + value: "500.10.0" # band 500 (Data Center), seq 10 + label: "core.location.dc.fra-dc1" + category: core_location + + location_hierarchy: + parent_handle: "locations/site/fra-campus-01" + parent_type: site + ancestors: + - { handle: "locations/az/eu-west-1a", type: zone } + - { handle: "locations/region/eu-west", type: region } + - { handle: "locations/country/de", type: country } + + data: + dc_name: "DC1 — Frankfurt Alpha" + dc_code: "FRA-DC1" + tier_classification: tier_3 + power_capacity_kw: 4000 + pue_rating: 1.35 + redundancy_model: "2N" + network_uplinks: + - { carrier: "DE-CIX", bandwidth_gbps: 100, redundant: true } + - { carrier: "NTT", bandwidth_gbps: 100, redundant: true } + dc_operations_team: "groups/dc-operations-fra" + certifications: + - { standard: "ISO 27001", expires_at: "2027-06-30" } + - { standard: "SOC 2 Type II", expires_at: "2026-12-31" } + + sovereignty: + zone_handle: "zones/eu-west-sovereign" + data_residency: EU + jurisdiction_codes: [DE] + cross_border_permitted: false + + placement: + eligible_resource_types: [] # empty = all eligible + ineligible_resource_types: [] + max_data_classification: restricted + requires_accreditations: [] + + concern_tags: [location, data-center, frankfurt, eu-west, tier-3] +``` + +--- + +## 2. Priority band allocation + +Location layers occupy a dedicated band in the Core Layer priority space: + +``` +100.xx.0 — Country layers +200.xx.0 — Region layers +300.xx.0 — Zone / Availability Zone layers +400.xx.0 — Site / Campus layers +500.xx.0 — Data Center layers +600.xx.0 — Hall / Pod / Row layers +700.xx.0 — Cage / Enclosure layers +800.xx.0 — Rack layers +900.xx.0 — Unit / Slot layers (provider-managed) + +xx = sequence number within the level (01, 02, ... 99) +``` + +This ensures hierarchy precedence is correct: Country always has lower +precedence than Region, etc. + +### 2.1 Pricing tier bands (premium/standard/budget) + +DCM supports an orthogonal pricing tier classification within a given +location level. Bands are configured as tags on location layers and consumed +by placement preferences: + +| Pricing band | Use cases | Selection signal | +|---|---|---| +| `premium` | Production-critical, low-latency, redundant power | DC tier_classification: tier_3+, redundancy_model: 2N+ | +| `standard` | Production, normal SLA | tier_2/tier_3, N+1 redundancy | +| `budget` | Dev, test, batch, ephemeral | tier_1/tier_2, N redundancy | + +Tagged via `concern_tags: [premium]` (etc.) on the location layer. Placement +filters can include `prefer_pricing_band: standard` in catalog items. + +--- + +## 3. Consumer selection model + +Consumers do not interact with location layers directly. Location selection +is part of the **catalog item field schema**. When a consumer calls +`GET /api/v1/catalog/{catalog_item_uuid}`, the `location` field constraint +of type `layer_reference` includes the `allowed_values` list — the set of +active location layer instances the consumer is entitled to and the resource +type is eligible for. + +### 3.1 Consumer request + +```json +POST /api/v1/requests +{ + "catalog_item_uuid": "", + "fields": { + "location": "layer-uuid-fra-dc1", // DC-level layer UUID + "os_image": "layer-uuid-rhel-9-4", + "cpu_count": 4 + } +} +``` + +If the consumer wants to express location at a coarser level (Zone or +Region), they submit the layer UUID of that level. DCM's Placement Engine +refines downward to a specific DC during placement. + +### 3.2 Filtering allowed_values + +The catalog item declaration controls which location layers appear via the +`filter` clause on the `layer_reference` constraint: + +```yaml +constraint: + type: layer_reference + layer_type: location.data_center + filter: + tags: [production] + min_tier: tier_3 + required_certifications: [iso_27001] +``` + +The Platform Team controls which DCs are eligible for each catalog item by +configuring the filter — without changing the location layers themselves. + +--- + +## 4. Authority and ownership model + +Each location type has a designated owning authority. Defaults ship with +DCM but are configurable per deployment: + +```yaml +location_authority_model: + country: + creating_authority: Platform Governance Team + approval_required: true + approval_tier: platform_admin + + region: + creating_authority: Network Operations + approval_tier: platform_admin + + zone: + creating_authority: Data Center Operations + approval_tier: platform_admin + + site: + creating_authority: Facilities Management + approval_tier: team_lead + + data_center: + creating_authority: Data Center Operations + approval_tier: team_lead + + hall: + creating_authority: Data Center Operations + approval_tier: operator + + cage: + creating_authority: Data Center Operations + approval_tier: operator + + rack: + creating_authority: Data Center Operations + approval_tier: operator +``` + +All location layer changes follow the standard GitOps workflow — PRs +reviewed by the owning authority, merged on approval. Location layers are +**immutable once active** — a new version is created for any change, +preserving the full history. + +--- + +## 5. Relationship to the Placement Engine + +The Placement Manager uses location topology data at three steps of the +six-step placement algorithm (see +[`../convergence-engine/scoring.md`](../convergence-engine/scoring.md)): + +### 5.1 Step 1 — Sovereignty pre-filter + +Location layers carry `sovereignty.zone_handle`. The Placement Engine +eliminates any provider whose declared sovereignty zones do not include the +zone associated with the requested location. **Hard pre-filter** — not a +tie-breaker. + +### 5.2 Step 3 — Capability filter + +Location layers carry `placement.max_data_classification` and +`placement.requires_accreditations`. Providers that cannot satisfy these +location-level constraints are eliminated, even if they satisfy global +accreditation requirements. + +### 5.3 Step 6 — Tie-breaking + +When multiple providers qualify, location-level priority declarations can +be used as a tie-breaking preference (e.g., "prefer providers in the same +DC over providers in a different DC in the same zone"). + +### 5.4 Layer fields available to placement policies + +Location layers populate `location.*` fields in the assembled payload, +which Placement policies use in their constraint expressions: + +```rego +# Example: Placement policy for PHI data +placement if { + input.payload.location.jurisdiction == "EU/GDPR" + input.payload.location.max_data_classification == "restricted" + "hipaa_baa" in input.payload.location.required_accreditations +} +``` + +--- + +## 6. Location layer lifecycle management + +Location layers follow the standard layer lifecycle: + +``` +developing → proposed → active → deprecated → retired +``` + +### 6.1 Decommissioning a location + +When a Data Center is being decommissioned: + +1. Location layer transitions to `deprecated` +2. Placement Engine stops routing new requests to providers in that DC +3. Existing resources receive a `location.decommission_warning` notification +4. Resources are migrated to alternative locations +5. Layer transitions to `retired` when all resources have been migrated + +### 6.2 Operational draining + +For planned maintenance (not decommission), an operational draining mode is +supported: + +```yaml +operational_draining: + enabled: true + drain_for: PT4H + drain_reason: "Quarterly maintenance window" + block_new_requests: true + allow_existing_operations: true + emergency_override: requires_platform_admin +``` + +During drain, placement skips this location for new requests; existing +operations continue. + +### 6.3 Re-placement + +When a location is permanently retired and resources must be re-placed, +DCM: + +1. Generates re-placement candidates for each affected entity +2. Notifies entity owners with the proposed new location +3. Awaits owner approval or runs auto-re-placement per policy +4. Executes the re-placement as a standard request (cancel + new request, or + migrate where supported) + +### 6.4 Location data changes + +When a DC gets a new network uplink or achieves a new certification, a new +version of the location layer is published (minor version bump). The +Requested State for existing resources is NOT retroactively updated — +provenance is preserved. Future requests and re-realizations pick up the +new data. + +### 6.5 Capacity changes + +`rack_units_available` is a mutable field, updated by Data Center Operations +as capacity changes without a new version. All other location fields +require a new version to change. + +--- + +## 7. Profile-governed topology constraints + +| Profile | Topology constraints | +|---|---| +| homelab | No constraint enforcement; sovereignty optional | +| dev | Sovereignty optional; placement filter advisory | +| standard | Sovereignty required for restricted+; placement filter enforced | +| prod | Sovereignty enforced; max_data_classification enforced | +| fsi | All topology constraints enforced; cross-jurisdiction blocked at hard rule | +| sovereign | All constraints enforced; hardware attestation required for federation | + +--- + +## 8. Policy IDs (DCM realization) + +| Policy | Rule | +|---|---| +| `LOC-001-DCM` | Every DCM resource entity has a resolved location_uuid at DC level or below; requests without one rejected at validation | +| `LOC-002-DCM` | Location layers are Core Layers; service-specific or provider-specific data is invalid | +| `LOC-003-DCM` | DCM validates location hierarchy acyclicity at layer submission | +| `LOC-004-DCM` | DCM rejects location layer handles not matching pattern `locations/{type}/{code}` | +| `LOC-005-DCM` | DCM Placement Engine resolves abstract location levels (Zone, Region) to specific DC before dispatch | +| `LOC-006-DCM` | DCM enforces max_data_classification as upper bound; requests exceeding it rejected at capability filter | +| `LOC-007-DCM` | DCM propagates location layer changes to registry and Service Catalog within next sync cycle | +| `LOC-008-DCM` | Custom location types use decimal levels; level values unique across all registered types | +| `LOC-009-DCM` | DCM rejects location layers without sovereignty.zone_handle or explicit sovereignty: not_applicable in standard+ profiles | diff --git a/architecture/trust-attestation.md b/architecture/trust-attestation.md new file mode 100644 index 0000000..4a88f27 --- /dev/null +++ b/architecture/trust-attestation.md @@ -0,0 +1,50 @@ +# Trust Attestation — how DCM/UDLM attest to, build, and ensure trust + +**Goal:** earn and ensure trust for **customers, users, producers, and consumers** — not by assertion ("trust us"), but by making the trust model itself **transparent, standards-grounded, validation-backed, and independently verifiable**, and by **holding ourselves to the same bar we require of producers** (self-application). "We don't ask you to trust us — we give you what you need to *verify* us, by the same rules we apply to everyone." + +## Minimization first: this is a *projection*, not a new framework — ZERO new primitives + +A **Trust Attestation** composes records UDLM already has. We invent nothing: + +| Existing record (reused) | Contribution to the attestation | +|---|---| +| **DecisionRecord** (the WHY; reaches `CANONICAL` only with passing use-case validation) | **why** the process is the way it is — *validation-backed*, so the rationale is evidenced, not asserted | +| **Accreditation** artifact (versioned, time-bounded conformance to a framework) | **what** it conforms to + the attestation **tier** of that claim | +| **CONFORMANCE** declaration + independent-verifier flow | **self-declaration + third-party verification** of what's implemented | +| **Audit & Tamper-Evidence** (ADR-010) + field provenance | the **evidence** trail | +| **trust_posture** (ADR-022) exposed at well-known endpoints | **publication** — any party fetches + verifies | + +The Trust Attestation is the **assembled, signed, published view** of these. (One thin addition: a *projection/exposure* — not a new data primitive.) + +## The artifact — a Trust Posture Statement +A signed, versioned record any party can fetch and verify, composed of six parts: + +1. **Rationale (why).** The governing DecisionRecords — ADR-022 (credential API selection), ADR-022 (trust model), the design priorities (broker-not-authority, claim≠trust, security/trust/fit > portability, CPX-001 value-never-in-DCM). Each is validation-backed → the *why* is demonstrably sound, not just stated. +2. **Standards followed.** The adopt-by-reference catalog, version-pinned + conformance refs: X.509/PKIX, ACME/EST/SCEP/CMP, OAuth2/OIDC/RFC7662, RFC 8693, KMIP/PKCS#11, RATS (RFC 9334), NIST 800-63B (AAL), FIPS 140-3, Common Criteria, eIDAS, PCI-DSS, SOC 2, SecNumCloud, C5, IRAP. +3. **Best practices followed.** Zero-trust (**NIST SP 800-207**), least-privilege, short-lived credentials, defense-in-depth, **minimal trust surface** (broker — DCM holds no managed secrets), value-never-in-control-plane (CPX-001), separation of duties, auditability. +4. **Conformance claim.** What DCM/UDLM *implements*, at which **profile** (homelab→sovereign), **plus the attestation tier of the claim itself** (`self_asserted` → `independently_verified` → `accredited`) — the *same ladder* we apply to producers (ADR-022). A sovereign customer sees an accredited claim or it doesn't count. +5. **Evidence.** Audit/provenance + use-case validation results; and, by reference where a market needs them: supply-chain provenance (**in-toto / SLSA**), runtime attestation (**RATS**), and formal conformity assessments (SOC 2 / CC / eIDAS). +6. **Exposure.** Signed, versioned, served at a well-known endpoint (`/.well-known/udlm/trust-posture`) so customers/peers verify it without asking — the ADR-022 "expose" obligation, applied to ourselves. + +## Self-application — the credibility multiplier +DCM runs its **own** trust model **on itself**: its trust posture is declared, attested, and verified by the *same* machinery and at the *same* tiers it demands of producers. This is the strongest possible attestation — we are not exempt from our own rules. It also means a producer or customer evaluating DCM uses the *same* tooling they'd use to evaluate any provider (no special case). + +## Per-audience projection (one source, scoped views) +Same record, audience-scoped lenses (one source, many projections): +- **Customer** (regulated): the accreditation evidence + conformity assessments for *their* market (FedRAMP/eIDAS/PCI…). +- **User**: the identity + authorization posture (how they're authenticated, session/revocation guarantees). +- **Producer**: what DCM requires to register + how DCM authenticates the introductions it brokers. +- **Consumer**: what's brokered, the CPX-001 guarantee (DCM never sees the value), and the selection/attestation guarantees. + +**Renderer/assessor is an abstract role — non-normative.** The Trust Posture Statement is rendered/verified by **any conformant, independent assessor**; the model names no specific tool and depends on none. An external assessment consumer or testbed *may* exercise this role to produce/validate the views, but that is illustrative, not a structural dependency. + +## Adopt-by-reference for the *form* of attestation too (don't invent) +Even the *shape* of the attestation follows established forms — **SOC 2 / ISO conformity assessment**, **eIDAS conformity**, **C2PA / in-toto / SLSA** provenance, **RATS** runtime attestation, and the **trust-center / well-known-endpoint** publication pattern. We map onto these, we don't define a new attestation language. + +## Data · Policy · Provider +- **Data (UDLM):** the Trust Posture Statement = a projection over DecisionRecord + Accreditation + CONFORMANCE + Audit (all existing UDLM records); the `trust_posture` exposure shape. +- **Policy (DCM):** assembles, signs, and serves the statement; gates federation/selection on peers' statements; re-verifies on schedule. +- **Provider:** publishes its own posture the same way; DCM (as a participant) publishes *its* posture identically. + +## Net +**Zero new data primitives** — the attestation is a signed, published *projection* of records UDLM already defines, plus a well-known exposure endpoint. It earns trust by transparency + validation-backing + self-application + independent verifiability, all adopt-by-reference. Fully consistent with the minimize-standards/processes priority: it adds **publication + projection**, not a new framework. diff --git a/architecture/trust-flows.md b/architecture/trust-flows.md new file mode 100644 index 0000000..9b940aa --- /dev/null +++ b/architecture/trust-flows.md @@ -0,0 +1,90 @@ +# Trust & Credential Flows — expressed via existing DCM primitives + +The operational flows behind ADR-022 (DCM Trust Model, incl. Credential API Selection). **Design intent: express every flow in terms of primitives DCM already has; where a primitive is genuinely missing, propose one that follows DCM's own methodology and industry best practice.** This is the layer between the requirements (ADR-022) and engineering — engineering implements *named flows over named primitives*, it does not invent them. + +## Primitives reused (no change) +| Primitive | Role in trust/credential flows | +|---|---| +| **Request Orchestrator** (event bus) | carries `credential.requested` / lifecycle events to subscribers | +| **Placement Engine** (sovereignty pre-filter → accreditation filter → capability filter → score → select) | **is** the credential-API selection engine — credential capability is the capability dimension, attestation is the accreditation filter | +| **Policy Engine** — Validation / Transformation / Recovery | profile gating, requirement validation, provenance on mutations, failure handling | +| **Governance Matrix** (`boundary_control`) | evaluated at the consumer↔producer boundary crossing | +| **Accreditation** artifact (versioned, time-bounded) | carries `attestation[]`; the accreditation filter reads it | +| **Audit & Tamper Evidence** (ADR-010) + field provenance | every match/gate/introduction/issuance recorded | +| **Credential / Session Revocation Registry**, **Transition Window**, **Emergency Rotation** | rotation + revocation | +| **Discovery Service** (scheduled polling, configurable interval) | the scheduler for rotation/re-attestation | +| **Provider registration + lifecycle-event endpoint**, **Provider Callback Auth (PCA, mTLS)**, **Component Identity**, **Trust Anchor / Internal+External CA (ICOM-009)**, **Token Introspection (RFC 7662) / JWKS / well-known** | identity, transport, exposure | + +--- + +## Flow 1 — Broker introduction (request → direct issuance) +1. Consumer submits `credential_requirements` → `credential.requested` on the **Request Orchestrator**. *[reuse]* +2. **Placement Engine** runs as the selection engine: **sovereignty pre-filter** → **accreditation filter** (= attestation gate, ADR-022) → **capability filter** (credential_capability match) → **score** (credential scoring profile — *new, §P4*) → select producer + spec. *[reuse + P4]* +3. **Validation/Governance-Matrix** gate the selection on the profile floor (tier/frameworks/FIPS/AAL/residency; vendor-native opt-in). *[reuse]* +4. DCM mints an **Introduction Grant** (*new, §P1*) — short-lived, audience-scoped to (consumer, producer, request) — and returns it + producer endpoint + trust anchors. *[P1]* +5. Consumer connects **directly** to the producer's selected-spec endpoint, presents the Grant; producer validates it against DCM's **JWKS/introspection** (DCM's "expose"). *[reuse + P1]* +6. Producer issues the credential **direct to consumer** over the standard spec (ACME/EST/OAuth/KMIP). **Value never transits DCM** (CPX-001). *[reuse]* +7. Introduction + issuance-confirmation recorded in **Audit** with the attestation that justified it. *[reuse]* + +## Flow 2 — Attestation verification +1. On registration/update, a producer's `attestation[]` lands as **Accreditation** artifacts. *[reuse]* +2. **Attestation Verifier** (*new, §P2*) validates each: signature → issuing authority is a **recognized Accreditation Authority** (*new registry, §P2*, modeled on **Trust Anchor**) → validity window → revocation (OCSP/CRL). For `hardware_attested`, runtime **RATS** quote (RFC 9334). *[P2]* +3. Verified attestation tier/framework is cached (profile-governed TTL) and is what the **accreditation filter** (Flow 1.2) reads. *[reuse]* +4. Verification result + provenance → **Audit**. *[reuse]* + +## Flow 3 — Bootstrap (pre-registration trust) +1. Before any Credential Provider is registered, DCM uses a **Bootstrap Trust Anchor** + one-time **bootstrap token** (*new but small, §P3*) — out-of-band installed root + short-lived enrolment token. *[P3]* +2. First provider registers over PCA mTLS, presenting the bootstrap token; DCM validates against the bootstrap anchor (ICOM-009). *[reuse + P3]* +3. After bootstrap, all issuance flows through registered, attested providers via Flow 1; the bootstrap token is single-use and expires. *[reuse]* + +## Flow 4 — Rotation & revocation +1. **Discovery-Service-style scheduler** fires a rotation trigger at the declared interval (or on `expires_at` approach). *[reuse]* +2. An **Orchestration-Flow Policy** re-runs Flow 1 for the resource; **Transition Window** keeps old+new valid; cutover; old retired. *[reuse]* +3. Revocation (scheduled or **Emergency Rotation** on a security event) writes the **Credential/Session Revocation Registry**; producers + components check it per use. *[reuse]* +4. Re-attestation: the scheduler also re-runs Flow 2 before an Accreditation's `valid_until`; an expired/revoked attestation drops the provider from the accreditation filter (fail-safe). *[reuse]* + +## Flow 5 — Selection algorithm detail +The **Placement Engine scoring model** with a **credential scoring profile** (*new, §P4*): hard **filter/gate** = security + trust(attestation) + fit-for-purpose; **score/tie-break** = portability (standardized > vendor-native) + attestation-strength + lifetime fit. Portability is a tiebreak weight, never a gate (ADR-022 inversion). *[reuse + P4]* + +## Flow 6 — DCM as participant (its own credentials) +DCM, needing a credential for its own identity/user-auth, is just another **consumer**: it runs Flow 1 through the same orchestrator/placement/gating (no privileged bypass), obtaining from a registered producer or its **Internal CA** (self-producer, own components only). DCM-operational secrets are held + protected per profile (ADR-022). *[reuse]* + +--- + +## Proposed NEW primitives (the genuine gaps) + +> **v1 scope (ADR-022):** only **P1** is required for v1. **P2** ships thin (self/vendor tiers; accredited + RATS deferred), **P3** is small, **P4** is configuration. Accredited/hardware mechanisms are *declared-but-deferred* until a market needs them (fail-safe rule). + +### P1 — Introduction Grant +A short-lived, audience-scoped, signed token DCM mints to authorize a **direct** consumer↔producer credential exchange — DCM's "introduction," then it steps out (ADR-022 broker boundary). +- **Why new:** the existing **DCM Interaction Credential** authorizes *DCM→provider dispatch* (PCA); there is no token for brokering a *consumer↔producer* direct channel. This is that. +- **Basis:** OAuth 2.0 / **Token Exchange (RFC 8693)** + audience-restricted JWT; producer validates against DCM JWKS — standard, and reuses DCM's existing JWKS/introspection. Conceptually a capability token (cf. macaroons, SPIFFE JWT-SVID). +- **Fits:** minted in Flow 1.4; a Validation-class artifact; audited. + +### P2 — Attestation Verifier + Accreditation Authority registry +A verifier that turns a *claimed* attestation into a *trusted* one, and a registry of recognized authorities per framework/market. +- **Why new:** DCM has the **Accreditation** artifact + an accreditation *filter*, but not the **verification** (signature/authority/validity/revocation) nor a registry of *which* authorities are recognized for *which* market. +- **Basis:** extend the **Trust Anchor** model (ICOM-009) to accreditation authorities (CMVP/CC/FedRAMP/eIDAS/…); **RATS (RFC 9334)** for hardware/TEE remote attestation; OCSP/CRL for cert validity. A **Validation Policy** type runs it. +- **Fits:** Flow 2; feeds the accreditation filter in Flow 1.2. + +### P3 — Bootstrap Trust Anchor + bootstrap token +A minimal, out-of-band root + single-use enrolment token for the pre-registration chicken-and-egg. +- **Why new:** referenced in `credentials.md` but not specified as a primitive. +- **Basis:** kubeadm bootstrap tokens / SPIRE node attestation / TOFU-with-OOB-verification — established patterns; reuses ICOM-009 trust-anchor validation. +- **Fits:** Flow 3. + +### P4 — Credential scoring profile +A scoring profile for the existing Placement scoring model that encodes the credential priority order. +- **Why new:** it's a *configuration* of the existing scoring engine, not a new engine — but it must exist so portability scores as a **tiebreak**, not a filter (the ADR-022 inversion), with attestation-strength weighted. +- **Basis:** the existing Placement scoring model (ADR-007 / scoring.md) — just a new named profile. +- **Fits:** Flow 1.2 / Flow 5. + +--- + +## Net assessment +**~80% reuse.** The selection engine, gating, audit, accreditation artifact, revocation, rotation windows, scheduler, identity/transport, and exposure are all existing DCM primitives. **Four net-new pieces**, each grounded in an existing DCM mechanism *and* an industry standard: **P1** Introduction Grant (RFC 8693), **P2** Attestation Verifier + Authority registry (Trust-Anchor model + RATS RFC 9334), **P3** Bootstrap anchor/token (kubeadm/SPIRE), **P4** credential scoring profile (Placement scoring). Only P1 and P2 are substantive; P3 is small, P4 is configuration. Engineering then implements named flows over named primitives. + +## Data · Policy · Provider +- **Data (UDLM):** Introduction Grant shape, attestation/accreditation records, credential scoring inputs — declared/auditable. +- **Policy (DCM):** the flows are Placement + Policy-Engine + Governance-Matrix compositions; P2/P4 are policy/engine config; P1 is a minted, gated artifact. +- **Provider:** presents attestation + identity, implements the selected spec, issues direct to the consumer under the Introduction Grant. diff --git a/architecture/trust-profiles.md b/architecture/trust-profiles.md new file mode 100644 index 0000000..4251910 --- /dev/null +++ b/architecture/trust-profiles.md @@ -0,0 +1,101 @@ +# Trust & attestation — accepted methods by operational profile (homelab → sovereign) + +**Principle:** trust/attestation strength is a **definable parameter with per-profile defaults** in DCM operational profiles — *and* DCM must carry the **mechanisms** to actually satisfy each target framework (declaration without enforcement is theater). Everything below is **adopt-by-reference** (ADR-021): real, accepted standards, not invented. Each cell is the *default floor* for that profile; a request may tighten (never loosen below the floor — governance). + +Profiles (the six built-ins — the UDLM registry `profile-*` instances are the owner; `minimal` is the retired pre-ADR-017 name for `homelab`): +`homelab → dev → standard → prod → fsi → sovereign` + +--- + +## The matrix — accepted method per trust plane × profile + +> **`prod` floors:** not yet ratified per-plane — `prod` takes `standard`'s floors hardened toward `fsi` +> (shorter cert lifetimes, introspection required). To be pinned in the ADR-022 per-profile pass; a `prod` +> deployment today MUST meet at least the `standard` row. + +### Identity & transport (who is talking) +| | method (default floor) | +|---|---| +| homelab | internal/self-signed CA; TLS optional; bearer between components | +| dev | internal CA; TLS 1.2+; mTLS optional | +| standard | **mTLS everywhere**; real PKI (internal CA or ACME/Let's Encrypt); TLS 1.3; short-lived certs; **SPIFFE/SPIRE** workload identity (CNCF) recommended | +| fsi | mTLS mandatory; enterprise PKI w/ documented chain; OCSP stapling; cert lifetimes ≤ 90d; HSM-protected CA keys | +| sovereign | mTLS mandatory; **accredited** PKI; in-jurisdiction CA; hardware-protected CA (FIPS L3 HSM); RATS remote attestation of endpoints (RFC 9334) | + +### Anchor — root of trust (`anchor_type`, ADR-022) +*Suggested methods + the settable minimum per profile. `anchor_type` is a pluggable, declared dimension (ADR-022) — public-acme, internal-ca, private-acme, enterprise-pki, authority-list, hardware-rats, transparency-log, spiffe-bundle, tofu, did/ledger, threshold.* +| | suggested anchor(s) | minimum requirement | +|---|---|---| +| homelab | **`public-acme` (Let's Encrypt → ISRG root) for the public TLS edge** + **`internal-ca` or `private-acme` (step-ca) for the mesh/mTLS**; `tofu` bootstrap OK | any rooted anchor (internal-ca / private-acme / public-acme). *Note: a Let's Encrypt **leaf** can root the public edge but **cannot issue** mesh/client certs (CA:FALSE) — use step-ca/internal CA for mTLS.* self-signed only for throwaway dev | +| dev | `internal-ca` or `private-acme` (step-ca) mesh; `public-acme` edge optional | a real issuing root (internal-ca / private-acme) — no bare self-signed for the mesh | +| standard | internal/enterprise PKI or `private-acme` mesh + `public-acme` edge; **`transparency-log` (CT/Sigstore)** recommended; `spiffe-bundle` optional | real CA + CRL/OCSP; transparency-logging recommended | +| fsi | `enterprise-pki` (documented chain) with **HSM-protected issuing root**; `authority-list` for attestation; CT | HSM-protected issuing CA **+** external `authority-list` for assurance claims | +| sovereign | **accredited, in-jurisdiction, HSM-L3 issuing root**, root key **`threshold`/ceremony-protected**; external authority roots (CMVP/eIDAS) **+ `hardware-rats`**; disconnected = import + re-anchor | accredited + HSM-L3 issuing root, external-authority attestation, hardware-attested (top tier), quorum-protected root key | + +### Authorization (what they may do) +| | method | +|---|---| +| homelab | static token / basic OIDC; long-ish sessions | +| dev | OIDC (Keycloak/RHSSO); JWT; introspection optional | +| standard | OIDC + **token introspection (RFC 7662)** + JWKS rotation; session revocation registry; AAL1–2 | +| fsi | OIDC + **AAL2 hardware MFA**; step-up-MFA for sensitive ops; short token TTL; dual-control on privileged actions | +| sovereign | **AAL3** hardware-bound (FIDO2/PIV/CAC); step-up everywhere; PT-scale token TTL; full revocation propagation | + +### Credential issuance / retrieval (ADR-022 selected spec) +| | x509 | tokens | keys | +|---|---|---|---| +| homelab / dev | self-signed / internal ACME | OAuth2 | software keys | +| standard | **ACME** (RFC 8555) / internal CA | OAuth2 + OIDC | software or KMS | +| fsi | **EST/CMP** to enterprise CA; ≤90d | OAuth2, short TTL | **HSM/KMIP**, FIPS 140-3 | +| sovereign | EST/CMP to accredited CA, in-jurisdiction | mTLS-bound tokens | **HSM L3 + key ceremony**, split-knowledge (NIST SP 800-57) | + +### Attestation (the trust backing — ADR-022 ladder) +| | default tier + accepted frameworks | +|---|---| +| homelab | `self_asserted` | +| dev | `self_asserted` → `vendor_attested` | +| standard | `vendor_attested` / `independently_verified`; SOC 2; ISO 27001; FIPS via CMVP if claimed | +| fsi | `independently_verified`+; **PCI-DSS, SOC 2 Type II, FIPS 140-3 CMVP** | +| sovereign | `accredited` + `hardware_attested`; market authority: **FedRAMP High + FIPS 140-3 L3 + Common Criteria** (US), **eIDAS QTSP / SecNumCloud / BSI C5 / EUCS** (EU), **IRAP** (AU), **ISMAP** (JP); TPM/HSM remote attestation; confidential-compute attestation (SEV-SNP/TDX/SGX) where applicable | + +### Federation (trusting another DCM/provider) +| | method | +|---|---| +| homelab / dev | manual trust-anchor add | +| standard | exchange trust anchors; verify CONFORMANCE declaration | +| fsi | + verify `independently_verified` attestation before federating | +| sovereign | + verify `accredited` posture, jurisdiction match, and live attestation; no federation to lower-posture peers | + +Cross-cutting baseline (ALL profiles, including homelab — strictness scales, existence doesn't): zero-trust posture (NIST SP 800-207); CPX-001 (values never transit DCM); audit of every trust decision (ADR-010); forbidden-weak-algorithm baseline; supply-chain integrity (SLSA / signed artifacts) recommended standard+. + +--- + +## Mechanisms DCM must implement (to *support*, not just declare, the targets) +1. **PKI/PKIX + mTLS** stack with trust-anchor management, chain validation, **CRL + OCSP** checking (ICOM-009). +2. **ACME / EST / SCEP / CMP** client(s) for cert issuance/rotation; SPIFFE/SPIRE integration option. +3. **OIDC relying-party + token introspection (RFC 7662) + JWKS** rotation; session revocation registry; step-up-MFA hook. +4. **HSM / PKCS#11 / KMIP** integration for key custody; BYOK/HYOK; key-ceremony + split-knowledge support. +5. **Attestation verification**: ingest + validate Accreditation artifacts (cert id, validity, revocation); **RATS remote attestation** (RFC 9334) for TPM/HSM/confidential-compute quotes. +6. **Accreditation/conformance registry** surface — queryable trust posture (provider + DCM's own), exposed at well-known endpoints. +7. **Profile engine**: the trust/attestation floor is a profile parameter; the selection/gate engine (ADR-022) enforces it per request, market-graded. + +> A profile may *require* a framework only if DCM has the mechanism to verify it. Gap = a tracked conformance item, not a silent pass. Where a mechanism is absent, the profile cannot claim that target (fail-safe). + +--- + +## The definable parameter (ties to DCM operational profiles) +```yaml +profile: sovereign +region: eu +trust_floor: # default per profile; request may tighten only + anchor: { allowed_types: [accredited-pki, hardware-rats], # the pluggable anchor_type set + root_protection: threshold, in_jurisdiction: true, + external_authority_required: true } # homelab e.g.: { allowed_types: [public-acme, internal-ca, private-acme, tofu] } + identity: { mtls: required, ca: accredited, endpoint_attestation: required } + authorization: { min_aal: aal3, step_up: required } + attestation: { min_tier: accredited, require_hardware_attested: true, + accepted_frameworks: [eidas-qtsp, secnumcloud, bsi-c5, fips-140-3] } + key_custody: { hsm: required, fips_level: 140-3-L3, jurisdiction_in: [eu] } + federation: { min_peer_tier: accredited, jurisdiction_match: true } +``` +Defaults shipped for `homelab`, `dev`, `standard`, `prod`, `fsi`, `sovereign`; operators override within governance bounds. This makes "what trust is required here" a **declared, queryable, governable** profile parameter — selected and enforced by the ADR-022/023 engine.