diff --git a/scripts/ci/check-scopes.json b/scripts/ci/check-scopes.json index a368e08a4..50f15a3b0 100644 --- a/scripts/ci/check-scopes.json +++ b/scripts/ci/check-scopes.json @@ -28,6 +28,19 @@ "NOTICE" ], "rules": [ + { + "paths": [ + "docs/internals/architecture/appserver/app-protocol-*.schema.json" + ], + "checks": [ + "gui", + "architecture", + "coding", + "apphost", + "appservice" + ], + "reason": "App Protocol shared contract and direct consumers" + }, { "paths": ["gui/**", "scripts/gui/**"], "checks": ["gui"], diff --git a/tests/appserver/test_connection.py b/tests/appserver/test_connection.py index ce34b3d18..b2f51c357 100644 --- a/tests/appserver/test_connection.py +++ b/tests/appserver/test_connection.py @@ -38,6 +38,8 @@ SessionEventKindV1, SessionEventV1, SessionIdentityV1, + SessionModelSelectV1, + SessionModelsV1, SessionOpenSpecV1, SessionScopeV1, SessionSnapshotRequestV1, @@ -150,6 +152,7 @@ async def scenario() -> None: mux = MuxSpaceV1("mux", "dev", 1, ()) snapshot = SessionSnapshotV1(identity, "Coding", 0, 0, False) attachment = MuxAttachmentV1("a", mux, 1, ()) + models = SessionModelsV1(None, ()) event = AttachmentEventV1( "a", "m", SessionEventV1("s", 1, SessionEventKindV1.TURN_COMPLETED) ) @@ -172,6 +175,16 @@ async def scenario() -> None: ), ("close_member", MuxMemberCloseV1(selector, "m"), mux), ("snapshot_session", SessionSnapshotRequestV1("a", 1, "m"), snapshot), + ( + "list_session_models", + SessionSnapshotRequestV1("a", 1, "m"), + models, + ), + ( + "select_session_model", + SessionModelSelectV1("a", 1, "m", "provider:model"), + models, + ), ("start_turn", TurnTextV1("a", 1, "m", "start"), AckV1()), ("steer_turn", TurnTextV1("a", 1, "m", "steer"), AckV1()), ("follow_up_turn", TurnTextV1("a", 1, "m", "follow up"), AckV1()), diff --git a/tests/architecture/test_hosted_application_g11.py b/tests/architecture/test_hosted_application_g11.py index 215cccfe5..c5b42e8f0 100644 --- a/tests/architecture/test_hosted_application_g11.py +++ b/tests/architecture/test_hosted_application_g11.py @@ -245,11 +245,13 @@ def test_g11_package_budgets_keep_new_owners_reviewable() -> None: # 271 lines to this exact group; all protocol modules still count. # See hosted-session-workflow-g17.md, Reviewability Budget Supplement. "appserver": 2_100, - "appservice-core": 1_500, + # Attachment-scoped model discovery and selection extend the existing + # AppService authority boundary without exposing provider credentials. + "appservice-core": 1_550, "appservice-continuity": 1_250, - # Optional execution adds omission metadata and a synchronous projection - # seam; its two owners have a separate budget in the Wave A contract. - "coding-adapter": 420, + # Optional execution plus the redacted, session-local model control + # bridge remain isolated in the Coding AppService adapter. + "coding-adapter": 470, "harnesstui-mux": 600, } for name, paths in groups.items(): diff --git a/tests/ci/test_change_gates.py b/tests/ci/test_change_gates.py index 1d2a6376a..764b7d09e 100644 --- a/tests/ci/test_change_gates.py +++ b/tests/ci/test_change_gates.py @@ -30,6 +30,42 @@ def load_script(name: str): class ScopeTests(unittest.TestCase): + def test_app_protocol_schema_selects_only_direct_consumers(self): + path = "docs/internals/architecture/appserver/app-protocol-v1.schema.json" + actual = self.selected(path) + + self.assertEqual( + actual, + { + "docs", + "gui", + "architecture", + "coding", + "apphost", + "appservice", + "host_runtime", + }, + ) + self.assertFalse( + { + "ai", + "agent", + "harness", + "harness_native", + "harnesstui", + "coding_ui", + "tui_unit", + "tui_playback", + "tui_native", + "hosting", + "windows_shell", + "install", + "lsp", + "foundation", + } + & actual + ) + def test_gui_code_and_tooling_select_only_gui_and_docs(self): for path in ("gui/src/App.tsx", "gui/src-tauri/Cargo.lock", "gui/pnpm-lock.yaml", "scripts/gui/run.mjs"): with self.subTest(path=path):