From 3c2cb5c409de01d19daac74954c9603e4eeafdbf Mon Sep 17 00:00:00 2001 From: "Feng, Haofei" Date: Thu, 23 Apr 2026 23:58:58 +1000 Subject: [PATCH 1/5] fix(kiro_cli): honor --yolo and profile.model at launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kiro_cli provider silently dropped two CAO launch flags: 1. --yolo (allowed_tools=['*']) never became kiro-cli --trust-all-tools, so every tool invocation re-prompted for permission and blocked assign/handoff flows inside multi-agent workflows. 2. profile.model was never passed to kiro-cli. Workflows pinning a specific model silently ran on kiro-cli's default (auto). Translate allowed_tools=['*'] into --trust-all-tools and profile.model into --model for both the TUI launch and the --legacy-ui fallback path. Profile load is best-effort — kiro-cli has its own agent store, so a CAO-unregistered profile must not block launch. PR #189 fixed the model gap for claude_code/codex/gemini/kimi/copilot but assumed kiro_cli handled model via install-time JSON baking; that assumption did not cover the runtime launch command. Co-Authored-By: Claude Opus 4.7 --- .../providers/kiro_cli.py | 43 +++++- test/providers/test_kiro_cli_unit.py | 122 +++++++++++++++++- 2 files changed, 158 insertions(+), 7 deletions(-) diff --git a/src/cli_agent_orchestrator/providers/kiro_cli.py b/src/cli_agent_orchestrator/providers/kiro_cli.py index 159e072a2..512b7c768 100644 --- a/src/cli_agent_orchestrator/providers/kiro_cli.py +++ b/src/cli_agent_orchestrator/providers/kiro_cli.py @@ -25,6 +25,7 @@ from cli_agent_orchestrator.clients.tmux import tmux_client from cli_agent_orchestrator.models.terminal import TerminalStatus from cli_agent_orchestrator.providers.base import BaseProvider +from cli_agent_orchestrator.utils.agent_profiles import load_agent_profile from cli_agent_orchestrator.utils.terminal import wait_for_shell, wait_until_status logger = logging.getLogger(__name__) @@ -141,6 +142,24 @@ def paste_enter_count(self) -> int: """Kiro CLI submits on single Enter after bracketed paste.""" return 1 + def _get_profile_model(self) -> Optional[str]: + """Return profile.model if the agent profile can be loaded, else None. + + Best-effort: historically the Kiro CLI provider has not required the + CAO agent profile to be loadable at runtime (kiro-cli has its own + agent store). A missing or unparseable profile must not block launch. + """ + try: + profile = load_agent_profile(self._agent_profile) + except (FileNotFoundError, RuntimeError) as exc: + logger.debug( + "Profile '%s' not loadable by CAO; skipping --model resolution: %s", + self._agent_profile, + exc, + ) + return None + return profile.model or None + def initialize(self) -> bool: """Initialize Kiro CLI provider by starting kiro-cli chat command. @@ -163,7 +182,19 @@ def initialize(self) -> bool: # Step 2: Start the Kiro CLI chat session using kiro-cli's default UI. # Detection code handles both legacy and TUI patterns (stateless). # If initialization fails, fall back to --legacy-ui. - command = shlex.join(["kiro-cli", "chat", "--agent", self._agent_profile]) + # + # --trust-all-tools: bypass Kiro CLI's permission prompts when CAO + # launches with --yolo (allowed_tools=['*']). Without this, every + # tool invocation re-prompts, blocking assign/handoff flows. + # --model: honor profile.model so workflows can pin a specific model. + base_args = ["kiro-cli", "chat"] + if self._allowed_tools and "*" in self._allowed_tools: + base_args.append("--trust-all-tools") + model = self._get_profile_model() + if model: + base_args.extend(["--model", model]) + base_args.extend(["--agent", self._agent_profile]) + command = shlex.join(base_args) tmux_client.send_keys(self.session_name, self.window_name, command) # Step 3: Wait for Kiro CLI to fully initialize and show the agent prompt. @@ -178,9 +209,13 @@ def initialize(self) -> bool: tmux_client.send_keys(self.session_name, self.window_name, "/exit") if not wait_for_shell(tmux_client, self.session_name, self.window_name, timeout=10.0): raise TimeoutError("Shell recovery timed out after --legacy-ui fallback") - legacy_command = shlex.join( - ["kiro-cli", "chat", "--legacy-ui", "--agent", self._agent_profile] - ) + legacy_args = ["kiro-cli", "chat", "--legacy-ui"] + if self._allowed_tools and "*" in self._allowed_tools: + legacy_args.append("--trust-all-tools") + if model: + legacy_args.extend(["--model", model]) + legacy_args.extend(["--agent", self._agent_profile]) + legacy_command = shlex.join(legacy_args) tmux_client.send_keys(self.session_name, self.window_name, legacy_command) if not wait_until_status( self, {TerminalStatus.IDLE, TerminalStatus.COMPLETED}, timeout=30.0 diff --git a/test/providers/test_kiro_cli_unit.py b/test/providers/test_kiro_cli_unit.py index 34068f95e..9926e8248 100644 --- a/test/providers/test_kiro_cli_unit.py +++ b/test/providers/test_kiro_cli_unit.py @@ -22,13 +22,17 @@ def load_fixture(filename: str) -> str: class TestKiroCliProviderInitialization: """Test Kiro CLI provider initialization.""" + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") - def test_initialize_success(self, mock_tmux, mock_wait_status, mock_wait_shell): + def test_initialize_success( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): """Test successful initialization.""" mock_wait_shell.return_value = True mock_wait_status.return_value = True + mock_load_profile.side_effect = FileNotFoundError("no profile") provider = KiroCliProvider("test1234", "test-session", "window-0", "developer") result = provider.initialize() @@ -51,27 +55,35 @@ def test_initialize_shell_timeout(self, mock_tmux, mock_wait_shell): with pytest.raises(TimeoutError, match="Shell initialization timed out"): provider.initialize() + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") - def test_initialize_kiro_cli_timeout(self, mock_tmux, mock_wait_status, mock_wait_shell): + def test_initialize_kiro_cli_timeout( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): """Test initialization fails when both TUI and --legacy-ui timeout.""" mock_wait_shell.return_value = True mock_wait_status.return_value = False + mock_load_profile.side_effect = FileNotFoundError("no profile") provider = KiroCliProvider("test1234", "test-session", "window-0", "developer") with pytest.raises(TimeoutError, match="timed out with TUI and `--legacy-ui`"): provider.initialize() + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") - def test_initialize_legacy_ui_fallback(self, mock_tmux, mock_wait_status, mock_wait_shell): + def test_initialize_legacy_ui_fallback( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): """Test fallback to --legacy-ui when TUI initialization fails.""" mock_wait_shell.return_value = True # First call (TUI) fails, second call (--legacy-ui) succeeds mock_wait_status.side_effect = [False, True] + mock_load_profile.side_effect = FileNotFoundError("no profile") provider = KiroCliProvider("test1234", "test-session", "window-0", "developer") result = provider.initialize() @@ -92,6 +104,110 @@ def test_initialize_legacy_ui_fallback(self, mock_tmux, mock_wait_status, mock_w "kiro-cli chat --legacy-ui --agent developer", ) + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") + @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") + def test_initialize_yolo_adds_trust_all_tools( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): + """--yolo (allowed_tools=['*']) must pass --trust-all-tools to kiro-cli.""" + mock_wait_shell.return_value = True + mock_wait_status.return_value = True + mock_load_profile.side_effect = FileNotFoundError("no profile") + + provider = KiroCliProvider( + "test1234", "test-session", "window-0", "developer", allowed_tools=["*"] + ) + provider.initialize() + + mock_tmux.send_keys.assert_called_once_with( + "test-session", + "window-0", + "kiro-cli chat --trust-all-tools --agent developer", + ) + + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") + @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") + def test_initialize_passes_profile_model( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): + """profile.model must be forwarded to kiro-cli via --model.""" + mock_wait_shell.return_value = True + mock_wait_status.return_value = True + profile = Mock() + profile.model = "claude-opus-4-6" + mock_load_profile.return_value = profile + + provider = KiroCliProvider("test1234", "test-session", "window-0", "developer") + provider.initialize() + + mock_tmux.send_keys.assert_called_once_with( + "test-session", + "window-0", + "kiro-cli chat --model claude-opus-4-6 --agent developer", + ) + + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") + @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") + def test_initialize_yolo_and_model_combine( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): + """--yolo and profile.model should both appear on the command line.""" + mock_wait_shell.return_value = True + mock_wait_status.return_value = True + profile = Mock() + profile.model = "claude-opus-4-6" + mock_load_profile.return_value = profile + + provider = KiroCliProvider( + "test1234", "test-session", "window-0", "developer", allowed_tools=["*"] + ) + provider.initialize() + + mock_tmux.send_keys.assert_called_once_with( + "test-session", + "window-0", + "kiro-cli chat --trust-all-tools --model claude-opus-4-6 --agent developer", + ) + + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") + @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") + def test_initialize_legacy_ui_fallback_preserves_flags( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): + """Legacy-UI fallback must preserve --trust-all-tools and --model.""" + mock_wait_shell.return_value = True + mock_wait_status.side_effect = [False, True] + profile = Mock() + profile.model = "claude-opus-4-6" + mock_load_profile.return_value = profile + + provider = KiroCliProvider( + "test1234", "test-session", "window-0", "developer", allowed_tools=["*"] + ) + provider.initialize() + + calls = mock_tmux.send_keys.call_args_list + assert len(calls) == 3 + assert calls[0].args == ( + "test-session", + "window-0", + "kiro-cli chat --trust-all-tools --model claude-opus-4-6 --agent developer", + ) + assert calls[1].args == ("test-session", "window-0", "/exit") + assert calls[2].args == ( + "test-session", + "window-0", + "kiro-cli chat --legacy-ui --trust-all-tools --model claude-opus-4-6 --agent developer", + ) + def test_initialization_with_different_agent_profiles(self): """Test initialization with various agent profile names.""" test_profiles = ["developer", "code-reviewer", "test_agent", "agent123"] From 214e57c5bf6f008cb18fdd3cacef755891f01ff8 Mon Sep 17 00:00:00 2001 From: "Feng, Haofei" Date: Fri, 24 Apr 2026 13:06:06 +1000 Subject: [PATCH 2/5] fix(kiro_cli): skip TUI when --yolo to bypass consent dialog kiro-cli 2.0.1's new TUI shows a non-bypassable "Yes, I accept" consent dialog when launched with --trust-all-tools, breaking CAO's headless launch. Only --legacy-ui / --classic / --no-interactive skip the dialog. When CAO launches with --yolo (allowed_tools=['*']), now launch directly with --legacy-ui + --trust-all-tools instead of attempting TUI first. Non-yolo launches preserve the existing TUI-first behavior with --legacy-ui fallback. Also fail fast on yolo timeout (no second fallback), avoiding the ~30-second double-timeout that previously surfaced as a 500 to the caller. Co-Authored-By: Claude Opus 4.7 --- .../providers/kiro_cli.py | 34 ++++++++--- test/providers/test_kiro_cli_unit.py | 56 ++++++++++++++----- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/cli_agent_orchestrator/providers/kiro_cli.py b/src/cli_agent_orchestrator/providers/kiro_cli.py index 512b7c768..2d7fab734 100644 --- a/src/cli_agent_orchestrator/providers/kiro_cli.py +++ b/src/cli_agent_orchestrator/providers/kiro_cli.py @@ -179,18 +179,31 @@ def initialize(self) -> bool: if not wait_for_shell(tmux_client, self.session_name, self.window_name, timeout=10.0): raise TimeoutError("Shell initialization timed out after 10 seconds") - # Step 2: Start the Kiro CLI chat session using kiro-cli's default UI. - # Detection code handles both legacy and TUI patterns (stateless). - # If initialization fails, fall back to --legacy-ui. + # Step 2: Start the Kiro CLI chat session. # # --trust-all-tools: bypass Kiro CLI's permission prompts when CAO # launches with --yolo (allowed_tools=['*']). Without this, every # tool invocation re-prompts, blocking assign/handoff flows. # --model: honor profile.model so workflows can pin a specific model. - base_args = ["kiro-cli", "chat"] - if self._allowed_tools and "*" in self._allowed_tools: - base_args.append("--trust-all-tools") + # + # UI mode selection: + # - Yolo (--trust-all-tools): kiro-cli 2.0.1 TUI blocks on an + # interactive "Yes, I accept" consent dialog before the chat is + # ready; only --legacy-ui/--classic/--no-interactive bypass it. + # CAO drives kiro-cli headlessly, so we force --legacy-ui for yolo. + # - Non-yolo: use the default TUI (fall back to --legacy-ui on + # timeout, preserving prior behavior for older kiro-cli versions). + yolo = bool(self._allowed_tools and "*" in self._allowed_tools) model = self._get_profile_model() + + if yolo: + logger.info( + "kiro_cli yolo mode: forcing --legacy-ui (kiro-cli 2.0.1 TUI " + "shows a non-bypassable trust-all-tools consent dialog)" + ) + base_args = ["kiro-cli", "chat", "--legacy-ui", "--trust-all-tools"] + else: + base_args = ["kiro-cli", "chat"] if model: base_args.extend(["--model", model]) base_args.extend(["--agent", self._agent_profile]) @@ -203,15 +216,18 @@ def initialize(self) -> bool: if not wait_until_status( self, {TerminalStatus.IDLE, TerminalStatus.COMPLETED}, timeout=30.0 ): - # TUI mode failed — fall back to --legacy-ui + if yolo: + # Yolo already launched with --legacy-ui; no further fallback. + raise TimeoutError( + "Kiro CLI initialization timed out with --legacy-ui (yolo mode)" + ) + # Non-yolo TUI mode failed — fall back to --legacy-ui logger.warning("Kiro CLI TUI initialization timed out, retrying with --legacy-ui") # Exit the current session and start fresh with --legacy-ui tmux_client.send_keys(self.session_name, self.window_name, "/exit") if not wait_for_shell(tmux_client, self.session_name, self.window_name, timeout=10.0): raise TimeoutError("Shell recovery timed out after --legacy-ui fallback") legacy_args = ["kiro-cli", "chat", "--legacy-ui"] - if self._allowed_tools and "*" in self._allowed_tools: - legacy_args.append("--trust-all-tools") if model: legacy_args.extend(["--model", model]) legacy_args.extend(["--agent", self._agent_profile]) diff --git a/test/providers/test_kiro_cli_unit.py b/test/providers/test_kiro_cli_unit.py index 9926e8248..73e7ab762 100644 --- a/test/providers/test_kiro_cli_unit.py +++ b/test/providers/test_kiro_cli_unit.py @@ -108,10 +108,14 @@ def test_initialize_legacy_ui_fallback( @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") - def test_initialize_yolo_adds_trust_all_tools( + def test_initialize_yolo_forces_legacy_ui_with_trust_all_tools( self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile ): - """--yolo (allowed_tools=['*']) must pass --trust-all-tools to kiro-cli.""" + """--yolo (allowed_tools=['*']) must launch directly with --legacy-ui + --trust-all-tools. + + kiro-cli 2.0.1 TUI shows a non-bypassable trust-all-tools consent + dialog; yolo headless launches must skip the TUI attempt entirely. + """ mock_wait_shell.return_value = True mock_wait_status.return_value = True mock_load_profile.side_effect = FileNotFoundError("no profile") @@ -124,7 +128,7 @@ def test_initialize_yolo_adds_trust_all_tools( mock_tmux.send_keys.assert_called_once_with( "test-session", "window-0", - "kiro-cli chat --trust-all-tools --agent developer", + "kiro-cli chat --legacy-ui --trust-all-tools --agent developer", ) @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") @@ -157,11 +161,11 @@ def test_initialize_passes_profile_model( def test_initialize_yolo_and_model_combine( self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile ): - """--yolo and profile.model should both appear on the command line.""" + """--yolo + profile.model: --legacy-ui + --trust-all-tools + --model, all in one launch.""" mock_wait_shell.return_value = True mock_wait_status.return_value = True profile = Mock() - profile.model = "claude-opus-4-6" + profile.model = "claude-opus-4.6" mock_load_profile.return_value = profile provider = KiroCliProvider( @@ -172,26 +176,50 @@ def test_initialize_yolo_and_model_combine( mock_tmux.send_keys.assert_called_once_with( "test-session", "window-0", - "kiro-cli chat --trust-all-tools --model claude-opus-4-6 --agent developer", + "kiro-cli chat --legacy-ui --trust-all-tools --model claude-opus-4.6 --agent developer", ) @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") - def test_initialize_legacy_ui_fallback_preserves_flags( + def test_initialize_yolo_no_fallback_on_timeout( self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile ): - """Legacy-UI fallback must preserve --trust-all-tools and --model.""" + """Yolo launch is already --legacy-ui; on timeout, raise — do not re-fall-back. + + Prevents the old double-timeout behavior (TUI timeout → legacy fallback → + legacy timeout) which added ~30 seconds before returning the 500 to the caller. + """ mock_wait_shell.return_value = True - mock_wait_status.side_effect = [False, True] - profile = Mock() - profile.model = "claude-opus-4-6" - mock_load_profile.return_value = profile + mock_wait_status.return_value = False + mock_load_profile.side_effect = FileNotFoundError("no profile") provider = KiroCliProvider( "test1234", "test-session", "window-0", "developer", allowed_tools=["*"] ) + + with pytest.raises(TimeoutError, match="timed out with --legacy-ui"): + provider.initialize() + + # Only one launch attempt — no /exit, no second launch. + assert mock_tmux.send_keys.call_count == 1 + + @patch("cli_agent_orchestrator.providers.kiro_cli.load_agent_profile") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_for_shell") + @patch("cli_agent_orchestrator.providers.kiro_cli.wait_until_status") + @patch("cli_agent_orchestrator.providers.kiro_cli.tmux_client") + def test_initialize_non_yolo_legacy_ui_fallback_preserves_model( + self, mock_tmux, mock_wait_status, mock_wait_shell, mock_load_profile + ): + """Non-yolo TUI timeout → --legacy-ui fallback must preserve --model.""" + mock_wait_shell.return_value = True + mock_wait_status.side_effect = [False, True] + profile = Mock() + profile.model = "claude-opus-4.6" + mock_load_profile.return_value = profile + + provider = KiroCliProvider("test1234", "test-session", "window-0", "developer") provider.initialize() calls = mock_tmux.send_keys.call_args_list @@ -199,13 +227,13 @@ def test_initialize_legacy_ui_fallback_preserves_flags( assert calls[0].args == ( "test-session", "window-0", - "kiro-cli chat --trust-all-tools --model claude-opus-4-6 --agent developer", + "kiro-cli chat --model claude-opus-4.6 --agent developer", ) assert calls[1].args == ("test-session", "window-0", "/exit") assert calls[2].args == ( "test-session", "window-0", - "kiro-cli chat --legacy-ui --trust-all-tools --model claude-opus-4-6 --agent developer", + "kiro-cli chat --legacy-ui --model claude-opus-4.6 --agent developer", ) def test_initialization_with_different_agent_profiles(self): From 862bcca2b59dc91cc516047643dba423799a441a Mon Sep 17 00:00:00 2001 From: "Feng, Haofei" Date: Fri, 24 Apr 2026 13:10:42 +1000 Subject: [PATCH 3/5] style(kiro_cli): reformat with black Co-Authored-By: Claude Opus 4.7 --- src/cli_agent_orchestrator/providers/kiro_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cli_agent_orchestrator/providers/kiro_cli.py b/src/cli_agent_orchestrator/providers/kiro_cli.py index 2d7fab734..7206b3e3f 100644 --- a/src/cli_agent_orchestrator/providers/kiro_cli.py +++ b/src/cli_agent_orchestrator/providers/kiro_cli.py @@ -218,9 +218,7 @@ def initialize(self) -> bool: ): if yolo: # Yolo already launched with --legacy-ui; no further fallback. - raise TimeoutError( - "Kiro CLI initialization timed out with --legacy-ui (yolo mode)" - ) + raise TimeoutError("Kiro CLI initialization timed out with --legacy-ui (yolo mode)") # Non-yolo TUI mode failed — fall back to --legacy-ui logger.warning("Kiro CLI TUI initialization timed out, retrying with --legacy-ui") # Exit the current session and start fresh with --legacy-ui From ccd2e9ed3c26bc24aabb4bdae681159531d629ee Mon Sep 17 00:00:00 2001 From: "Feng, Haofei" Date: Fri, 24 Apr 2026 13:13:47 +1000 Subject: [PATCH 4/5] feat(launch): note kiro_cli legacy-ui fallback in --yolo warning Tell the user that kiro_cli will be launched with --legacy-ui when --yolo is set. Mirrors the provider-side behavior so the CLI output matches what actually runs. Co-Authored-By: Claude Opus 4.7 --- src/cli_agent_orchestrator/cli/commands/launch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cli_agent_orchestrator/cli/commands/launch.py b/src/cli_agent_orchestrator/cli/commands/launch.py index 4d2e64169..ba6ab2579 100644 --- a/src/cli_agent_orchestrator/cli/commands/launch.py +++ b/src/cli_agent_orchestrator/cli/commands/launch.py @@ -134,6 +134,14 @@ def launch( f" Agent can execute ANY command (aws, rm, curl, read credentials).\n" f" Directory: {display_dir}\n" ) + if provider == "kiro_cli": + # kiro-cli 2.0.1 TUI blocks on an interactive "Yes, I accept" + # consent dialog when --trust-all-tools is set. CAO cannot + # answer it headlessly, so yolo launches use --legacy-ui. + click.echo( + " Note: kiro_cli will launch in --legacy-ui mode so " + "--trust-all-tools can be applied non-interactively.\n" + ) else: # Normal launch: show tool summary and confirm tool_summary = format_tool_summary(resolved_allowed_tools) From 459fca69b9e1425771a4b240d007ebb3676828c2 Mon Sep 17 00:00:00 2001 From: "Feng, Haofei" Date: Fri, 24 Apr 2026 21:06:46 +1000 Subject: [PATCH 5/5] docs(opencode): flag --yolo as install-time only + warn at launch opencode's TUI has no runtime equivalent to --dangerously-skip-permissions / --yolo / --trust-all-tools (tracked upstream in sst/opencode#8463), so permissions must be baked into the profile at `cao install` time. `cao launch --yolo` on opencode_cli is silently ignored today; this commit makes the gap visible. - docs/opencode-cli.md: correct the "pass --yolo" note, add a "Known Limitations" subsection explaining the install-time-only workflow and linking the upstream issue. - cli/commands/launch.py: when provider == opencode_cli and --yolo is set, emit a yellow warning that --yolo has no runtime effect and point the user at `cao install` with `allowedTools: ["*"]`. Co-Authored-By: Claude Opus 4.7 --- docs/opencode-cli.md | 29 +++++++++++++++++-- .../cli/commands/launch.py | 13 +++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/docs/opencode-cli.md b/docs/opencode-cli.md index e06341b35..736b1bdc7 100644 --- a/docs/opencode-cli.md +++ b/docs/opencode-cli.md @@ -57,8 +57,10 @@ cao launch --agents developer --provider opencode_cli --auto-approve # Specify model override cao launch --agents developer --provider opencode_cli --model anthropic/claude-sonnet-4-6 -# Unrestricted (DANGEROUS) — agent can run any command -cao launch --agents developer --provider opencode_cli --yolo +# Unrestricted access — install the profile with allowedTools: ["*"] first. +# `cao launch --yolo` is a no-op on opencode_cli (see Known Limitations). +cao install developer --provider opencode_cli # profile must have allowedTools: ["*"] +cao launch --agents developer --provider opencode_cli ``` Via HTTP API: @@ -114,7 +116,7 @@ Tools not in any enabled category default to `deny`. The following tools have ha | `webfetch`, `websearch`, `codesearch` | deny | Network egress — opt-in only | | `todowrite`, `skill` | allow | In-memory / additive, no side-effects | -Pass `--yolo` (or set `allowedTools: ["*"]` in the profile) to allow all 13 tools including the above. +To allow all 13 tools including the above, set `allowedTools: ["*"]` in the profile and re-run `cao install`. Unlike the other providers, `cao launch --yolo` does **not** widen permissions at runtime on `opencode_cli` — see [`cao launch --yolo` is install-time only](#cao-launch---yolo-is-install-time-only) below. ### `cao launch --auto-approve` @@ -188,6 +190,27 @@ The `test_assign_with_callback` test validates all four orchestration modes: ## Known Limitations +### `cao launch --yolo` is install-time only + +Unlike every other CAO provider, `opencode_cli` does **not** honour `cao launch --yolo` at runtime. Permissions are baked into the installed agent's frontmatter `permission:` block at `cao install` time and cannot be loosened by a launch flag. + +Root cause: OpenCode's TUI (the mode CAO drives) has no equivalent to `--dangerously-skip-permissions` / `--yolo` / `--trust-all-tools`. The flag exists only on the `opencode run` headless one-shot command, which CAO does not use. Tracked upstream in [sst/opencode#8463](https://github.com/sst/opencode/issues/8463) and sibling issues. + +**To get unrestricted access on `opencode_cli`:** + +```bash +# 1. Edit the profile's frontmatter so it contains: +# allowedTools: ["*"] + +# 2. Re-run cao install — this rewrites the permission: block with all tools set to allow. +cao install my_agent --provider opencode_cli + +# 3. Launch normally (omit --yolo — it would emit a warning and still only honour what's installed). +cao launch --agents my_agent --provider opencode_cli +``` + +CAO's tracking issue for providing a runtime bypass (either via the temp-agent workaround or by consuming an upstream TUI flag once it ships): see the README's *experimental — single-agent only* notice. + ### Project-local `opencode.json` override OpenCode's config merge precedence places a project-local `opencode.json` in the current working directory **above** `OPENCODE_CONFIG` (the CAO-managed file). If you `cao launch` in a directory that has its own `opencode.json` with conflicting `agent..tools` or `tools` entries, CAO's MCP wiring can be silently overridden for that agent. diff --git a/src/cli_agent_orchestrator/cli/commands/launch.py b/src/cli_agent_orchestrator/cli/commands/launch.py index df82bdaf5..0a8623daa 100644 --- a/src/cli_agent_orchestrator/cli/commands/launch.py +++ b/src/cli_agent_orchestrator/cli/commands/launch.py @@ -143,6 +143,19 @@ def launch( " Note: kiro_cli will launch in --legacy-ui mode so " "--trust-all-tools can be applied non-interactively.\n" ) + elif provider == "opencode_cli": + # opencode's TUI has no runtime skip-permissions flag + # (tracked upstream in sst/opencode#8463). Permissions are + # install-time only, so --yolo cannot loosen them here. + click.echo( + click.style( + " Note: --yolo has no runtime effect on opencode_cli.\n" + " Permissions are set at cao install time. To get unrestricted\n" + " access, set 'allowedTools: [\"*\"]' in the profile and re-run\n" + " 'cao install'. See docs/opencode-cli.md for details.\n", + fg="yellow", + ) + ) else: # Normal launch: show tool summary and confirm tool_summary = format_tool_summary(resolved_allowed_tools)