provisioning/adrs/adr-042-ecosystem-integration-modes.ncl

141 lines
14 KiB
XML

let d = import "adr-defaults.ncl" in
d.make_adr {
id = "adr-042",
title = "Ecosystem integration via federated Integration Modes mediated by versioned Domain artifacts in OCI",
status = 'Accepted,
date = "2026-05-01",
context = "After the lift-out of lian-build (ADR-040) and cloudatasave (ADR-041) as standalone projects, no mechanism exists for them to integrate with provisioning without re-coupling to its filesystem. An earlier framing modeled the problem as 'extension of a host' — a plugin protocol with an ExtensionManifest parallel to Mode, where provisioning was the host and external projects were plugins. This framing is structurally incompatible with the ontoref protocol already adopted across the ecosystem: ontoref has Mode, Domain, and Reflection as native primitives that express the same relationship with semantic coherence. The term 'extension' is also overloaded in the codebase: it simultaneously names (a) the plugin-protocol framing (rejected) and (b) the IaC artifact catalog at provisioning/extensions/ (components, providers, taskservs, playbooks, workflows). These two concepts share a name by historical accident.",
decision = "Adopt the federated Integration Mode pattern: each participant (provisioning, lian-build, cloudatasave, vapora, CI) declares its integration points as Modes with kind = 'integration in its own reflection/modes/. Participants exchange typed contracts via Domain artifacts — versioned OCI blobs custodiados en reg.librecloud.online/domains/<id>:<semver> — without reading each other's filesystems. Provisioning-side caller context (SOPS secrets, component values, literals) is materialised as Cabling files (infra/<ws>/integrations/<mode-id>.ncl) resolved by a dedicated Rust crate (context-assembler) into a typed JSON payload sent via stdin to the Mode binary. Three primitives: Integration Mode (reflects self, declares domains_used), Domain artifact (shared OCI contract with inputs/outputs/events channels), Cabling (workspace-local materialisation of the contract). Separately, provisioning/extensions/ is renamed to provisioning/catalog/ to give the IaC building-block catalog its semantically correct name, decoupled from the integration protocol.",
rationale = [
{
claim = "federated Mode pattern is the native on+re idiom — no parallel schema needed",
detail = "ontoref already models operational units as Modes with DAG steps, QA specs, and capability declarations. Introducing a separate ExtensionManifest concept alongside Mode would split the semantic model. A Mode with kind = 'integration is a Mode — it fits the existing indexing, describe, and run primitives natively without new tooling.",
},
{
claim = "OCI Domain artifacts provide content-addressed versioned contracts without a custom server",
detail = "A Domain artifact is a set of Nickel schema files (inputs.ncl, outputs.ncl, events.ncl, capabilities.ncl, version.ncl) pushed to the existing zot registry with a custom mediaType. oras-cli handles push/pull. Content-addressable digests provide integrity without signing in v0.1 (acceptable for internal use; cosign added in hardening). No custom server, no custom protocol: standard OCI distribution API.",
},
{
claim = "Ruta beta for OCI CLI surface: local implementation in prvng integration, not upstream to ontoref",
detail = "ontoref v0.1.0 implements no OCI commands: no domain publish, no domain pull, no ecosystem domains, no OCI distribution API access, no custom mediaType management. Contributing upstream requires designing, reviewing, and merging a large module in a project with its own release cadence before the first proof-of-stack. Implementing locally under prvng integration and extracting after validation is coherent with the lift-out pattern already practised (lian-build and cloudatasave both originated in provisioning). Breaking changes during v0.1 stay local. Extraction is mechanical once the contract is stable.",
},
{
claim = "Ruta B for Mode schema extension: embedded subset in provisioning schemas until stable",
detail = "For the same iteration-cost reason: a Mode with kind = 'integration and its associated fields (domains_used, invocation, direction) lives in provisioning/schemas/lib/integration_mode_manifest.ncl until validated with two real consumers (lian-build, cloudatasave). Promoting upstream after validation avoids locking the ontoref schema to an API that may still change.",
},
{
claim = "catalog/ rename removes the semantic collision from the codebase permanently",
detail = "As long as provisioning/extensions/ exists, every future developer must reason about whether 'extension' means the IaC catalog or the rejected plugin protocol. A clean rename to provisioning/catalog/ eliminates the ambiguity at zero ongoing cost. The protocol framing disappears from commands, dispatch, schemas, and module paths simultaneously.",
},
{
claim = "Modes never read caller filesystem — context arrives as typed stdin JSON",
detail = "The plugin-protocol approach allowed extension manifests to declare filesystem paths the host would populate. This creates implicit coupling: the Mode depends on the host's directory layout. Delivering context as a typed JSON payload (assembled by context-assembler from Cabling) means the Mode binary needs no knowledge of the caller's filesystem. The contract is the Domain schema, not a path convention.",
},
],
consequences = {
positive = [
"lian-build and cloudatasave integrate with provisioning by declaring Modes and consuming Domains — no filesystem coupling, no re-absorption",
"New participants (vapora, CI, future) adopt the pattern by publishing a Mode artifact; no changes to provisioning internals required",
"Domain artifacts are OCI-addressable: discovery is standard registry catalog API, not filesystem grep",
"Cabling files are workspace-local NCL — type-safe, auditable, version-controlled alongside the workspace definition",
"provisioning/catalog/ correctly names the IaC building-block library; confusion with the integration protocol is eliminated",
"context-assembler Rust crate is generically reusable by any caller: vapora, CI, future participants",
"Signing (cosign) is addable as a hardening step without architectural change — the OCI push/pull path already exists",
],
negative = [
"oras-cli is a new runtime dependency for the OCI CLI surface (acceptable: single binary, brew install)",
"context-assembler introduces a Rust crate boundary for what was previously a Nushell inline operation — justified by crypto-sensitive plaintext handling and typed schema validation",
"provisioning/catalog/ rename propagates across all workspace component NCL imports — one-time cost, verified by nickel export smoke tests",
"Mode binary distribution (how the Mode binary reaches the operator's PATH) is out-of-band for v0.1 (invocation.method = 'path_assumed); resolved in a later iteration when 'oci_blob or 'cargo_install are needed",
"Signing of Domain and Mode artifacts is deferred to hardening (TASK-14); artifacts are unsigned during v0.1, acceptable for internal-only registry",
],
},
alternatives_considered = [
{
option = "Extension protocol: host-mediated plugin manifest parallel to Mode",
why_rejected = "Introduced a second schema object (ExtensionManifest) alongside Mode in the ontoref model, creating two registration paths for essentially the same concept. The host (provisioning) becoming a plugin-loader couples its internal lifecycle to external project release cadences. Filesystem-path coupling between host and extension was implicit and unvalidated. The on+re Mode primitive already expresses this relationship — a parallel schema is an anti-PAP.",
},
{
option = "Ruta alpha for OCI CLI: upstream contribution to ontoref",
why_rejected = "ontoref v0.1.0 has no OCI surface at all. Contributing upstream means coordinating design, review, and merge in a project with independent release cadence before proving the first round-trip. Ruta beta (local implementation, extract after validation) delivers the proof-of-stack sooner and keeps breaking changes local during the unstable v0.1 period. If ontoref gains throughput and there is demand outside provisioning, extraction is mechanical.",
},
{
option = "Ruta A for Mode schema: upstream contribution of kind = 'integration to ontoref core",
why_rejected = "Same reasoning as ruta alpha for OCI: the schema should stabilise against two real consumers (lian-build, cloudatasave) before being locked into upstream. Ruta B (embedded subset) allows iteration without external coordination.",
},
{
option = "Collapsing provisioning/extensions/ under provisioning/integrations/",
why_rejected = "A component, provider, taskserv, playbook, or workflow is not an integration in the federated Mode sense. Collapsing them under the same name would re-introduce the semantic collision the rename is trying to eliminate. The IaC building blocks are catalog items; the federated protocol participants are integration modes. Two concepts, two names: catalog/ and integrations/.",
},
],
constraints = [
{
id = "integration-mode-must-declare-domains-used",
claim = "A Mode with kind = 'integration MUST declare domains_used as a non-empty array — a Mode that declares no domains is not an integration mode",
scope = "provisioning/schemas/lib/integration_mode_manifest.ncl",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "provisioning/schemas/lib/integration_mode_manifest.ncl",
present = true,
},
rationale = "An integration Mode with no domains_used has no contract with its caller — it might as well be a regular Mode. The domains_used requirement enforces that every integration point is explicitly declared and versioned.",
},
{
id = "integration-mode-no-filesystem-read-of-caller",
claim = "Integration Mode binaries MUST NOT read the caller's filesystem — context arrives exclusively as typed stdin JSON assembled by context-assembler from the Cabling file",
scope = "provisioning/schemas/lib/integration_mode_manifest.ncl, crates/context-assembler/",
severity = 'Hard,
check = { tag = 'Grep, pattern = "path_read|filesystem_", paths = ["provisioning/schemas/lib/integration_mode_manifest.ncl"], must_be_empty = true },
rationale = "Filesystem reads couple the Mode binary to the host's directory layout, recreating the implicit coupling that the federated pattern is designed to eliminate.",
},
{
id = "catalog-not-integrations",
claim = "IaC building blocks (components, providers, taskservs, playbooks, workflows) MUST live under provisioning/catalog/ — they MUST NOT be placed under provisioning/integrations/",
scope = "provisioning/catalog/",
severity = 'Hard,
check = {
tag = 'FileExists,
path = "provisioning/catalog",
present = true,
},
rationale = "Mixing IaC catalog items with integration Mode artifacts recreates the semantic collision between the old 'extension' catalog and the old 'extension' protocol. The rename to catalog/ is the permanent resolution.",
},
{
id = "domain-artifacts-in-oci-registry",
claim = "Domain artifacts MUST be published to reg.librecloud.online/domains/<id>:<semver> and consumed via oras pull — inline filesystem imports of domain schemas are not permitted after v0.1",
scope = "infra/<ws>/integrations/, crates/context-assembler/",
severity = 'Hard,
check = { tag = 'Grep, pattern = "import.*domains/", paths = ["provisioning/core/nulib/"], must_be_empty = true },
rationale = "Filesystem-local domain schemas cannot be discovered, versioned, or integrity-checked by oras. Moving to OCI is what enables the federated discovery model.",
},
{
id = "extension-command-removed",
claim = "prvng extension (and aliases e, ext) MUST NOT exist after the catalog rename — the command is replaced by prvng integration for the federation surface and optionally prvng catalog for catalog browsing",
scope = "provisioning/core/cli/provisioning, provisioning/core/nulib/commands-registry.ncl",
severity = 'Hard,
check = {
tag = 'Grep,
pattern = "\"extension\"",
paths = ["provisioning/core/nulib/commands-registry.ncl"],
must_be_empty = true,
},
rationale = "A prvng extension command surviving the rename would perpetuate the vocabulary collision and confuse operators about whether 'extension' refers to the catalog or the protocol.",
},
],
ontology_check = {
decision_string = "Federated Integration Mode pattern via OCI Domain artifacts: Modes declare domains_used, context assembled by Rust crate from Cabling (SOPS + component + literal + env resolvers), no filesystem coupling, ruta beta OCI CLI in prvng integration, ruta B Mode schema embedded in provisioning schemas, catalog rename provisioning/extensions/ to provisioning/catalog/",
invariants_at_risk = ["config-driven-always", "type-safety-nickel"],
verdict = 'Safe,
},
related_adrs = ["adr-040-lian-build-lift-out", "adr-041-cloudatasave-lift-out", "adr-039-build-infrastructure-ephemeral", "adr-033-cluster-component-extension-pattern"],
}