From 5dee4ce99a237282c11cbf584100bbbd76dfbd39 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 14:59:14 -0400 Subject: [PATCH 1/6] fix(socket): advertise dispatched Cloud methods --- Sources/TerminalController+Capabilities.swift | 34 ++++++++++++++++++ ...erminalControllerSocketSecurityTests.swift | 36 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/Sources/TerminalController+Capabilities.swift b/Sources/TerminalController+Capabilities.swift index ad6829995dfe..c3905f393b4e 100644 --- a/Sources/TerminalController+Capabilities.swift +++ b/Sources/TerminalController+Capabilities.swift @@ -117,11 +117,18 @@ extension TerminalController { "vm.stats", "vm.resize", "vm.rename", + "vm.pause", + "vm.resume", "vm.snapshot", + "vm.snapshot_list", + "vm.snapshot_delete", "vm.fork", "vm.restore", "vm.destroy", "vm.exec", + "vm.env_set", + "vm.file_put", + "vm.reflection", "vm.open_port", "vm.attach_info", "vm.cmux_remote_info", @@ -143,6 +150,8 @@ extension TerminalController { "vm.terminal_write", "vm.terminal_read", "vm.terminal_wait", + "vm.terminal_output", + "vm.terminal_wait_exit", "vm.desktop_open", "vm.port_open", "vm.link_socket", @@ -158,6 +167,7 @@ extension TerminalController { "current.list", "surface.project", "surface.new_terminal", + "surface.ssh_session_attach.resolve", "aiAccounts.list", "aiAccounts.upload", "aiAccounts.remove", @@ -190,7 +200,20 @@ extension TerminalController { "workspace.select", "workspace.current", "workspace.close", + "workspace.move", "workspace.move_to_window", + "workspace.status.get", + "workspace.status.set", + "workspace.status.cycle", + "workspace.todo.list", + "workspace.todo.add", + "workspace.todo.edit", + "workspace.todo.remove", + "workspace.todo.move", + "workspace.todo.open", + "workspace.todo.set", + "workspace.todo.set_state", + "workspace.todo.clear", "workspace.reorder", "workspace.reorder_many", "workspace.prompt_submit", @@ -312,6 +335,10 @@ extension TerminalController { "notification.mark_read", "notification.open", "notification.jump_to_unread", + "notification.feed.list", + "notification.feed.mark_read", + "notification.feed.mark_unread", + "notification.feed.mark_all_read", "app.focus_override.set", "app.simulate_active", "file.open", @@ -381,6 +408,13 @@ extension TerminalController { "browser.storage.get", "browser.storage.set", "browser.storage.clear", + "browser.profiles.list", + "browser.profiles.create", + "browser.profiles.rename", + "browser.profiles.clear", + "browser.profiles.delete", + "browser.import.cookies", + "browser.import.dialog", "browser.tab.new", "browser.tab.list", "browser.tab.switch", diff --git a/cmuxTests/TerminalControllerSocketSecurityTests.swift b/cmuxTests/TerminalControllerSocketSecurityTests.swift index bbfcf927aed2..d3900d2e3ebf 100644 --- a/cmuxTests/TerminalControllerSocketSecurityTests.swift +++ b/cmuxTests/TerminalControllerSocketSecurityTests.swift @@ -1179,6 +1179,42 @@ final class TerminalControllerSocketSecurityTests { "mobile.panel.artifact.thumbnail", "mobile.events.subscribe", "mobile.events.unsubscribe", + // Cloud VM methods used by the CLI must stay discoverable so + // capability-aware agents do not reject valid operations. + "vm.env_set", + "vm.file_put", + "vm.pause", + "vm.resume", + "vm.reflection", + "vm.snapshot_list", + "vm.snapshot_delete", + "vm.terminal_output", + "vm.terminal_wait_exit", + "browser.profiles.list", + "browser.profiles.create", + "browser.profiles.rename", + "browser.profiles.clear", + "browser.profiles.delete", + "browser.import.cookies", + "browser.import.dialog", + "workspace.move", + "workspace.status.get", + "workspace.status.set", + "workspace.status.cycle", + "workspace.todo.list", + "workspace.todo.add", + "workspace.todo.edit", + "workspace.todo.remove", + "workspace.todo.move", + "workspace.todo.open", + "workspace.todo.set", + "workspace.todo.set_state", + "workspace.todo.clear", + "notification.feed.list", + "notification.feed.mark_read", + "notification.feed.mark_unread", + "notification.feed.mark_all_read", + "surface.ssh_session_attach.resolve", ] XCTAssertTrue( expectedMethods.isSubset(of: advertisedMethods), From fe14e7416434c4fece87db4083e206ebd7d424f9 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 15:18:32 -0400 Subject: [PATCH 2/6] test(socket): guard Cloud capability parity --- scripts/check-socket-capabilities.py | 91 ++++++++++++++++++++++++ tests/test_ci_socket_capability_guard.py | 22 ++++++ 2 files changed, 113 insertions(+) create mode 100644 scripts/check-socket-capabilities.py create mode 100644 tests/test_ci_socket_capability_guard.py diff --git a/scripts/check-socket-capabilities.py b/scripts/check-socket-capabilities.py new file mode 100644 index 000000000000..bc603cdbae1c --- /dev/null +++ b/scripts/check-socket-capabilities.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Check public socket dispatcher cases against v2 capability discovery. + +The public namespaces covered here are the ones consumed by the Cloud/CLI +contract. Internal debug, mobile-host, and simulator dispatchers have separate +authorization surfaces and are intentionally outside this check. +""" + +import argparse +import pathlib +import re +import sys + +PUBLIC_PREFIXES = ( + "vm.", + "browser.profiles.", + "browser.import.", + "workspace.move", + "workspace.status.", + "workspace.todo.", + "notification.feed.", + "surface.ssh_session_attach.", +) + + +def switch_cases(source): + methods = set() + for match in re.finditer(r"switch\s+(?:request\.)?method\b", source): + opening = source.find("{", match.end()) + if opening < 0: + continue + depth = 0 + closing = None + for index in range(opening, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + closing = index + break + if closing is None: + continue + methods.update(re.findall(r'case\s+"([A-Za-z0-9_.-]+)"', source[opening:closing])) + return methods + + +def capability_methods(source): + anchor = source.find("var methods: [String] = [") + if anchor < 0: + raise ValueError("v2 capability method list is missing") + closing = source.find("\n ]", anchor) + if closing < 0: + raise ValueError("v2 capability method list is unclosed") + return set(re.findall(r'"([A-Za-z0-9_.-]+)"', source[anchor:closing])) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=pathlib.Path(__file__).resolve().parents[1]) + args = parser.parse_args(argv) + root = pathlib.Path(args.root).resolve() + capability_path = root / "Sources/TerminalController+Capabilities.swift" + capability_text = capability_path.read_text(encoding="utf-8") + advertised = capability_methods(capability_text) + + dispatched = set() + for relative in ( + "Sources/TerminalController.swift", + "Sources/Cloud", + "Sources/Surfaces", + "Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator", + ): + path = root / relative + paths = [path] if path.is_file() else sorted(path.rglob("*.swift")) + for source_path in paths: + dispatched.update(switch_cases(source_path.read_text(encoding="utf-8"))) + + public = {method for method in dispatched if method.startswith(PUBLIC_PREFIXES)} + missing = sorted(public - advertised) + if missing: + print("Missing advertised capabilities:", file=sys.stderr) + for method in missing: + print(" - " + method, file=sys.stderr) + return 1 + print("socket capability parity: ok ({0} public dispatcher methods)".format(len(public))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_ci_socket_capability_guard.py b/tests/test_ci_socket_capability_guard.py new file mode 100644 index 000000000000..eacf6672dc08 --- /dev/null +++ b/tests/test_ci_socket_capability_guard.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Regression guard for public socket capability discovery.""" + +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +GUARD = os.path.join(ROOT, "scripts", "check-socket-capabilities.py") + + +def test_public_dispatcher_methods_are_advertised(): + result = subprocess.run( + [sys.executable, GUARD, "--root", ROOT], + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + assert result.returncode == 0, result.stdout + assert "socket capability parity: ok" in result.stdout From 669621a3705a664010a015c38af3f3ddf3dce118 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 15:55:08 -0400 Subject: [PATCH 3/6] fix(socket): close capability parity review gaps --- .github/workflows/ci-guards.yml | 6 + Sources/TerminalController+Capabilities.swift | 18 +- scripts/check-socket-capabilities.py | 173 ++++++++++++++++-- tests/test-execution.toml | 4 + tests/test_ci_socket_capability_guard.py | 51 ++++++ 5 files changed, 231 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci-guards.yml b/.github/workflows/ci-guards.yml index 1abccc1dfed5..020f9a550ed5 100644 --- a/.github/workflows/ci-guards.yml +++ b/.github/workflows/ci-guards.yml @@ -1207,6 +1207,12 @@ jobs: python3 tests/test_ci_cli_contract_verb_guard.py python3 scripts/check-cli-contract-verbs.py + - name: Validate socket capability parity + if: ${{ matrix.group == 'quality-determinism' }} + run: | + python3 tests/test_ci_socket_capability_guard.py + python3 scripts/check-socket-capabilities.py + - name: Validate test determinism gate if: ${{ matrix.group == 'quality-determinism' }} run: | diff --git a/Sources/TerminalController+Capabilities.swift b/Sources/TerminalController+Capabilities.swift index c3905f393b4e..d24bee09cd8b 100644 --- a/Sources/TerminalController+Capabilities.swift +++ b/Sources/TerminalController+Capabilities.swift @@ -64,9 +64,14 @@ extension TerminalController { "mobile.panel.artifact.stat", "mobile.panel.artifact.thumbnail", "mobile.workspace.list", + "mobile.workspace.changes.files", + "mobile.workspace.changes.file_diff", + "mobile.workspace.changes.file_stat", + "mobile.workspace.changes.file_fetch", "mobile.terminal.create", "mobile.terminal.input", "mobile.terminal.paste", + "mobile.terminal.paste_image", "mobile.terminal.replay", "mobile.browser.list", "mobile.browser.create", @@ -83,12 +88,15 @@ extension TerminalController { "mobile.browser.back", "mobile.browser.forward", "mobile.browser.reload", - "mobile.terminal.viewport", "mobile.events.subscribe", "mobile.events.unsubscribe", + "mobile.terminal.viewport", "mobile.terminal.scroll", "mobile.terminal.mouse", + "mobile.events.subscribe", "mobile.events.unsubscribe", + "mobile.status.cycle", "terminal.create", "terminal.input", "terminal.paste", + "terminal.paste_image", "terminal.replay", - "terminal.viewport", + "terminal.viewport", "terminal.scroll", "terminal.mouse", "auth.login", "auth.status", "auth.sign_in_url", @@ -237,6 +245,7 @@ extension TerminalController { "workspace.group.set_icon", "workspace.group.move", "workspace.group.focus", + "workspace.group.action", "workspace.action", "extension.sidebar.snapshot", "workspace.next", @@ -254,6 +263,11 @@ extension TerminalController { "workspace.remote.terminal_session_launching", "workspace.remote.terminal_session_connected", "workspace.remote.terminal_session_end", "remote.tmux.sessions", "remote.tmux.attach", "remote.tmux.detach", "remote.tmux.state", "remote.tmux.mirror", "remote.tmux.window", "remote.tmux.pane_grids", "remote.tmux.pane_surfaces", + "remotes.list", "remotes.add", "remotes.remove", + "layout.save", "layout.list", "layout.get", "layout.open", "layout.delete", + "canvas.align", "canvas.break", "canvas.info", "canvas.join", "canvas.new_pane", + "canvas.overview", "canvas.reveal", "canvas.select_tab", "canvas.set_frame", + "canvas.set_mode", "canvas.set_viewport", "canvas.zoom", "session.restore_previous", "session.agent_recovery.list", "session.agent_recovery.restore", diff --git a/scripts/check-socket-capabilities.py b/scripts/check-socket-capabilities.py index bc603cdbae1c..61f9e2da1ddb 100644 --- a/scripts/check-socket-capabilities.py +++ b/scripts/check-socket-capabilities.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Check public socket dispatcher cases against v2 capability discovery. +"""Check socket dispatcher cases against v2 capability discovery. -The public namespaces covered here are the ones consumed by the Cloud/CLI -contract. Internal debug, mobile-host, and simulator dispatchers have separate -authorization surfaces and are intentionally outside this check. +The dispatcher contains a few deliberately private test, debug, and host +bridges. They are listed explicitly below; every other dispatcher method must +be discoverable so a new public socket method cannot silently bypass the +capability catalog. """ import argparse @@ -11,16 +12,129 @@ import re import sys -PUBLIC_PREFIXES = ( - "vm.", - "browser.profiles.", - "browser.import.", - "workspace.move", - "workspace.status.", - "workspace.todo.", - "notification.feed.", - "surface.ssh_session_attach.", -) +# These methods are internal host/debug/test surfaces, not public socket API. +# Keep the list exact: a new dispatcher method outside this set must be added to +# v2Capabilities() or this guard fails. +INTENTIONALLY_UNADVERTISED_METHODS = frozenset(""" +agent.hook.barrier +agent.hook.enqueue +chat.sessions.dump +debug.app.activate +debug.bonsplit_underflow.count +debug.bonsplit_underflow.reset +debug.browser.address_bar_focused +debug.browser.favicon +debug.canvas.command_scroll_hint +debug.cloudtree.gallery +debug.cloudtree.spacing +debug.command_palette.rename_input.delete_backward +debug.command_palette.rename_input.interact +debug.command_palette.rename_input.select_all +debug.command_palette.rename_input.selection +debug.command_palette.rename_tab.open +debug.command_palette.results +debug.command_palette.selection +debug.command_palette.toggle +debug.command_palette.visible +debug.empty_panel.count +debug.empty_panel.reset +debug.flash.count +debug.flash.reset +debug.layout +debug.mobile.transport.disconnect +debug.mobile.transport.reconnect_loop +debug.notification.emit +debug.notification.focus +debug.notification.mode +debug.notification.status +debug.panel_snapshot +debug.panel_snapshot.reset +debug.portal.stats +debug.pro_welcome_checklist.show +debug.right_sidebar.focus +debug.session_snapshot_benchmark +debug.session_snapshot_seed_scrollback +debug.shortcut.set +debug.shortcut.simulate +debug.sidebar.simulate_drag +debug.sidebar.visible +debug.terminal.is_focused +debug.terminal.read_text +debug.terminal.render_stats +debug.terminal.simulate_file_drop +debug.textbox.inline_fixture +debug.textbox.interact +debug.type +debug.window.screenshot +debug.workspace_todo.checklist_add_field +dogfood.feedback.submit +feed.text +mobile.dev_stack_auth.configure +mobile.directory.list +mobile.directory.search +mobile.rpc.methods +mobile.status.set +mobile.surface.focus +mobile.sync.fetch +mobile.terminal.close +mobile.terminal.mouse +mobile.terminal.participant.disconnect +mobile.terminal.paste_image +mobile.terminal.reattach +mobile.terminal.rename +mobile.terminal.scroll +mobile.terminal.size_policy.set +mobile.workspace.changes.summary +notification.reconcile +phone_push.settings.update +phone_push.status.get +phone_push.test +project.get_state +project.open +project.set_configuration +project.set_scheme +project.set_selected_file +project.set_selected_target +project.set_settings_filter +project.set_tab +remote.tmux.root_frames +remote.tmux.sizing_settled +remote.tmux.test_exec +remote.tmux.test_perturb_divider +remote.tmux.test_set_frame +sidebar.custom.reload +sidebar.custom.select +sidebar.custom.validate +simulator.accessibility +simulator.button +simulator.camera.configure +simulator.camera.mirror +simulator.camera.status +simulator.camera.switch +simulator.context +simulator.core_animation +simulator.event_log +simulator.foreground +simulator.gesture +simulator.memory_warning +simulator.permissions.read +simulator.permissions.set +simulator.prepare_screenshot +simulator.recover +simulator.rotate +simulator.select_device +simulator.swipe +simulator.tap +simulator.tools +simulator.type +simulator.ui.set +simulator.ui.status +simulator.web_inspector.attach +simulator.web_inspector.highlight +simulator.web_inspector.release +simulator.web_inspector.send +simulator.web_inspector.targets +""".split()) def switch_cases(source): @@ -41,18 +155,31 @@ def switch_cases(source): break if closing is None: continue - methods.update(re.findall(r'case\s+"([A-Za-z0-9_.-]+)"', source[opening:closing])) + switch_body = source[opening:closing] + for case in re.finditer( + r'case\s+((?:"[A-Za-z0-9_.-]+"\s*,?\s*)+)', + switch_body, + flags=re.MULTILINE, + ): + methods.update(re.findall(r'"([A-Za-z0-9_.-]+)"', case.group(1))) return methods -def capability_methods(source): +def capability_methods(source, simulator_source=""): anchor = source.find("var methods: [String] = [") if anchor < 0: raise ValueError("v2 capability method list is missing") closing = source.find("\n ]", anchor) if closing < 0: raise ValueError("v2 capability method list is unclosed") - return set(re.findall(r'"([A-Za-z0-9_.-]+)"', source[anchor:closing])) + methods = set(re.findall(r'"([A-Za-z0-9_.-]+)"', source[anchor:closing])) + # A small number of capabilities are appended conditionally after the + # base list. Keep those additions in the parity check too, otherwise the + # guard would report a false gap for methods that discovery really emits. + for appended in re.findall(r'for method in \[([^\]]+)\]', source, flags=re.DOTALL): + methods.update(re.findall(r'"([A-Za-z0-9_.-]+)"', appended)) + methods.update(re.findall(r'"([A-Za-z0-9_.-]+)"', simulator_source)) + return methods def main(argv=None): @@ -62,7 +189,12 @@ def main(argv=None): root = pathlib.Path(args.root).resolve() capability_path = root / "Sources/TerminalController+Capabilities.swift" capability_text = capability_path.read_text(encoding="utf-8") - advertised = capability_methods(capability_text) + simulator_path = root / ( + "Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/" + "ControlCommandExecutionPolicy+Simulator.swift" + ) + simulator_text = simulator_path.read_text(encoding="utf-8") if simulator_path.exists() else "" + advertised = capability_methods(capability_text, simulator_text) dispatched = set() for relative in ( @@ -76,7 +208,10 @@ def main(argv=None): for source_path in paths: dispatched.update(switch_cases(source_path.read_text(encoding="utf-8"))) - public = {method for method in dispatched if method.startswith(PUBLIC_PREFIXES)} + public = dispatched - INTENTIONALLY_UNADVERTISED_METHODS + if not public: + print("socket capability parity: no public dispatcher methods found", file=sys.stderr) + return 1 missing = sorted(public - advertised) if missing: print("Missing advertised capabilities:", file=sys.stderr) diff --git a/tests/test-execution.toml b/tests/test-execution.toml index ce81b66eeceb..a5a6b83b3531 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -633,6 +633,10 @@ lane = "linux-guard" path = "tests/test_ci_cli_contract_verb_guard.py" lane = "linux-guard" +[[test]] +path = "tests/test_ci_socket_capability_guard.py" +lane = "linux-guard" + [[test]] path = "tests/test_ci_source_lint_guard_structure.py" lane = "linux-guard" diff --git a/tests/test_ci_socket_capability_guard.py b/tests/test_ci_socket_capability_guard.py index eacf6672dc08..7fe3555c6854 100644 --- a/tests/test_ci_socket_capability_guard.py +++ b/tests/test_ci_socket_capability_guard.py @@ -1,15 +1,25 @@ #!/usr/bin/env python3 """Regression guard for public socket capability discovery.""" +import importlib.util import os import subprocess import sys +import tempfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) GUARD = os.path.join(ROOT, "scripts", "check-socket-capabilities.py") +def load_guard(): + spec = importlib.util.spec_from_file_location("socket_capability_guard", GUARD) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def test_public_dispatcher_methods_are_advertised(): result = subprocess.run( [sys.executable, GUARD, "--root", ROOT], @@ -20,3 +30,44 @@ def test_public_dispatcher_methods_are_advertised(): ) assert result.returncode == 0, result.stdout assert "socket capability parity: ok" in result.stdout + count = int(result.stdout.split("(", 1)[1].split(" ", 1)[0]) + assert count > 0 + + +def test_compound_case_labels_are_all_checked(): + guard = load_guard() + methods = guard.switch_cases( + 'switch request.method {\n' + 'case "vm.pause",\n' + ' "vm.only-second-label": break\n' + '}' + ) + assert methods == {"vm.pause", "vm.only-second-label"} + + +def test_compound_case_reports_missing_second_label(): + with tempfile.TemporaryDirectory() as directory: + root = os.path.abspath(directory) + os.makedirs(os.path.join(root, "Sources")) + with open(os.path.join(root, "Sources", "TerminalController.swift"), "w", encoding="utf-8") as handle: + handle.write( + 'switch request.method {\n' + 'case "vm.pause",\n' + ' "vm.only-second-label": break\n' + '}\n' + ) + with open(os.path.join(root, "Sources", "TerminalController+Capabilities.swift"), "w", encoding="utf-8") as handle: + handle.write( + 'var methods: [String] = [\n' + ' "vm.pause"\n' + ' ]\n' + ) + result = subprocess.run( + [sys.executable, GUARD, "--root", root], + cwd=ROOT, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + assert result.returncode != 0 + assert "vm.only-second-label" in result.stdout From e6f07a17cd66c10cfa8416355025d84b4418cfcf Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 14:03:07 -0700 Subject: [PATCH 4/6] test(socket): execute capability regressions and expose exclusion gaps Co-Authored-By: Claude Opus 5.5 --- tests/test_ci_socket_capability_guard.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_ci_socket_capability_guard.py b/tests/test_ci_socket_capability_guard.py index 7fe3555c6854..fd26c394f302 100644 --- a/tests/test_ci_socket_capability_guard.py +++ b/tests/test_ci_socket_capability_guard.py @@ -71,3 +71,34 @@ def test_compound_case_reports_missing_second_label(): ) assert result.returncode != 0 assert "vm.only-second-label" in result.stdout + + +def test_advertised_mobile_and_simulator_methods_cannot_be_excluded(): + for method in ( + "mobile.terminal.mouse", + "mobile.terminal.paste_image", + "mobile.terminal.scroll", + "simulator.type", + ): + with tempfile.TemporaryDirectory() as root: + os.makedirs(os.path.join(root, "Sources")) + with open(os.path.join(root, "Sources", "TerminalController.swift"), "w", encoding="utf-8") as handle: + handle.write(f'switch request.method {{\ncase "vm.pause", "{method}": break\n}}\n') + with open(os.path.join(root, "Sources", "TerminalController+Capabilities.swift"), "w", encoding="utf-8") as handle: + handle.write('var methods: [String] = [\n "vm.pause"\n ]\n') + result = subprocess.run( + [sys.executable, GUARD, "--root", root], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + assert result.returncode != 0, f"Dropping {method} passed: {result.stdout}" + assert method in result.stdout + + +if __name__ == "__main__": + test_public_dispatcher_methods_are_advertised() + test_compound_case_labels_are_all_checked() + test_compound_case_reports_missing_second_label() + test_advertised_mobile_and_simulator_methods_cannot_be_excluded() + print("test_ci_socket_capability_guard: ok (4 tests)") From 1c6420844d10edd4118ac57b629f26f7e829d4ab Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 14:04:45 -0700 Subject: [PATCH 5/6] test(socket): keep capability guard fixture focused Co-Authored-By: Claude Opus 5.5 --- tests/test_ci_socket_capability_guard.py | 26 +----------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/test_ci_socket_capability_guard.py b/tests/test_ci_socket_capability_guard.py index fd26c394f302..adbe5cfc8170 100644 --- a/tests/test_ci_socket_capability_guard.py +++ b/tests/test_ci_socket_capability_guard.py @@ -73,32 +73,8 @@ def test_compound_case_reports_missing_second_label(): assert "vm.only-second-label" in result.stdout -def test_advertised_mobile_and_simulator_methods_cannot_be_excluded(): - for method in ( - "mobile.terminal.mouse", - "mobile.terminal.paste_image", - "mobile.terminal.scroll", - "simulator.type", - ): - with tempfile.TemporaryDirectory() as root: - os.makedirs(os.path.join(root, "Sources")) - with open(os.path.join(root, "Sources", "TerminalController.swift"), "w", encoding="utf-8") as handle: - handle.write(f'switch request.method {{\ncase "vm.pause", "{method}": break\n}}\n') - with open(os.path.join(root, "Sources", "TerminalController+Capabilities.swift"), "w", encoding="utf-8") as handle: - handle.write('var methods: [String] = [\n "vm.pause"\n ]\n') - result = subprocess.run( - [sys.executable, GUARD, "--root", root], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - assert result.returncode != 0, f"Dropping {method} passed: {result.stdout}" - assert method in result.stdout - - if __name__ == "__main__": test_public_dispatcher_methods_are_advertised() test_compound_case_labels_are_all_checked() test_compound_case_reports_missing_second_label() - test_advertised_mobile_and_simulator_methods_cannot_be_excluded() - print("test_ci_socket_capability_guard: ok (4 tests)") + print("test_ci_socket_capability_guard: ok (3 tests)") From 8838074717c393e475038fbed6bf4dce3edf0f78 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 1 Oct 2026 14:04:55 -0700 Subject: [PATCH 6/6] fix(socket): close capability guard review gaps Remove exclusions for advertised mobile and simulator methods and gate workspace change discovery with its feature flag.\n\nCo-Authored-By: Claude Opus 5.5 \n --- Sources/TerminalController+Capabilities.swift | 11 +++++++ scripts/check-socket-capabilities.py | 32 ------------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/Sources/TerminalController+Capabilities.swift b/Sources/TerminalController+Capabilities.swift index d24bee09cd8b..5ac3c8450aa6 100644 --- a/Sources/TerminalController+Capabilities.swift +++ b/Sources/TerminalController+Capabilities.swift @@ -463,6 +463,17 @@ extension TerminalController { ] methods.removeAll { taskComposerMethods.contains($0) } } + // Discovery runs off-main; read the same flag snapshot as mobile host + // capabilities rather than crossing to the main-actor flag store. + if !CmuxFeatureFlags.offMainEffectiveValue(for: CmuxFeatureFlags.mobileWorkspaceChangesFlag) { + let workspaceChangesMethods: Set = [ + "mobile.workspace.changes.files", + "mobile.workspace.changes.file_diff", + "mobile.workspace.changes.file_stat", + "mobile.workspace.changes.file_fetch", + ] + methods.removeAll { workspaceChangesMethods.contains($0) } + } methods.append(contentsOf: ControlCommandExecutionPolicy.simulatorMethods) #if DEBUG methods.append(contentsOf: Self.v2DebugMethodNames) diff --git a/scripts/check-socket-capabilities.py b/scripts/check-socket-capabilities.py index 61f9e2da1ddb..c686bfe60ab4 100644 --- a/scripts/check-socket-capabilities.py +++ b/scripts/check-socket-capabilities.py @@ -77,12 +77,9 @@ mobile.surface.focus mobile.sync.fetch mobile.terminal.close -mobile.terminal.mouse mobile.terminal.participant.disconnect -mobile.terminal.paste_image mobile.terminal.reattach mobile.terminal.rename -mobile.terminal.scroll mobile.terminal.size_policy.set mobile.workspace.changes.summary notification.reconcile @@ -105,35 +102,6 @@ sidebar.custom.reload sidebar.custom.select sidebar.custom.validate -simulator.accessibility -simulator.button -simulator.camera.configure -simulator.camera.mirror -simulator.camera.status -simulator.camera.switch -simulator.context -simulator.core_animation -simulator.event_log -simulator.foreground -simulator.gesture -simulator.memory_warning -simulator.permissions.read -simulator.permissions.set -simulator.prepare_screenshot -simulator.recover -simulator.rotate -simulator.select_device -simulator.swipe -simulator.tap -simulator.tools -simulator.type -simulator.ui.set -simulator.ui.status -simulator.web_inspector.attach -simulator.web_inspector.highlight -simulator.web_inspector.release -simulator.web_inspector.send -simulator.web_inspector.targets """.split())