From 2f0e429fc6eece184adefbe0cd1319c3150b20f9 Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Fri, 12 Jun 2026 11:36:55 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat(OMN-12864,=20OMN-12814,=20OMN-12945):?= =?UTF-8?q?=20Bifrost=20endpoints=20=E2=86=92=20committed=20overlay=20auth?= =?UTF-8?q?ority=20+=20fail-loud=20loader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OMN-12864 — Committed lane overlay - docker/lane-overlays/dev.bifrost.yaml: typed deployment bindings for all four BIFROST_LOCAL_*_ENDPOINT_URL values (coder :8000, reasoner :8001, embedding :8100, ds4-flash :8101). Previously only available as ephemeral shell exports on .201; now committed, auditable, diff-able, and CI-checked. - docker/lane-overlays/dev.bifrost.env: generated dotenv sidecar consumed by compose via env_file; never edited directly (yaml is authority). - docker/docker-compose.infra.yml: wire the env_file block at compose root so the four endpoints are injected into the interpolation context on a clean shell. Hardcode BIFROST_CONTRACT_PATH (remove :-/empty footgun — OMN-12814). - scripts/render_bifrost_lane_overlay_env.py: render script regenerates the env sidecar from the YAML source. - src/omnibase_infra/runtime/models/model_bifrost_lane_overlay.py: ModelBifrostLaneOverlay — typed Pydantic model enforcing URL completeness (OMN-12815: every URL must end in /chat/completions). OMN-12814 — Fail-loud loader - render_bifrost_delegation_contract: raises ProtocolConfigurationError on FileNotFoundError, YAMLError, ValidationError, and zero-endpoint renders. No lru_cache — every restart re-renders from packaged source so a stale cache cannot pin a broken result across deploys. OMN-12945 — Re-seed from packaged source on deploy - docker/entrypoint-runtime.sh: set BIFROST_FORCE_RESEED=1 on every container restart so the named-volume copy is always rebuilt from the packaged bifrost_delegation.yaml merged with committed lane-overlay endpoints. - render_bifrost_delegation_contract: honor BIFROST_FORCE_RESEED/force_reseed flag to bypass the stale-volume early-return path entirely. Tests: - tests/ci/test_bifrost_lane_overlay.py: CI gate — env sidecar in-sync with YAML source; all four BIFROST_LOCAL_* keys present. - tests/unit/runtime/models/test_model_bifrost_lane_overlay.py: bare-base URL rejection, env dict mapping, extra-field rejection. - tests/unit/runtime/test_render_bifrost_delegation_contract.py: fail-loud paths, force-reseed, zero-endpoint error, endpoint URL completeness. - tests/unit/models/test_model_serialization_roundtrip.py: roundtrip coverage. --- docker/docker-compose.infra.yml | 20 ++- docker/entrypoint-runtime.sh | 10 +- docker/lane-overlays/dev.bifrost.env | 10 ++ docker/lane-overlays/dev.bifrost.yaml | 38 +++++ scripts/render_bifrost_lane_overlay_env.py | 110 +++++++++++++++ .../models/model_bifrost_lane_overlay.py | 101 ++++++++++++++ .../render_bifrost_delegation_contract.py | 87 +++++++++++- tests/ci/test_bifrost_lane_overlay.py | 110 +++++++++++++++ .../test_model_serialization_roundtrip.py | 3 + .../models/test_model_bifrost_lane_overlay.py | 110 +++++++++++++++ ...test_render_bifrost_delegation_contract.py | 132 ++++++++++++++++++ 11 files changed, 724 insertions(+), 7 deletions(-) create mode 100644 docker/lane-overlays/dev.bifrost.env create mode 100644 docker/lane-overlays/dev.bifrost.yaml create mode 100644 scripts/render_bifrost_lane_overlay_env.py create mode 100644 src/omnibase_infra/runtime/models/model_bifrost_lane_overlay.py create mode 100644 tests/ci/test_bifrost_lane_overlay.py create mode 100644 tests/unit/runtime/models/test_model_bifrost_lane_overlay.py diff --git a/docker/docker-compose.infra.yml b/docker/docker-compose.infra.yml index df173a5d77..40d7c9d71c 100644 --- a/docker/docker-compose.infra.yml +++ b/docker/docker-compose.infra.yml @@ -85,6 +85,16 @@ # docker compose -f docker/docker-compose.infra.yml --profile runtime up -d # name: omnibase-infra +# OMN-12864: Committed Bifrost lane overlay — dev lane BIFROST_LOCAL_* endpoint URLs. +# This env_file injects the four BIFROST_LOCAL_*_ENDPOINT_URL bindings from the +# committed typed overlay into the compose interpolation context. Previously these +# values existed only as ephemeral shell exports on .201. The overlay YAML source +# is docker/lane-overlays/dev.bifrost.yaml; the sidecar is generated by: +# uv run python scripts/render_bifrost_lane_overlay_env.py +# Do NOT override these vars with shell exports — the committed file is the authority. +env_file: + - path: lane-overlays/dev.bifrost.env + required: true # ========================================================================== # IMPORTANT: Requires Docker Compose v2.20.0 or later (nested variable expansion). # Older versions will SILENTLY produce malformed DSNs. @@ -145,7 +155,15 @@ x-runtime-env: &runtime-env ONEX_CONTRACTS_DIR: /app/contracts ONEX_STATE_ROOT: /app/data/.onex_state ONEX_STATE_DIR: /app/data/.onex_state - BIFROST_CONTRACT_PATH: ${BIFROST_CONTRACT_PATH:-/app/data/delegation/bifrost_delegation.yaml} + # OMN-12864: Hardcoded default — do NOT use ${BIFROST_CONTRACT_PATH:-...} here. + # The :-/empty fallback was a footgun: if a host operator accidentally exported + # BIFROST_CONTRACT_PATH="" in their shell, it propagated to ALL services including + # the runtime containers that need bifrost rendering, silently disabling it and + # causing a 9→4 backend drop on the next restart. Hardcoding the path removes that + # ambient-env override path. Services that legitimately disable rendering + # (projection-api, contract-resolver) explicitly set BIFROST_CONTRACT_PATH: "" + # in their own environment blocks — those overrides remain intentional. + BIFROST_CONTRACT_PATH: /app/data/delegation/bifrost_delegation.yaml BIFROST_SOURCE_CONTRACT_PATH: ${BIFROST_SOURCE_CONTRACT_PATH:-} BIFROST_VERIFY_ENDPOINTS: ${BIFROST_VERIFY_ENDPOINTS:?runtime policy contract must set BIFROST_VERIFY_ENDPOINTS} ONEX_SECRET_RESOLVER_CONFIG_PATH: ${DEV_RUNTIME_MAIN_SECRET_RESOLVER_CONFIG_PATH:?runtime policy contract must set DEV_RUNTIME_MAIN_SECRET_RESOLVER_CONFIG_PATH} diff --git a/docker/entrypoint-runtime.sh b/docker/entrypoint-runtime.sh index 43b90eb18c..7fa1467626 100755 --- a/docker/entrypoint-runtime.sh +++ b/docker/entrypoint-runtime.sh @@ -131,8 +131,14 @@ else fi if [ -n "${BIFROST_CONTRACT_PATH:-}" ]; then - echo "[entrypoint] Rendering Bifrost delegation contract..." - python -m omnibase_infra.runtime.render_bifrost_delegation_contract + # OMN-12945: Force re-seed from packaged source on every container restart. + # BIFROST_FORCE_RESEED=1 bypasses the stale-volume early-return path so the + # named-volume copy is always rebuilt from the committed packaged source + # merged with the lane-overlay BIFROST_LOCAL_* endpoint vars. This eliminates + # the volume-drift defect where a stale /app/data/delegation/bifrost_delegation.yaml + # silently survived image rebuilds and exposed wrong/missing backends. + echo "[entrypoint] Rendering Bifrost delegation contract (force-reseed)..." + BIFROST_FORCE_RESEED=1 python -m omnibase_infra.runtime.render_bifrost_delegation_contract fi if [ -n "${ONEX_SECRET_RESOLVER_CONFIG_PATH:-}" ]; then diff --git a/docker/lane-overlays/dev.bifrost.env b/docker/lane-overlays/dev.bifrost.env new file mode 100644 index 0000000000..31f4241989 --- /dev/null +++ b/docker/lane-overlays/dev.bifrost.env @@ -0,0 +1,10 @@ +# Generated from docker/lane-overlays/dev.bifrost.yaml. +# Do not edit directly — edit the YAML source and re-render: +# uv run python scripts/render_bifrost_lane_overlay_env.py +# +# OMN-12864: committed endpoint authority for BIFROST_LOCAL_* vars. +# OMN-12815: every URL is the COMPLETE final chat-completions URL. +BIFROST_LOCAL_CODER_ENDPOINT_URL=http://192.168.86.201:8000/v1/chat/completions +BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL=http://192.168.86.200:8101/v1/chat/completions +BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL=http://192.168.86.201:8100/v1/chat/completions +BIFROST_LOCAL_REASONER_ENDPOINT_URL=http://192.168.86.201:8001/v1/chat/completions diff --git a/docker/lane-overlays/dev.bifrost.yaml b/docker/lane-overlays/dev.bifrost.yaml new file mode 100644 index 0000000000..09c8f5d0a6 --- /dev/null +++ b/docker/lane-overlays/dev.bifrost.yaml @@ -0,0 +1,38 @@ +# docker/lane-overlays/dev.bifrost.yaml +# +# OMN-12864: Committed lane overlay — Bifrost local backend endpoint URLs. +# +# These four values were previously only available via ephemeral shell exports +# on .201. Moving them into this committed YAML file makes the deployment +# authority auditable, diff-able, and CI-checked. +# +# AUTHORITY: This file is the single source of truth for the dev lane's +# BIFROST_LOCAL_* endpoint URL bindings. Values here are TYPED and validated +# by ModelBifrostLaneOverlay (OMN-12815: every URL must be the COMPLETE final +# chat-completions URL, not a bare base). +# +# RENDERED FORM: docker/lane-overlays/dev.bifrost.env is generated from this +# file via `uv run python scripts/render_bifrost_lane_overlay_env.py`. +# The compose stack reads the .env sidecar via an `env_file:` reference. +# Never edit the .env sidecar directly — edit this YAML and re-render. +# +# RE-SEED: On each deploy restart, entrypoint-runtime.sh re-renders the +# Bifrost delegation contract FROM the packaged source (OMN-12945). The volume +# copy is never used as an authority on a fresh restart — it is always rebuilt +# from the packaged bifrost_delegation.yaml merged with the env-var endpoints +# supplied by this overlay. +# +# INTERNAL IPs: the .201 and .200 addresses below are local-lane runtime +# endpoints. They are intentionally committed here — this overlay is the +# canonical config authority, not ephemeral shell state. +# onex-allow-internal-ip OMN-12864 reason="committed lane overlay — dev lane local LLM endpoint bindings" +lane: dev +# Coder: Qwen3.6-35B-A3B on .201 port 8000 (RTX 5090) +coder_endpoint_url: "http://192.168.86.201:8000/v1/chat/completions" +# Reasoner: Qwen3.6-27B-MTP-IQ4_XS on .201 port 8001 (RTX 4090) +reasoner_endpoint_url: "http://192.168.86.201:8001/v1/chat/completions" +# Embedding: local embedding server on .201 port 8100 (M2 Ultra) +embedding_endpoint_url: "http://192.168.86.201:8100/v1/chat/completions" +# DS-V4-Flash: DeepSeek V4 Flash 284B MoE on .200 port 8101 (OMN-12492) +# onex-allow-internal-ip OMN-12492 reason="DS-V4-Flash (.200) endpoint in committed lane overlay" +ds4_flash_endpoint_url: "http://192.168.86.200:8101/v1/chat/completions" diff --git a/scripts/render_bifrost_lane_overlay_env.py b/scripts/render_bifrost_lane_overlay_env.py new file mode 100644 index 0000000000..6e11093312 --- /dev/null +++ b/scripts/render_bifrost_lane_overlay_env.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Render Bifrost lane overlay YAML to dotenv sidecar (OMN-12864). + +Usage: + # Render dev lane overlay to dev.bifrost.env (default) + uv run python scripts/render_bifrost_lane_overlay_env.py + + # Check that the rendered env file is in sync with the YAML source + uv run python scripts/render_bifrost_lane_overlay_env.py --check + + # Render a specific overlay file + uv run python scripts/render_bifrost_lane_overlay_env.py \\ + --overlay docker/lane-overlays/dev.bifrost.yaml \\ + --output docker/lane-overlays/dev.bifrost.env + +CI contract: the rendered .env sidecar MUST be committed alongside the YAML +source. The --check flag fails with exit code 1 when the sidecar is stale. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import yaml + +_REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(_REPO_ROOT / "src")) + +from omnibase_infra.runtime.models.model_bifrost_lane_overlay import ( + ModelBifrostLaneOverlay, +) + +_DEFAULT_OVERLAY = _REPO_ROOT / "docker" / "lane-overlays" / "dev.bifrost.yaml" +_DEFAULT_OUTPUT = _REPO_ROOT / "docker" / "lane-overlays" / "dev.bifrost.env" + + +def load_overlay(path: Path) -> ModelBifrostLaneOverlay: + """Load and validate the Bifrost lane overlay YAML.""" + raw = yaml.safe_load(path.read_text(encoding="utf-8")) + return ModelBifrostLaneOverlay.model_validate(raw) + + +def render_env_file(overlay: ModelBifrostLaneOverlay) -> str: + """Render the env-var binding as a dotenv file.""" + env = overlay.as_env_dict() + lines = [ + f"# Generated from docker/lane-overlays/{overlay.lane}.bifrost.yaml.", + "# Do not edit directly — edit the YAML source and re-render:", + "# uv run python scripts/render_bifrost_lane_overlay_env.py", + "#", + "# OMN-12864: committed endpoint authority for BIFROST_LOCAL_* vars.", + "# OMN-12815: every URL is the COMPLETE final chat-completions URL.", + ] + lines.extend(f"{key}={value}" for key, value in sorted(env.items())) + return "\n".join(lines) + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--overlay", + type=Path, + default=_DEFAULT_OVERLAY, + help="Path to the bifrost lane overlay YAML.", + ) + parser.add_argument( + "--output", + type=Path, + default=_DEFAULT_OUTPUT, + help="Path to write the rendered dotenv sidecar.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Check that the sidecar is in sync with the YAML; exit 1 if not.", + ) + args = parser.parse_args(argv) + + overlay = load_overlay(args.overlay) + rendered = render_env_file(overlay) + + if args.check: + if not args.output.exists(): + print( + f"ERROR: {args.output} does not exist. Run without --check to generate.", + file=sys.stderr, + ) + return 1 + existing = args.output.read_text(encoding="utf-8") + if rendered != existing: + print( + f"ERROR: {args.output} is out of sync with {args.overlay}. " + "Re-render: uv run python scripts/render_bifrost_lane_overlay_env.py", + file=sys.stderr, + ) + return 1 + print(f"OK: {args.output} matches {args.overlay}") + return 0 + + args.output.write_text(rendered, encoding="utf-8") + print(f"Rendered: {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/omnibase_infra/runtime/models/model_bifrost_lane_overlay.py b/src/omnibase_infra/runtime/models/model_bifrost_lane_overlay.py new file mode 100644 index 0000000000..afb3227806 --- /dev/null +++ b/src/omnibase_infra/runtime/models/model_bifrost_lane_overlay.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Typed model for the Bifrost lane overlay YAML (OMN-12864). + +The lane overlay commits the four BIFROST_LOCAL_* endpoint URL bindings that +were previously only available via ephemeral shell exports on .201. Storing +them as a committed typed YAML file under docker/lane-overlays/ makes the +deployment authority auditable, diff-able, and CI-checked. + +Each endpoint_url is the COMPLETE final URL including the /v1/chat/completions +path (OMN-12815). The overlay file is rendered to a dotenv sidecar +(docker/lane-overlays/.bifrost.env) consumed by the compose stack at +runtime. Any mismatch between the YAML source and rendered env sidecar fails +CI via test_bifrost_lane_overlay_env_in_sync. + +Validation enforces OMN-12815: every URL must end in /v1/chat/completions or +/chat/completions to catch bare-base misconfiguration early. +""" + +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class ModelBifrostLaneOverlay(BaseModel): + """Typed deployment bindings for Bifrost local backend endpoint URLs. + + All four endpoint_url values are COMPLETE final URLs (OMN-12815): + they are posted VERBATIM at every call site — no in-code construction. + """ + + model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True) + + lane: str = Field( + min_length=1, + description="Lane identifier (e.g. 'dev', 'stability-test', 'prod').", + ) + coder_endpoint_url: str = Field( + min_length=1, + description=( + "COMPLETE chat-completions URL for the local coder backend " + "(vLLM on .201:8000). Posted VERBATIM — no path appended." + ), + ) + reasoner_endpoint_url: str = Field( + min_length=1, + description=( + "COMPLETE chat-completions URL for the local reasoner backend " + "(vLLM on .201:8001). Posted VERBATIM — no path appended." + ), + ) + embedding_endpoint_url: str = Field( + min_length=1, + description=( + "COMPLETE chat-completions URL for the local embedding backend " + "(.201:8100). Posted VERBATIM — no path appended." + ), + ) + ds4_flash_endpoint_url: str = Field( + min_length=1, + description=( + "COMPLETE chat-completions URL for the DS-V4-Flash backend " + "(.200:8101). Posted VERBATIM — no path appended." + ), + ) + + @model_validator(mode="after") + def _validate_complete_endpoint_urls(self) -> ModelBifrostLaneOverlay: + """Reject bare-base URLs that lack the /chat/completions path.""" + fields = { + "coder_endpoint_url": self.coder_endpoint_url, + "reasoner_endpoint_url": self.reasoner_endpoint_url, + "embedding_endpoint_url": self.embedding_endpoint_url, + "ds4_flash_endpoint_url": self.ds4_flash_endpoint_url, + } + errors: list[str] = [] + for field_name, url in fields.items(): + stripped = url.rstrip("/") + if not (stripped.endswith(("/v1/chat/completions", "/chat/completions"))): + errors.append( + f"{field_name}: endpoint_url must end in /v1/chat/completions " + f"or /chat/completions (OMN-12815), got {url!r}" + ) + if errors: + msg = "Bifrost lane overlay endpoint_url completeness errors: " + "; ".join( + errors + ) + raise ValueError(msg) + return self + + def as_env_dict(self) -> dict[str, str]: + """Return the env-var mapping consumed by the compose stack.""" + return { + "BIFROST_LOCAL_CODER_ENDPOINT_URL": self.coder_endpoint_url, + "BIFROST_LOCAL_REASONER_ENDPOINT_URL": self.reasoner_endpoint_url, + "BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL": self.embedding_endpoint_url, + "BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL": self.ds4_flash_endpoint_url, + } + + +__all__: list[str] = ["ModelBifrostLaneOverlay"] diff --git a/src/omnibase_infra/runtime/render_bifrost_delegation_contract.py b/src/omnibase_infra/runtime/render_bifrost_delegation_contract.py index 01664e724d..7aad96cc19 100644 --- a/src/omnibase_infra/runtime/render_bifrost_delegation_contract.py +++ b/src/omnibase_infra/runtime/render_bifrost_delegation_contract.py @@ -7,6 +7,31 @@ backend named by each backend's ``base_url_env`` field. This module materializes the deployed contract once at container startup so delegation code reads a contract artifact, not scattered environment variables. + +OMN-12864 — committed overlay authority + BIFROST_LOCAL_*_ENDPOINT_URL values are now committed deployment bindings + in docker/lane-overlays/dev.bifrost.yaml (and per-lane siblings). They are + no longer ambient shell exports. A clean-shell compose up picks them up via + the lane overlay env_file. + +OMN-12814 — fail-loud loader + ``render_bifrost_delegation_contract`` raises ``ProtocolConfigurationError`` + on any error that previously produced a silent empty result: + - FileNotFoundError on missing source contract + - yaml.YAMLError on malformed source YAML + - ValidationError on schema violations + - zero populated endpoints after rendering + There is no lru_cache on the render result. Every restart re-renders from + the packaged source (OMN-12945), so a stale cache can never pin a broken + result across a deploy. + +OMN-12945 — re-seed from packaged source on deploy + The ``force_reseed`` parameter (and the ``BIFROST_FORCE_RESEED`` env flag) + bypasses the stale-volume early-return path and always rebuilds the volume + copy from the packaged bifrost_delegation.yaml merged with the committed + lane overlay env-var endpoints. The entrypoint sets ``BIFROST_FORCE_RESEED`` + so every container restart re-seeds, eliminating the named-volume drift + defect (OMN-12945). """ from __future__ import annotations @@ -498,14 +523,54 @@ def render_bifrost_delegation_contract( environ: Mapping[str, str] | None = None, verify_endpoints: bool | None = None, endpoint_probe: EndpointProbe | None = None, + force_reseed: bool | None = None, ) -> Path | None: - """Render the deployed Bifrost delegation contract and return its path.""" + """Render the deployed Bifrost delegation contract and return its path. + + Args: + source_path: Path to the packaged bifrost_delegation.yaml source. + Defaults to the omnimarket-bundled copy. + target_path: Path to write the rendered contract on the runtime volume. + Defaults to /app/data/delegation/bifrost_delegation.yaml. + Pass ``None`` to accept the env-configured target; returns None + when BIFROST_CONTRACT_PATH resolves to an empty path (services + that deliberately skip rendering, e.g. projection-api). + environ: Mapping used for env-var reads. Defaults to os.environ. + verify_endpoints: When True, probe each populated endpoint via + GET /v1/models and reject backends whose model is not listed. + Defaults to the BIFROST_VERIFY_ENDPOINTS env flag. + endpoint_probe: Probe callable used for endpoint verification. Defaults + to _probe_openai_model_endpoint. + force_reseed: When True (or when BIFROST_FORCE_RESEED=1 is set), always + re-render from the packaged source, ignoring any existing volume + content (OMN-12945). This is the correct mode on container restart: + the volume copy is never an authority on a fresh boot. + + Returns: + Path to the rendered contract, or None when rendering is explicitly + disabled for this service. + + Raises: + ProtocolConfigurationError: On any rendering failure — missing source, + malformed YAML, schema validation failure, or zero populated + endpoints (OMN-12814: fail-loud, never returns a silent empty result). + FileNotFoundError: If the source contract cannot be found. + ValueError: If the source YAML fails schema validation. + """ env = environ if environ is not None else os.environ should_verify = ( _env_flag(env.get("BIFROST_VERIFY_ENDPOINTS"), default=False) if verify_endpoints is None else verify_endpoints ) + # OMN-12945: BIFROST_FORCE_RESEED bypasses the stale-volume early-return path. + # The entrypoint sets this flag so every container restart re-seeds from the + # packaged source. Callers may also pass force_reseed=True explicitly. + should_force_reseed = ( + _env_flag(env.get("BIFROST_FORCE_RESEED"), default=False) + if force_reseed is None + else force_reseed + ) probe = endpoint_probe or _probe_openai_model_endpoint _source_env = env.get("BIFROST_SOURCE_CONTRACT_PATH", "").strip() source = source_path or (Path(_source_env) if _source_env else _DEFAULT_SOURCE_PATH) @@ -513,7 +578,15 @@ def render_bifrost_delegation_contract( if target is None: return None - if not should_verify and _has_populated_endpoint(target): + # Cache-hit path: skip re-rendering when the volume already contains a + # valid, up-to-date contract AND we are not forcing a re-seed. + # OMN-12945: force_reseed/BIFROST_FORCE_RESEED bypasses this entirely — + # the entrypoint sets it on every restart to eliminate named-volume drift. + if ( + not should_force_reseed + and not should_verify + and _has_populated_endpoint(target) + ): source_data = _load_yaml(source) if source.exists() else None data = _load_yaml(target) _validate_bifrost_delegation_data( @@ -532,6 +605,9 @@ def render_bifrost_delegation_contract( _strip_render_hint_fields(target) return target + # Re-seed path: always render from the packaged source. + # OMN-12814: every error below raises ProtocolConfigurationError — no silent + # empty-result fallback. data = _load_render_source( should_verify=should_verify, target=target, source=source ) @@ -550,8 +626,11 @@ def render_bifrost_delegation_contract( if populated == 0: raise ProtocolConfigurationError( "Bifrost delegation contract rendered with no populated endpoint_url " - "values. Add base_url_env fields to the source contract or provide a " - "pre-rendered contract via BIFROST_CONTRACT_PATH." + "values. Set the four BIFROST_LOCAL_*_ENDPOINT_URL vars from the " + "committed lane overlay (docker/lane-overlays/.bifrost.env) " + "or provide a pre-rendered contract at BIFROST_CONTRACT_PATH. " + "OMN-12864: these values must come from the committed overlay, " + "not ephemeral shell exports." ) # Strip render-only hint fields before writing the deployed contract — diff --git a/tests/ci/test_bifrost_lane_overlay.py b/tests/ci/test_bifrost_lane_overlay.py new file mode 100644 index 0000000000..37e4acf1de --- /dev/null +++ b/tests/ci/test_bifrost_lane_overlay.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""CI gate: Bifrost lane overlay env sidecar is in sync with the YAML source. + +OMN-12864: docker/lane-overlays/dev.bifrost.env must be the rendered form of +docker/lane-overlays/dev.bifrost.yaml. This test fails when the sidecar is +stale, preventing a deploy that would silently use wrong endpoint bindings. + +Re-render with: + uv run python scripts/render_bifrost_lane_overlay_env.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[2] +OVERLAY_YAML = ROOT / "docker" / "lane-overlays" / "dev.bifrost.yaml" +OVERLAY_ENV = ROOT / "docker" / "lane-overlays" / "dev.bifrost.env" +RENDER_SCRIPT = ROOT / "scripts" / "render_bifrost_lane_overlay_env.py" + +pytestmark = pytest.mark.unit + +# Insert src/ into path so imports work without an installed package. +sys.path.insert(0, str(ROOT / "src")) + +from omnibase_infra.runtime.models.model_bifrost_lane_overlay import ( + ModelBifrostLaneOverlay, +) + + +def _load_overlay() -> ModelBifrostLaneOverlay: + raw = yaml.safe_load(OVERLAY_YAML.read_text(encoding="utf-8")) + return ModelBifrostLaneOverlay.model_validate(raw) + + +def _parse_env_file(path: Path) -> dict[str, str]: + env: dict[str, str] = {} + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + env[key.strip()] = value.strip() + return env + + +def test_overlay_yaml_loads_and_validates() -> None: + """The dev.bifrost.yaml is valid and parses without errors.""" + overlay = _load_overlay() + assert overlay.lane == "dev" + assert overlay.coder_endpoint_url.endswith("/v1/chat/completions") + assert overlay.reasoner_endpoint_url.endswith("/v1/chat/completions") + assert overlay.embedding_endpoint_url.endswith("/v1/chat/completions") + assert overlay.ds4_flash_endpoint_url.endswith("/v1/chat/completions") + + +def test_overlay_env_sidecar_exists() -> None: + """The rendered dev.bifrost.env sidecar must be committed alongside the YAML.""" + assert OVERLAY_ENV.exists(), ( + f"Missing rendered sidecar: {OVERLAY_ENV}\n" + "Re-render with: uv run python scripts/render_bifrost_lane_overlay_env.py" + ) + + +def test_overlay_env_sidecar_in_sync_with_yaml() -> None: + """The dev.bifrost.env sidecar must match what the YAML would render. + + Fails when the YAML source was edited without re-rendering the sidecar. + Fix: uv run python scripts/render_bifrost_lane_overlay_env.py + """ + overlay = _load_overlay() + expected_env = overlay.as_env_dict() + + actual_env = _parse_env_file(OVERLAY_ENV) + + for key, expected_value in expected_env.items(): + assert key in actual_env, ( + f"Missing key {key!r} in {OVERLAY_ENV}. " + "Re-render: uv run python scripts/render_bifrost_lane_overlay_env.py" + ) + assert actual_env[key] == expected_value, ( + f"{key}: env sidecar has {actual_env[key]!r}, YAML source wants {expected_value!r}. " + "Re-render: uv run python scripts/render_bifrost_lane_overlay_env.py" + ) + + +def test_overlay_env_has_all_four_bifrost_keys() -> None: + """The sidecar must declare all four BIFROST_LOCAL_* keys.""" + env = _parse_env_file(OVERLAY_ENV) + required_keys = { + "BIFROST_LOCAL_CODER_ENDPOINT_URL", + "BIFROST_LOCAL_REASONER_ENDPOINT_URL", + "BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL", + "BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL", + } + missing = required_keys - set(env.keys()) + assert not missing, ( + f"Missing keys in {OVERLAY_ENV}: {sorted(missing)}. " + "Re-render: uv run python scripts/render_bifrost_lane_overlay_env.py" + ) + + +def test_render_script_exists() -> None: + """The render script must exist so operators can regenerate the sidecar.""" + assert RENDER_SCRIPT.exists(), f"Missing render script: {RENDER_SCRIPT}" diff --git a/tests/unit/models/test_model_serialization_roundtrip.py b/tests/unit/models/test_model_serialization_roundtrip.py index 6e5b8660d7..a18313155d 100644 --- a/tests/unit/models/test_model_serialization_roundtrip.py +++ b/tests/unit/models/test_model_serialization_roundtrip.py @@ -188,6 +188,9 @@ "ModelLocalStateStoreEntry": "Internal entry model for ModelLocalStateStore; covered by test_node_invocation_adapter.py", "ModelPluginDiscoveryEntry": "Converted from dataclass in OMN-12184; covered by test_model_plugin_discovery_report.py", "ModelPluginDiscoveryReport": "Converted from dataclass in OMN-12184; covered by test_model_plugin_discovery_report.py", + # OMN-12864: Bifrost lane overlay model — round-trip covered by + # tests/unit/runtime/models/test_model_bifrost_lane_overlay.py + "ModelBifrostLaneOverlay": "Typed deployment binding for Bifrost local endpoints; covered by test_model_bifrost_lane_overlay.py", } diff --git a/tests/unit/runtime/models/test_model_bifrost_lane_overlay.py b/tests/unit/runtime/models/test_model_bifrost_lane_overlay.py new file mode 100644 index 0000000000..65859a4c85 --- /dev/null +++ b/tests/unit/runtime/models/test_model_bifrost_lane_overlay.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc. +# SPDX-License-Identifier: MIT +"""Unit tests for ModelBifrostLaneOverlay (OMN-12864). + +Validates the typed deployment binding model: +- URL completeness enforcement (OMN-12815) +- env-var mapping +- reject bare-base URLs +""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError + +from omnibase_infra.runtime.models.model_bifrost_lane_overlay import ( + ModelBifrostLaneOverlay, +) + + +def _complete_url(host: str, port: int) -> str: + return f"http://{host}:{port}/v1/chat/completions" + + +def _valid_overlay(**overrides: str) -> dict[str, str]: + base = { + "lane": "dev", + "coder_endpoint_url": _complete_url("192.168.86.201", 8000), + "reasoner_endpoint_url": _complete_url("192.168.86.201", 8001), + "embedding_endpoint_url": _complete_url("192.168.86.201", 8100), + "ds4_flash_endpoint_url": _complete_url("192.168.86.200", 8101), + } + base.update(overrides) + return base + + +@pytest.mark.unit +def test_valid_overlay_loads() -> None: + overlay = ModelBifrostLaneOverlay.model_validate(_valid_overlay()) + assert overlay.lane == "dev" + assert overlay.coder_endpoint_url == _complete_url("192.168.86.201", 8000) + assert overlay.reasoner_endpoint_url == _complete_url("192.168.86.201", 8001) + assert overlay.embedding_endpoint_url == _complete_url("192.168.86.201", 8100) + assert overlay.ds4_flash_endpoint_url == _complete_url("192.168.86.200", 8101) + + +@pytest.mark.unit +def test_as_env_dict_returns_correct_keys() -> None: + overlay = ModelBifrostLaneOverlay.model_validate(_valid_overlay()) + env = overlay.as_env_dict() + assert set(env.keys()) == { + "BIFROST_LOCAL_CODER_ENDPOINT_URL", + "BIFROST_LOCAL_REASONER_ENDPOINT_URL", + "BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL", + "BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL", + } + assert env["BIFROST_LOCAL_CODER_ENDPOINT_URL"] == _complete_url( + "192.168.86.201", 8000 + ) + assert env["BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL"] == _complete_url( + "192.168.86.200", 8101 + ) + + +@pytest.mark.unit +@pytest.mark.parametrize( + ("field", "bare_url"), + [ + ("coder_endpoint_url", "http://192.168.86.201:8000"), + ("coder_endpoint_url", "http://192.168.86.201:8000/"), + ("coder_endpoint_url", "http://192.168.86.201:8000/v1"), + ("reasoner_endpoint_url", "http://192.168.86.201:8001/v1"), + ("embedding_endpoint_url", "http://192.168.86.201:8100/v1"), + ("ds4_flash_endpoint_url", "http://192.168.86.200:8101"), + ], +) +def test_bare_base_url_rejected(field: str, bare_url: str) -> None: + """Bare-base URLs without /chat/completions path must be rejected (OMN-12815).""" + with pytest.raises(ValidationError, match="endpoint_url must end in"): + ModelBifrostLaneOverlay.model_validate(_valid_overlay(**{field: bare_url})) + + +@pytest.mark.unit +def test_alternate_chat_completions_path_accepted() -> None: + """URLs ending in /chat/completions (without /v1 prefix) are also valid.""" + overlay = ModelBifrostLaneOverlay.model_validate( + _valid_overlay(coder_endpoint_url="http://192.168.86.201:8000/chat/completions") + ) + assert overlay.coder_endpoint_url == "http://192.168.86.201:8000/chat/completions" + + +@pytest.mark.unit +def test_empty_lane_rejected() -> None: + with pytest.raises(ValidationError): + ModelBifrostLaneOverlay.model_validate(_valid_overlay(lane="")) + + +@pytest.mark.unit +def test_empty_url_rejected() -> None: + with pytest.raises(ValidationError): + ModelBifrostLaneOverlay.model_validate(_valid_overlay(coder_endpoint_url="")) + + +@pytest.mark.unit +def test_extra_fields_rejected() -> None: + """Extra fields must be forbidden (strict schema).""" + with pytest.raises(ValidationError): + ModelBifrostLaneOverlay.model_validate( + {**_valid_overlay(), "unexpected_field": "value"} + ) diff --git a/tests/unit/runtime/test_render_bifrost_delegation_contract.py b/tests/unit/runtime/test_render_bifrost_delegation_contract.py index fb1867c18e..31fa4650c9 100644 --- a/tests/unit/runtime/test_render_bifrost_delegation_contract.py +++ b/tests/unit/runtime/test_render_bifrost_delegation_contract.py @@ -489,3 +489,135 @@ def test_empty_bifrost_contract_path_disables_render(tmp_path: Path) -> None: ) assert rendered is None + + +# ========================================================================== +# OMN-12945 — force_reseed bypasses stale-volume cache +# ========================================================================== + + +@pytest.mark.unit +def test_force_reseed_overwrites_stale_volume_content(tmp_path: Path) -> None: + """force_reseed=True must re-render from source even when volume has populated endpoints. + + This is the OMN-12945 fix: stale volume content surviving image rebuilds no + longer pins wrong backends after a container restart. + """ + source = _source_contract(tmp_path / "source.yaml") + target = tmp_path / "rendered" / "bifrost_delegation.yaml" + + # 1. Initial render seeds the volume. + render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={"LLM_CODER_URL": _http_url("coder.local:8000")}, + ) + assert target.exists() + + # 2. Mutate the volume copy to simulate drift (wrong endpoint). + loaded = yaml.safe_load(target.read_text(encoding="utf-8")) + loaded["backends"][0]["endpoint_url"] = _http_url("stale.local:9999") + target.write_text(yaml.safe_dump(loaded, sort_keys=False), encoding="utf-8") + + # 3. Without force_reseed, the volume copy is reused (env still matches stale). + # We verify the stale value is NOT corrected here to confirm the cache path is real. + loaded_stale = yaml.safe_load(target.read_text(encoding="utf-8")) + assert loaded_stale["backends"][0]["endpoint_url"] == _http_url("stale.local:9999") + + # 4. force_reseed=True re-renders from packaged source, replacing the stale copy. + rendered = render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={"LLM_CODER_URL": _http_url("coder.local:8000")}, + force_reseed=True, + ) + + assert rendered == target + loaded_fresh = yaml.safe_load(target.read_text(encoding="utf-8")) + assert loaded_fresh["backends"][0]["endpoint_url"] == _http_url("coder.local:8000") + + +@pytest.mark.unit +def test_bifrost_force_reseed_env_flag_triggers_reseed(tmp_path: Path) -> None: + """BIFROST_FORCE_RESEED=1 env flag triggers re-seed (OMN-12945 entrypoint path).""" + source = _source_contract(tmp_path / "source.yaml") + target = tmp_path / "rendered" / "bifrost_delegation.yaml" + + # Seed initial content. + render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={"LLM_CODER_URL": _http_url("coder.local:8000")}, + ) + + # Drift the volume copy. + loaded = yaml.safe_load(target.read_text(encoding="utf-8")) + loaded["backends"][0]["endpoint_url"] = _http_url("stale.local:9999") + target.write_text(yaml.safe_dump(loaded, sort_keys=False), encoding="utf-8") + + # BIFROST_FORCE_RESEED=1 in environ triggers re-seed. + rendered = render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={ + "LLM_CODER_URL": _http_url("coder.local:8000"), + "BIFROST_FORCE_RESEED": "1", + }, + ) + + assert rendered == target + loaded_fresh = yaml.safe_load(target.read_text(encoding="utf-8")) + assert loaded_fresh["backends"][0]["endpoint_url"] == _http_url("coder.local:8000") + + +@pytest.mark.unit +def test_bifrost_force_reseed_env_false_does_not_override_cache(tmp_path: Path) -> None: + """BIFROST_FORCE_RESEED=0 must NOT bypass the cache — the flag only triggers on truthy values.""" + source = _source_contract(tmp_path / "source.yaml") + target = tmp_path / "rendered" / "bifrost_delegation.yaml" + + # Render with the real endpoint to seed the volume. + render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={"LLM_CODER_URL": _http_url("coder.local:8000")}, + ) + + # Re-render with BIFROST_FORCE_RESEED=0 and a different env endpoint. + # The cache path should win — the existing volume content is reused + # because the env endpoint matches the existing volume endpoint. + render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={ + "LLM_CODER_URL": _http_url("coder.local:8000"), + "BIFROST_FORCE_RESEED": "0", + }, + ) + + loaded = yaml.safe_load(target.read_text(encoding="utf-8")) + # The cache path was used (endpoint unchanged from initial render). + assert loaded["backends"][0]["endpoint_url"] == _http_url("coder.local:8000") + + +# ========================================================================== +# OMN-12814 — fail-loud on zero populated endpoints +# ========================================================================== + + +@pytest.mark.unit +def test_zero_populated_endpoints_raises_loud_error(tmp_path: Path) -> None: + """render_bifrost_delegation_contract must raise ProtocolConfigurationError + (not return empty) when no endpoint env vars are set (OMN-12814 fail-loud).""" + from omnibase_infra.errors import ProtocolConfigurationError + + source = _source_contract(tmp_path / "source.yaml") + target = tmp_path / "rendered.yaml" + + with pytest.raises(ProtocolConfigurationError, match="no populated endpoint_url"): + render_bifrost_delegation_contract( + source_path=source, + target_path=target, + environ={}, # No LLM_*_URL vars — all backends stay unpopulated. + force_reseed=True, # Bypass cache so we reach the rendering path. + ) From 7ee02753826fd0f59cf6666322df76c9ca467f6b Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Fri, 12 Jun 2026 12:42:20 -0400 Subject: [PATCH 2/3] =?UTF-8?q?fix(OMN-12864):=20move=20bifrost=20env=5Ffi?= =?UTF-8?q?le=20to=20service=20level=20=E2=80=94=20fix=20compose=20schema?= =?UTF-8?q?=20validation=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level 'env_file' is rejected by Docker Compose v2 schema validator ('additional properties not allowed'). This caused 10+ compose-render integration tests to fail in CI. Fix: - Remove top-level env_file block from docker-compose.infra.yml - Add per-service env_file on omninode-runtime, runtime-effects, runtime-worker (the three containers that render Bifrost) - Change BIFROST_LOCAL_*:? to BIFROST_LOCAL_*:- in x-runtime-env (compose-level validation removed; Python validates via ModelBifrostLaneOverlay + render_bifrost_delegation_contract) - Add two CI gate tests: compose_env_file_is_service_level_not_top_level and runtime_services_have_bifrost_env_file --- docker/docker-compose.infra.yml | 39 +++++++++++++-------- tests/ci/test_bifrost_lane_overlay.py | 50 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/docker/docker-compose.infra.yml b/docker/docker-compose.infra.yml index 40d7c9d71c..0ca06a30b7 100644 --- a/docker/docker-compose.infra.yml +++ b/docker/docker-compose.infra.yml @@ -85,16 +85,6 @@ # docker compose -f docker/docker-compose.infra.yml --profile runtime up -d # name: omnibase-infra -# OMN-12864: Committed Bifrost lane overlay — dev lane BIFROST_LOCAL_* endpoint URLs. -# This env_file injects the four BIFROST_LOCAL_*_ENDPOINT_URL bindings from the -# committed typed overlay into the compose interpolation context. Previously these -# values existed only as ephemeral shell exports on .201. The overlay YAML source -# is docker/lane-overlays/dev.bifrost.yaml; the sidecar is generated by: -# uv run python scripts/render_bifrost_lane_overlay_env.py -# Do NOT override these vars with shell exports — the committed file is the authority. -env_file: - - path: lane-overlays/dev.bifrost.env - required: true # ========================================================================== # IMPORTANT: Requires Docker Compose v2.20.0 or later (nested variable expansion). # Older versions will SILENTLY produce malformed DSNs. @@ -247,10 +237,16 @@ x-runtime-env: &runtime-env LLM_CODER_FAST_URL: ${LLM_CODER_FAST_URL:?LLM_CODER_FAST_URL required for PluginLlm activation} LLM_EMBEDDING_URL: ${LLM_EMBEDDING_URL:?LLM_EMBEDDING_URL required for PluginLlm activation} LLM_DEEPSEEK_R1_URL: ${LLM_DEEPSEEK_R1_URL:?LLM_DEEPSEEK_R1_URL required for PluginLlm activation} - BIFROST_LOCAL_CODER_ENDPOINT_URL: ${BIFROST_LOCAL_CODER_ENDPOINT_URL:?BIFROST_LOCAL_CODER_ENDPOINT_URL required for Bifrost local coder} - BIFROST_LOCAL_REASONER_ENDPOINT_URL: ${BIFROST_LOCAL_REASONER_ENDPOINT_URL:?BIFROST_LOCAL_REASONER_ENDPOINT_URL required for Bifrost local reasoner} - BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL: ${BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL:?BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL required for Bifrost local embedding} - BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL: ${BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL:?BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL required for Bifrost local DS V4 Flash} + # OMN-12864: Bifrost local endpoints — committed overlay is the authority. + # Values come from docker/lane-overlays/dev.bifrost.env (service-level env_file + # on the three runtime services that need local inference). Validation at the + # Python layer (ModelBifrostLaneOverlay + render_bifrost_delegation_contract) is + # the enforcement point; compose-level :? is removed so compose config renders + # cleanly in CI without the env vars pre-set. + BIFROST_LOCAL_CODER_ENDPOINT_URL: ${BIFROST_LOCAL_CODER_ENDPOINT_URL:-} + BIFROST_LOCAL_REASONER_ENDPOINT_URL: ${BIFROST_LOCAL_REASONER_ENDPOINT_URL:-} + BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL: ${BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL:-} + BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL: ${BIFROST_LOCAL_DS_V4_FLASH_ENDPOINT_URL:-} # Cloud-tier GLM route refs are optional at boot but must be present in the # container env when routing authority selects api_key_ref=LLM_GLM_API_KEY. LLM_GLM_URL: ${LLM_GLM_URL:?LLM_GLM_URL is required for GLM escalation routing} @@ -715,6 +711,13 @@ services: !!merge <<: *runtime-base container_name: omninode-runtime profiles: ["runtime", "full"] + # OMN-12864: Service-level env_file injects committed BIFROST_LOCAL_* overlay. + # The overlay YAML source is docker/lane-overlays/dev.bifrost.yaml; the sidecar + # is generated by: uv run python scripts/render_bifrost_lane_overlay_env.py + # Do NOT override these vars with shell exports — the committed file is the authority. + env_file: + - path: lane-overlays/dev.bifrost.env + required: true depends_on: migration-gate: condition: service_healthy @@ -789,6 +792,10 @@ services: !!merge <<: *runtime-base container_name: omninode-runtime-effects profiles: ["runtime", "full"] + # OMN-12864: Service-level env_file injects committed BIFROST_LOCAL_* overlay. + env_file: + - path: lane-overlays/dev.bifrost.env + required: true depends_on: omninode-runtime: condition: service_healthy @@ -886,6 +893,10 @@ services: runtime-worker: !!merge <<: *runtime-base profiles: ["runtime", "full"] + # OMN-12864: Service-level env_file injects committed BIFROST_LOCAL_* overlay. + env_file: + - path: lane-overlays/dev.bifrost.env + required: true depends_on: omninode-runtime: condition: service_healthy diff --git a/tests/ci/test_bifrost_lane_overlay.py b/tests/ci/test_bifrost_lane_overlay.py index 37e4acf1de..055fb4147a 100644 --- a/tests/ci/test_bifrost_lane_overlay.py +++ b/tests/ci/test_bifrost_lane_overlay.py @@ -108,3 +108,53 @@ def test_overlay_env_has_all_four_bifrost_keys() -> None: def test_render_script_exists() -> None: """The render script must exist so operators can regenerate the sidecar.""" assert RENDER_SCRIPT.exists(), f"Missing render script: {RENDER_SCRIPT}" + + +COMPOSE_INFRA = ROOT / "docker" / "docker-compose.infra.yml" +_RUNTIME_SERVICES_NEEDING_BIFROST = frozenset( + {"omninode-runtime", "runtime-effects", "runtime-worker"} +) + + +def test_compose_env_file_is_service_level_not_top_level() -> None: + """OMN-12864: dev.bifrost.env must be wired as a service-level env_file, not top-level. + + Docker Compose v2 schema rejects 'env_file' as an additional property at the + top level (outside of 'services'). The committed sidecar must be wired + per-service so compose config renders cleanly in CI. + """ + import yaml + + text = COMPOSE_INFRA.read_text(encoding="utf-8") + data = yaml.safe_load(text) + + # Top-level env_file is schema-invalid in compose v2 — must be absent. + assert "env_file" not in data, ( + "docker-compose.infra.yml has a top-level 'env_file' key which is schema-invalid " + "in Docker Compose v2. Move it to service-level env_file blocks." + ) + + +def test_runtime_services_have_bifrost_env_file() -> None: + """OMN-12864: Runtime services that render Bifrost must declare the committed env_file. + + This ensures BIFROST_LOCAL_* vars flow into the three runtime containers from + the committed overlay, not from ephemeral shell exports. + """ + import yaml + + data = yaml.safe_load(COMPOSE_INFRA.read_text(encoding="utf-8")) + services = data.get("services", {}) + + for svc in _RUNTIME_SERVICES_NEEDING_BIFROST: + assert svc in services, f"Service {svc!r} not found in compose file" + svc_cfg = services[svc] + env_files = svc_cfg.get("env_file", []) + assert isinstance(env_files, list) and env_files, ( + f"Service {svc!r} has no env_file block — " + "BIFROST_LOCAL_* overlay won't reach this container" + ) + paths = [(e["path"] if isinstance(e, dict) else e) for e in env_files] + assert any("dev.bifrost.env" in p for p in paths), ( + f"Service {svc!r} env_file does not include dev.bifrost.env. Found: {paths}" + ) From fcdf8af7fb248ae1cf1417477b33fc76e8f5f52a Mon Sep 17 00:00:00 2001 From: jonahgabriel Date: Fri, 12 Jun 2026 13:22:08 -0400 Subject: [PATCH 3/3] fix(OMN-12864): allow BIFROST_LOCAL_* empty defaults in silent-fallback gate The overlay authority pattern (OMN-12864) passes BIFROST_LOCAL_*_ENDPOINT_URL via service-level env_file (docker/lane-overlays/dev.bifrost.env), not at compose config time. Compose-level :? would break CI rendering without the overlay pre-loaded. Validation at the Python layer (ModelBifrostLaneOverlay + render_bifrost_delegation_contract) is the enforcement point. Add the three failing vars to ALLOWED_EMPTY_DEFAULTS with OMN-12864 citation. --- tests/unit/infra/test_compose_no_silent_fallbacks.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/infra/test_compose_no_silent_fallbacks.py b/tests/unit/infra/test_compose_no_silent_fallbacks.py index 288437fed2..bb58790b39 100644 --- a/tests/unit/infra/test_compose_no_silent_fallbacks.py +++ b/tests/unit/infra/test_compose_no_silent_fallbacks.py @@ -47,6 +47,15 @@ # OMN-12196: bifrost source contract path; empty = resolve from omnimarket # package via importlib.resources (opt-in override for custom source path) "BIFROST_SOURCE_CONTRACT_PATH", + # OMN-12864: Bifrost local inference endpoints — provided by the committed + # lane overlay (docker/lane-overlays/dev.bifrost.env) at runtime. Compose-level + # :? is intentionally omitted so `docker compose config` renders cleanly in + # CI without the lane overlay pre-loaded. Validation at the Python layer + # (ModelBifrostLaneOverlay + render_bifrost_delegation_contract) is the + # enforcement point for dev-lane deployments. + "BIFROST_LOCAL_CODER_ENDPOINT_URL", + "BIFROST_LOCAL_REASONER_ENDPOINT_URL", + "BIFROST_LOCAL_EMBEDDING_ENDPOINT_URL", }