From 957d37f51f0ba294bcc905dd6e66f44a43722d38 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 10 Jun 2026 04:08:17 +0000 Subject: [PATCH 1/2] fix: merge egress routes across extends --- bot_bottle/manifest_extends.py | 28 +++++++++++++-- tests/unit/test_manifest_extends.py | 54 ++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/bot_bottle/manifest_extends.py b/bot_bottle/manifest_extends.py index 37176f3..a4a2920 100644 --- a/bot_bottle/manifest_extends.py +++ b/bot_bottle/manifest_extends.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from .manifest import ManifestBottle, ManifestGitEntry + from .manifest_egress import ManifestEgressConfig def resolve_bottles(raws: dict[str, dict[str, object]]) -> dict[str, ManifestBottle]: @@ -99,9 +100,16 @@ def _merge_bottles( else: merged_git = parent.git - # Presence-driven full-replace for the remaining list-valued + - # scalar fields. - merged_egress = child.egress if "egress" in child_raw else parent.egress + # egress.routes: missing means inherit; otherwise parent and child + # route lists concatenate. Other egress scalar fields remain + # presence-driven overlays. + merged_egress = ( + _merge_egress(parent.egress, child.egress, child_raw) + if "egress" in child_raw + else parent.egress + ) + + # Presence-driven full-replace for the remaining scalar fields. merged_agent_provider = ( child.agent_provider if "agent_provider" in child_raw @@ -140,3 +148,17 @@ def _merge_git_remotes( for entry in child: by_host[entry.UpstreamHost] = entry return tuple(by_host.values()) + + +def _merge_egress( + parent: ManifestEgressConfig, + child: ManifestEgressConfig, + child_raw: dict[str, object], +) -> ManifestEgressConfig: + from .manifest_egress import ManifestEgressConfig + from .manifest_util import as_json_object + + child_egress_raw = as_json_object(child_raw.get("egress"), "child egress") + routes = parent.routes + child.routes + log = child.Log if "log" in child_egress_raw else parent.Log + return ManifestEgressConfig(routes=routes, Log=log) diff --git a/tests/unit/test_manifest_extends.py b/tests/unit/test_manifest_extends.py index b1a20ad..85b9590 100644 --- a/tests/unit/test_manifest_extends.py +++ b/tests/unit/test_manifest_extends.py @@ -173,10 +173,10 @@ class TestExtendsGitMerge(unittest.TestCase): self.assertEqual("Child", m.bottles["child"].git_user.name) -class TestExtendsListsFullReplace(unittest.TestCase): - """egress: remains full-replace when the child declares it.""" +class TestExtendsEgressMerge(unittest.TestCase): + """egress.routes merges; egress.log overlays only when declared.""" - def test_child_egress_replaces_parent_entirely(self): + def test_child_egress_routes_merge_with_parent(self): m = _build( base={"egress": {"routes": [{"host": "a.example.com"}]}}, child={ @@ -185,7 +185,7 @@ class TestExtendsListsFullReplace(unittest.TestCase): }, ) hosts = [r.Host for r in m.bottles["child"].egress.routes] - self.assertEqual(["b.example.com"], hosts) + self.assertEqual(["a.example.com", "b.example.com"], hosts) def test_child_omits_egress_inherits(self): m = _build( @@ -195,6 +195,52 @@ class TestExtendsListsFullReplace(unittest.TestCase): hosts = [r.Host for r in m.bottles["child"].egress.routes] self.assertEqual(["a.example.com"], hosts) + def test_child_egress_log_inherits_parent_routes(self): + m = _build( + base={ + "egress": { + "routes": [{"host": "a.example.com"}], + "log": 1, + }, + }, + child={"extends": "base", "egress": {"log": 2}}, + ) + child = m.bottles["child"].egress + self.assertEqual(["a.example.com"], [r.Host for r in child.routes]) + self.assertEqual(2, child.Log) + + def test_child_egress_routes_inherit_parent_log_when_omitted(self): + m = _build( + base={ + "egress": { + "routes": [{"host": "a.example.com"}], + "log": 1, + }, + }, + child={ + "extends": "base", + "egress": {"routes": [{"host": "b.example.com"}]}, + }, + ) + child = m.bottles["child"].egress + self.assertEqual( + ["a.example.com", "b.example.com"], + [r.Host for r in child.routes], + ) + self.assertEqual(1, child.Log) + + def test_duplicate_host_across_parent_and_child_dies(self): + msg = _error_message( + _build, + base={"egress": {"routes": [{"host": "a.example.com"}]}}, + child={ + "extends": "base", + "egress": {"routes": [{"host": "A.EXAMPLE.COM"}]}, + }, + ) + self.assertIn("duplicate host", msg) + self.assertIn("A.EXAMPLE.COM", msg) + class TestExtendsGitUserOverlay(unittest.TestCase): """git-gate.user: per-field overlay. Each non-empty field on child -- 2.52.0 From 019efab8043659d5acb87fc5d5e9b25c0f4935c7 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 10 Jun 2026 04:30:13 +0000 Subject: [PATCH 2/2] fix: narrow pi numeric settings types --- bot_bottle/contrib/pi/agent_provider.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/bot_bottle/contrib/pi/agent_provider.py b/bot_bottle/contrib/pi/agent_provider.py index dfc9120..874e82f 100644 --- a/bot_bottle/contrib/pi/agent_provider.py +++ b/bot_bottle/contrib/pi/agent_provider.py @@ -77,6 +77,19 @@ def _settings_value( return default if value is None else value +def _settings_int( + settings: dict[str, object], + key: str, + default: int, +) -> int: + value = _settings_value(settings, key, default) + if isinstance(value, bool): + return default + if isinstance(value, (int, str)): + return int(value) + return default + + def _pi_models_json( settings: dict[str, object], ) -> tuple[dict[str, object], str, str, list[str], str]: @@ -98,12 +111,10 @@ def _pi_models_json( max_tokens_field = str( _settings_value(settings, "max_tokens_field", "max_tokens") ) - context_window = int(_settings_value( + context_window = _settings_int( settings, "context_window", _DEFAULT_CONTEXT_WINDOW, - )) - max_tokens = int(_settings_value( - settings, "max_tokens", _DEFAULT_MAX_TOKENS, - )) + ) + max_tokens = _settings_int(settings, "max_tokens", _DEFAULT_MAX_TOKENS) input_context_window = max(1, context_window - max_tokens) provider: dict[str, object] = { "baseUrl": base_url, -- 2.52.0