From 61528b825f4adc4f0a546fe8e97df466fd89defa Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Thu, 6 Aug 2026 00:15:18 -0700 Subject: [PATCH 1/2] fix: require Relay 0.7 for coding-agent gateways Signed-off-by: Ajay Thorve --- adapters/claude/README.md | 4 +- adapters/codex/README.md | 4 +- .../common/relay_gateway.py | 8 +- .../src/nemo_fabric_adapters/common/utils.py | 111 ++++++++++++++++- docs/integrations/harness/claude.mdx | 4 +- docs/integrations/harness/codex.mdx | 4 +- docs/sdk/python.mdx | 5 +- examples/code_review_agent/README.md | 2 +- examples/harbor/README.md | 2 +- examples/harbor/swebench/README.md | 4 +- tests/adapters/test_adapaters_common_utils.py | 112 +++++++++++++----- .../test_adapters_common_relay_gateway.py | 18 +-- tests/adapters/test_claude_adapter.py | 4 +- tests/adapters/test_codex_adapter.py | 4 +- tests/e2e/test_claude.py | 2 +- 15 files changed, 220 insertions(+), 68 deletions(-) diff --git a/adapters/claude/README.md b/adapters/claude/README.md index e809ac0cf..bbb45520d 100644 --- a/adapters/claude/README.md +++ b/adapters/claude/README.md @@ -120,8 +120,8 @@ by the SDK and is not persisted as a NeMo Fabric artifact. ## Relay Observability -Relay requires a separately installed NeMo Relay 0.6.x CLI on `PATH`; the Python -`nemo-relay` package does not provide the executable. Follow the +Relay requires a separately installed NeMo Relay 0.7.x CLI on `PATH`; the +Python `nemo-relay` package does not provide the executable. Follow the [NeMo Relay installation instructions](https://docs.nvidia.com/nemo/fabric/getting-started/install#install-nemo-relay). Enable Relay through the normalized NeMo Fabric configuration: diff --git a/adapters/codex/README.md b/adapters/codex/README.md index a073a231b..3f4f3a63c 100644 --- a/adapters/codex/README.md +++ b/adapters/codex/README.md @@ -145,8 +145,8 @@ Codex state variables, the selected model's `api_key_env`, and explicit ## Relay Integration -Relay requires a separately installed NeMo Relay 0.6.x CLI on `PATH`; the Python -`nemo-relay` package does not provide the executable. Follow the +Relay requires a separately installed NeMo Relay 0.7.x CLI on `PATH`; the +Python `nemo-relay` package does not provide the executable. Follow the [NeMo Relay installation instructions](https://docs.nvidia.com/nemo/fabric/getting-started/install#install-nemo-relay). Enable Relay with `FabricConfig.enable_relay(...)`. The adapter starts the diff --git a/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py b/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py index ae7529c30..f8c87c4e6 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py +++ b/adapters/common/src/nemo_fabric_adapters/common/relay_gateway.py @@ -20,8 +20,8 @@ RELAY_HEALTH_TIMEOUT_SECONDS = 10.0 RELAY_STOP_TIMEOUT_SECONDS = 5.0 RELAY_VERSION_TIMEOUT_SECONDS = 5.0 -RELAY_MINIMUM_VERSION = (0, 6, 0) -RELAY_MAXIMUM_VERSION = (0, 7, 0) +RELAY_MINIMUM_VERSION = (0, 7, 0) +RELAY_MAXIMUM_VERSION = (0, 8, 0) class RelayGatewayError(RuntimeError): @@ -96,9 +96,9 @@ def relay_cli_contract(executable: Path) -> RelayCliContract: raise RelayGatewayError( "unsupported NeMo Relay CLI version " f"{'.'.join(str(value) for value in version)}; " - "NeMo Fabric requires >=0.6.0,<0.7.0" + "NeMo Fabric requires >=0.7.0,<0.8.0" ) - return RelayCliContract(version=version, observability_version=2) + return RelayCliContract(version=version, observability_version=3) def wait_for_relay_gateway( diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index 96ae9c72f..6b5584c51 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -5,6 +5,7 @@ from __future__ import annotations +import copy import glob import json import os @@ -383,11 +384,110 @@ def collect_relay_artifacts(plugin_config: dict[str, Any]) -> list[dict[str, str return artifacts +_RELAY_V2_OTLP_ONLY_FIELDS = { + "attribute_mappings", + "capture_content", + "mark_exclude_names", + "mark_projection", + "semantic_selector", +} + + +def _relay_v3_otlp_endpoint( + section: dict[str, Any], *, endpoint_type: str, section_name: str +) -> dict[str, Any] | None: + if not section.get("enabled"): + return None + + endpoint = section.get("endpoint") + if not isinstance(endpoint, str) or not endpoint.strip(): + raise ValueError( + f"NeMo Relay observability config version 3 requires an endpoint " + f"for enabled {section_name} export" + ) + + unsupported = sorted(_RELAY_V2_OTLP_ONLY_FIELDS.intersection(section)) + if unsupported: + fields = ", ".join(unsupported) + raise ValueError( + f"NeMo Relay observability config version 3 cannot preserve " + f"{section_name} fields: {fields}" + ) + + return { + "type": endpoint_type, + **{ + key: value + for key, value in section.items() + if key not in {"enabled", "type"} + }, + } + + +def _relay_plugin_config_for_version( + plugin_config: dict[str, Any], observability_version: int +) -> dict[str, Any]: + if observability_version != 3: + raise ValueError( + f"unsupported NeMo Relay observability config version " + f"{observability_version}" + ) + + rendered = copy.deepcopy(plugin_config) + for component in rendered.get("components", []): + if component.get("kind") != "observability": + continue + config = component.get("config") + if not isinstance(config, dict): + continue + + config_version = config.get("version", 2) + if config_version == observability_version: + continue + if config_version != 2: + raise ValueError( + f"cannot render NeMo Relay observability config version " + f"{config_version} for version {observability_version}" + ) + + legacy_sections = ( + ("opentelemetry", "full"), + ("openinference", "openinference"), + ) + endpoints = [] + section_present = False + for section_name, endpoint_type in legacy_sections: + section = config.pop(section_name, None) + if section is None: + continue + section_present = True + if not isinstance(section, dict): + raise ValueError( + f"NeMo Relay {section_name} config must be an object" + ) + migrated = _relay_v3_otlp_endpoint( + section, + endpoint_type=endpoint_type, + section_name=section_name, + ) + if migrated is not None: + endpoints.append(migrated) + + config["version"] = 3 + if section_present: + config["opentelemetry"] = { + "enabled": bool(endpoints), + "endpoints": endpoints, + } + + return rendered + + def write_relay_configs( *, relay_config: dict[str, Any] | None = None, plugin_config: dict[str, Any] | None = None, - observability_version: int = 2, + observability_version: int = 3, ) -> tuple[Path | None, Path | None]: try: import tomli_w @@ -409,13 +509,12 @@ def write_relay_configs( relay_config_path.write_text(tomli_w.dumps(relay_config), encoding="utf-8") if plugin_config is not None: - if observability_version != 2: - raise ValueError( - f"unsupported NeMo Relay observability config version {observability_version}" - ) + rendered_plugin_config = _relay_plugin_config_for_version( + plugin_config, observability_version + ) plugin_config_path = config_dir / "plugins.toml" plugin_config_path.write_text( - tomli_w.dumps(plugin_config), + tomli_w.dumps(rendered_plugin_config), encoding="utf-8", ) diff --git a/docs/integrations/harness/claude.mdx b/docs/integrations/harness/claude.mdx index 75d0038e8..2ed9da0bf 100644 --- a/docs/integrations/harness/claude.mdx +++ b/docs/integrations/harness/claude.mdx @@ -170,7 +170,7 @@ gateway is scoped to that single invocation. Claude still resolves its credentia through the selected mode, and NeMo Fabric does not write authentication values to Relay configuration or artifacts. -NeMo Fabric supports the external NeMo Relay CLI from `0.6.0` up to, but not -including, `0.7.0`. The Python package named `nemo-relay` does not install this +NeMo Fabric supports the external NeMo Relay CLI from `0.7.0` up to, but not +including, `0.8.0`. The Python package named `nemo-relay` does not install this CLI. NeMo Fabric owns sidecar supervision, Claude configuration, and upstream selection. Relay owns the gateway transport and semantic observability pipeline. diff --git a/docs/integrations/harness/codex.mdx b/docs/integrations/harness/codex.mdx index 499fc86e1..4cc1f37a1 100644 --- a/docs/integrations/harness/codex.mdx +++ b/docs/integrations/harness/codex.mdx @@ -203,8 +203,8 @@ NeMo Fabric supplies runtime-scoped Relay configuration to the SDK. It does not copy the Codex credential store into Relay configuration or persist credentials in Relay artifacts. -NeMo Fabric supports the external NeMo Relay CLI from `0.6.0` up to, but not -including, `0.7.0`. The Python package named `nemo-relay` is a separate library +NeMo Fabric supports the external NeMo Relay CLI from `0.7.0` up to, but not +including, `0.8.0`. The Python package named `nemo-relay` is a separate library dependency and does not install the CLI. NeMo Fabric owns sidecar supervision and runtime-scoped SDK configuration. Relay owns gateway transport behavior, including decoding `Content-Encoding` before it constructs managed LLM events. diff --git a/docs/sdk/python.mdx b/docs/sdk/python.mdx index 06fed6656..bd74af86b 100644 --- a/docs/sdk/python.mdx +++ b/docs/sdk/python.mdx @@ -275,7 +275,7 @@ variant = review_agent_config(config, github_mcp=True, relay=True) ``` NeMo Relay observability is represented directly in the SDK config's top-level -`relay` block. ATOF uses NeMo Relay 0.6 file and stream sinks: +`relay` block. ATOF uses NeMo Relay file and stream sinks: ```python from nemo_fabric import ( @@ -505,7 +505,8 @@ Streaming has the following v0.1 constraints: warning check. Claude and Codex streaming use the NeMo Relay `nemo-relay` gateway CLI and require a -stream-sink-capable release from `0.6.0` up to, but not including, `0.7.0`. Follow +release from `0.7.0` up to, but not including, `0.8.0`. At launch, NeMo Fabric +translates its public observability model to Relay's version 3 configuration. Follow the [NeMo Relay CLI installation instructions](../getting-started/install.mdx#install-nemo-relay) to provision it. Hermes Agent and Deep Agents use their in-process NeMo Relay integrations. diff --git a/examples/code_review_agent/README.md b/examples/code_review_agent/README.md index b471ec978..f1e3bf972 100644 --- a/examples/code_review_agent/README.md +++ b/examples/code_review_agent/README.md @@ -74,7 +74,7 @@ The entrypoint exposes complete harness configs defined in Add `--relay` to any variant to enable the Relay ATOF and ATIF configuration: Relay requirements depend on the selected adapter. The Codex and Claude -adapters require an external `nemo-relay` CLI in the supported `0.6.x` range; +adapters require an external `nemo-relay` CLI in the supported 0.7.x range; the Python package named `nemo-relay` does not install the `nemo-relay` CLI tool. Hermes Agent and Deep Agents require the Relay Python package in their selected adapter environment. Refer to the diff --git a/examples/harbor/README.md b/examples/harbor/README.md index 942ebffac..049cd0c57 100644 --- a/examples/harbor/README.md +++ b/examples/harbor/README.md @@ -73,7 +73,7 @@ version `0.1.0`. | --- | --- | --- | | Harbor host | `nemo-fabric[harbor]==0.1.0` | Harbor CLI, `FabricAgent`, and typed `FabricConfig` construction | | Claude task without Relay | `nemo-fabric[claude]==0.1.0` | NeMo Fabric runner, Claude adapter, and supported Claude harness | -| Claude task with Relay | `nemo-fabric[claude]==0.1.0` plus a NeMo Relay 0.6.x CLI on `PATH` | NeMo Fabric runner, Claude adapter and harness, and the adapter-managed Relay gateway and hooks | +| Claude task with Relay | `nemo-fabric[claude]==0.1.0` plus a NeMo Relay 0.7.x CLI on `PATH` | NeMo Fabric runner, Claude adapter and harness, and the adapter-managed Relay gateway and hooks | | Hermes Agent task with Relay | `nemo-fabric[hermes-agent,relay]==0.1.0` | NeMo Fabric runner, Hermes Agent adapter and harness, and the NeMo Relay Python package | The `nemo-fabric` package installs the runtime. The `relay` extra installs the diff --git a/examples/harbor/swebench/README.md b/examples/harbor/swebench/README.md index 2210649f2..60f01ea64 100644 --- a/examples/harbor/swebench/README.md +++ b/examples/harbor/swebench/README.md @@ -31,11 +31,11 @@ export FABRIC_PACKAGE='nemo-fabric[claude,hermes-agent,relay]==0.1.0' export RUNS_DIR="$PWD/.tmp/harbor/fabric-swebench" curl -fsSL https://raw.githubusercontent.com/NVIDIA/NeMo-Relay/main/install.sh | - NEMO_RELAY_VERSION=0.6.0 sh -s -- \ + NEMO_RELAY_VERSION=0.7.0 sh -s -- \ --install-dir "$FABRIC_BUNDLE/.relay/bin" ``` -The curl command downloads and installs the standalone NeMo Relay 0.6.0 CLI tool +The curl command downloads and installs the standalone NeMo Relay 0.7.0 CLI tool and verifies its checksum. For other installation methods, refer to the [NeMo Relay installation instructions](../../../docs/getting-started/install.mdx#install-nemo-relay). diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index f63b9fd40..0df66944a 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -747,7 +747,7 @@ def test_write_relay_configs( assert tomllib.load(stream) == config -def test_write_relay_configs_preserves_current_cli_contract(tmp_path: Path): +def test_write_relay_configs_migrates_observability_for_relay_0_7(tmp_path: Path): os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(tmp_path / "relay.json") plugin_config = { "version": 1, @@ -763,21 +763,25 @@ def test_write_relay_configs_preserves_current_cli_contract(tmp_path: Path): { "type": "file", "output_directory": "/tmp/atof", - "filename": "events.jsonl", - "mode": "overwrite", - }, - { - "type": "stream", - "url": "https://example.test/events", - "transport": "http_post", - "headers": {"x-test": "value"}, - "header_env": {"authorization": "TOKEN"}, - "timeout_millis": 1000, - "field_name_policy": "replace_dots", - }, + } ], }, "atif": {"enabled": True, "output_directory": "/tmp/atif"}, + "opentelemetry": { + "enabled": True, + "endpoint": "http://localhost:4318/v1/traces", + "transport": "http_binary", + "headers": {"x-tenant-id": "demo"}, + "resource_attributes": {"deployment.environment": "test"}, + "service_name": "fabric", + "timeout_millis": 1000, + }, + "openinference": { + "enabled": True, + "endpoint": "http://localhost:6006/v1/traces", + "transport": "http_binary", + "service_name": "fabric", + }, }, } ], @@ -785,36 +789,80 @@ def test_write_relay_configs_preserves_current_cli_contract(tmp_path: Path): _, plugin_path = common_utils.write_relay_configs( plugin_config=plugin_config, - observability_version=2, + observability_version=3, ) assert plugin_path is not None with plugin_path.open("rb") as stream: rendered = tomllib.load(stream) observability = rendered["components"][0]["config"] - assert observability["version"] == 2 - assert observability["atof"] == { + assert observability["version"] == 3 + assert observability["atof"] == plugin_config["components"][0]["config"]["atof"] + assert observability["atif"] == plugin_config["components"][0]["config"]["atif"] + assert "openinference" not in observability + assert observability["opentelemetry"] == { "enabled": True, - "sinks": [ + "endpoints": [ { - "type": "file", - "output_directory": "/tmp/atof", - "filename": "events.jsonl", - "mode": "overwrite", + "type": "full", + "endpoint": "http://localhost:4318/v1/traces", + "transport": "http_binary", + "headers": {"x-tenant-id": "demo"}, + "resource_attributes": {"deployment.environment": "test"}, + "service_name": "fabric", + "timeout_millis": 1000, }, { - "type": "stream", - "url": "https://example.test/events", - "transport": "http_post", - "headers": {"x-test": "value"}, - "header_env": {"authorization": "TOKEN"}, - "timeout_millis": 1000, - "field_name_policy": "replace_dots", + "type": "openinference", + "endpoint": "http://localhost:6006/v1/traces", + "transport": "http_binary", + "service_name": "fabric", }, ], } - assert observability["atif"] == { - "enabled": True, - "output_directory": "/tmp/atif", + assert plugin_config["components"][0]["config"]["version"] == 2 + assert "openinference" in plugin_config["components"][0]["config"] + + +def test_relay_0_7_config_requires_enabled_otlp_endpoint(): + plugin_config = { + "components": [ + { + "kind": "observability", + "config": { + "version": 2, + "openinference": {"enabled": True}, + }, + } + ] } - assert rendered == plugin_config + + with pytest.raises( + ValueError, + match="version 3 requires an endpoint for enabled openinference export", + ): + common_utils._relay_plugin_config_for_version(plugin_config, 3) + + +def test_relay_0_7_config_rejects_removed_otlp_controls(): + plugin_config = { + "components": [ + { + "kind": "observability", + "config": { + "version": 2, + "opentelemetry": { + "enabled": True, + "endpoint": "http://localhost:4318/v1/traces", + "mark_projection": "tool", + }, + }, + } + ] + } + + with pytest.raises( + ValueError, + match="cannot preserve opentelemetry fields: mark_projection", + ): + common_utils._relay_plugin_config_for_version(plugin_config, 3) diff --git a/tests/adapters/test_adapters_common_relay_gateway.py b/tests/adapters/test_adapters_common_relay_gateway.py index 757e1da56..248216e5a 100644 --- a/tests/adapters/test_adapters_common_relay_gateway.py +++ b/tests/adapters/test_adapters_common_relay_gateway.py @@ -46,14 +46,18 @@ def test_resolve_relay_command_rejects_missing_executable(monkeypatch, tmp_path) @pytest.mark.parametrize( - ("output", "expected_version"), + ("output", "expected_version", "expected_observability_version"), [ - ("nemo-relay 0.6.0-alpha.20260714\n", (0, 6, 0)), - ("nemo-relay 0.6.99\n", (0, 6, 99)), + ("nemo-relay 0.7.0-alpha.20260805\n", (0, 7, 0), 3), + ("nemo-relay 0.7.99\n", (0, 7, 99), 3), ], ) def test_relay_cli_contract_selects_compatible_contract( - monkeypatch, tmp_path, output, expected_version + monkeypatch, + tmp_path, + output, + expected_version, + expected_observability_version, ): monkeypatch.setattr( relay_gateway.subprocess, @@ -65,11 +69,11 @@ def test_relay_cli_contract_selects_compatible_contract( tmp_path / "nemo-relay" ) == relay_gateway.RelayCliContract( version=expected_version, - observability_version=2, + observability_version=expected_observability_version, ) -@pytest.mark.parametrize("output", ["nemo-relay 0.5.9", "nemo-relay 0.7.0"]) +@pytest.mark.parametrize("output", ["nemo-relay 0.6.99", "nemo-relay 0.8.0"]) def test_relay_cli_contract_rejects_unsupported_version(monkeypatch, tmp_path, output): monkeypatch.setattr( relay_gateway.subprocess, @@ -79,7 +83,7 @@ def test_relay_cli_contract_rejects_unsupported_version(monkeypatch, tmp_path, o with pytest.raises( relay_gateway.RelayGatewayError, - match=r"NeMo Fabric requires >=0\.6\.0,<0\.7\.0", + match=r"NeMo Fabric requires >=0\.7\.0,<0\.8\.0", ): relay_gateway.relay_cli_contract(tmp_path / "nemo-relay") diff --git a/tests/adapters/test_claude_adapter.py b/tests/adapters/test_claude_adapter.py index f97c53d10..4dfcb7278 100644 --- a/tests/adapters/test_claude_adapter.py +++ b/tests/adapters/test_claude_adapter.py @@ -328,7 +328,7 @@ def test_prepare_claude_relay_writes_gateway_config_and_complete_hook_plugin( "relay_cli_contract", MagicMock( return_value=adapter.relay_gateway.RelayCliContract( - version=(0, 6, 0), observability_version=2 + version=(0, 7, 0), observability_version=3 ) ), ) @@ -404,7 +404,7 @@ def test_build_options_adds_relay_plugin_and_gateway_environment( "relay_cli_contract", MagicMock( return_value=adapter.relay_gateway.RelayCliContract( - version=(0, 6, 0), observability_version=2 + version=(0, 7, 0), observability_version=3 ) ), ) diff --git a/tests/adapters/test_codex_adapter.py b/tests/adapters/test_codex_adapter.py index b17a191c8..cb9184f05 100644 --- a/tests/adapters/test_codex_adapter.py +++ b/tests/adapters/test_codex_adapter.py @@ -812,7 +812,7 @@ def test_prepare_relay_reuses_one_resolved_executable( resolve = MagicMock(return_value=executable) contract = MagicMock( return_value=adapter.relay_gateway.RelayCliContract( - version=(0, 6, 0), observability_version=2 + version=(0, 7, 0), observability_version=3 ) ) write = MagicMock(return_value=(config_path, plugin_path)) @@ -840,7 +840,7 @@ def test_prepare_relay_reuses_one_resolved_executable( write.assert_called_once_with( relay_config={}, plugin_config={"version": 1, "components": []}, - observability_version=2, + observability_version=3, ) diff --git a/tests/e2e/test_claude.py b/tests/e2e/test_claude.py index ddd681ada..8597cbf04 100644 --- a/tests/e2e/test_claude.py +++ b/tests/e2e/test_claude.py @@ -47,7 +47,7 @@ def write_mock_relay_gateway(path: Path, log_path: Path) -> None: args = sys.argv[1:] if args == ["--version"]: - print("nemo-relay 0.6.0") + print("nemo-relay 0.7.0") raise SystemExit(0) Path({str(log_path)!r}).write_text(json.dumps(args), encoding="utf-8") bind = args[args.index("--bind") + 1] From 4c33cbfcf58aebbe7f3a54c21fd76999a39bfb51 Mon Sep 17 00:00:00 2001 From: Ajay Thorve Date: Thu, 6 Aug 2026 00:23:50 -0700 Subject: [PATCH 2/2] test: preserve Relay stream sink coverage Signed-off-by: Ajay Thorve --- .../src/nemo_fabric_adapters/common/utils.py | 2 ++ tests/adapters/test_adapaters_common_utils.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/adapters/common/src/nemo_fabric_adapters/common/utils.py b/adapters/common/src/nemo_fabric_adapters/common/utils.py index 6b5584c51..b08104edc 100644 --- a/adapters/common/src/nemo_fabric_adapters/common/utils.py +++ b/adapters/common/src/nemo_fabric_adapters/common/utils.py @@ -427,6 +427,8 @@ def _relay_v3_otlp_endpoint( def _relay_plugin_config_for_version( plugin_config: dict[str, Any], observability_version: int ) -> dict[str, Any]: + """Render Fabric's public Relay config for one CLI without mutating it.""" + if observability_version != 3: raise ValueError( f"unsupported NeMo Relay observability config version " diff --git a/tests/adapters/test_adapaters_common_utils.py b/tests/adapters/test_adapaters_common_utils.py index 0df66944a..2bfd6fd25 100644 --- a/tests/adapters/test_adapaters_common_utils.py +++ b/tests/adapters/test_adapaters_common_utils.py @@ -747,7 +747,9 @@ def test_write_relay_configs( assert tomllib.load(stream) == config -def test_write_relay_configs_migrates_observability_for_relay_0_7(tmp_path: Path): +def test_write_relay_configs_migrates_otlp_and_preserves_sinks_for_relay_0_7( + tmp_path: Path, +): os.environ["FABRIC_RELAY_CONFIG_PATH"] = str(tmp_path / "relay.json") plugin_config = { "version": 1, @@ -763,7 +765,16 @@ def test_write_relay_configs_migrates_observability_for_relay_0_7(tmp_path: Path { "type": "file", "output_directory": "/tmp/atof", - } + }, + { + "type": "stream", + "url": "https://example.test/events", + "transport": "http_post", + "headers": {"x-test": "value"}, + "header_env": {"authorization": "TOKEN"}, + "timeout_millis": 1000, + "field_name_policy": "replace_dots", + }, ], }, "atif": {"enabled": True, "output_directory": "/tmp/atif"},