From 97a42bb8fd21288a7086231d3636169fbf05dec5 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Tue, 17 Mar 2026 15:27:57 +0100 Subject: [PATCH] Add ikwid mode for agent autoaproval --- src/vibepod/commands/run.py | 14 +++ src/vibepod/core/agents.py | 3 + tests/test_agents.py | 16 ++++ tests/test_run.py | 173 ++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+) diff --git a/src/vibepod/commands/run.py b/src/vibepod/commands/run.py index 9531ea3..a9b2eb8 100644 --- a/src/vibepod/commands/run.py +++ b/src/vibepod/commands/run.py @@ -228,6 +228,13 @@ def run( help="Enable image pasting via X11 clipboard (requires DISPLAY to be set)", ), ] = False, + ikwid: Annotated[ + bool, + typer.Option( + "--ikwid", + help="I Know What I'm Doing: enable auto-approval / skip permission prompts", + ), + ] = False, ) -> None: """Start an agent container.""" config = get_config() @@ -290,6 +297,13 @@ def run( raise typer.Exit(1) from exc entrypoint = _init_entrypoint(init_commands) + if ikwid: + if spec.ikwid_args: + info(f"IKWID mode: appending {spec.ikwid_args} to {selected_agent} command") + command = list(command or []) + spec.ikwid_args + else: + warning(f"IKWID mode not supported for agent '{selected_agent}', ignoring") + config_dir = agent_config_dir(selected_agent) config_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/vibepod/core/agents.py b/src/vibepod/core/agents.py index 467b3fd..34cedaf 100644 --- a/src/vibepod/core/agents.py +++ b/src/vibepod/core/agents.py @@ -21,6 +21,7 @@ class AgentSpec: extra_env: dict[str, str] platform: str | None = None run_as_host_user: bool = False + ikwid_args: list[str] | None = None AGENT_SPECS: dict[str, AgentSpec] = { @@ -32,6 +33,7 @@ class AgentSpec: ["claude"], "/claude", {"CLAUDE_CONFIG_DIR": "/claude"}, + ikwid_args=["--dangerously-skip-permissions"], ), "gemini": AgentSpec( "gemini", @@ -88,6 +90,7 @@ class AgentSpec: ["codex"], "/config", {"HOME": "/config"}, + ikwid_args=["--full-auto"], ), } diff --git a/tests/test_agents.py b/tests/test_agents.py index 75a5b12..e44b09a 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -54,6 +54,22 @@ def test_resolve_agent_name_accepts_short_and_full_forms() -> None: assert resolve_agent_name("unknown") is None +def test_claude_spec_has_ikwid_args() -> None: + spec = get_agent_spec("claude") + assert spec.ikwid_args == ["--dangerously-skip-permissions"] + + +def test_codex_spec_has_ikwid_args() -> None: + spec = get_agent_spec("codex") + assert spec.ikwid_args == ["--full-auto"] + + +def test_unsupported_agents_have_no_ikwid_args() -> None: + for agent in ("gemini", "opencode", "devstral", "auggie", "copilot"): + spec = get_agent_spec(agent) + assert spec.ikwid_args is None, f"{agent} should not have ikwid_args" + + def test_get_agent_shortcut_known_agent() -> None: expected_by_agent = {agent: shortcut for shortcut, agent in AGENT_SHORTCUTS.items()} assert set(expected_by_agent.keys()) == set(SUPPORTED_AGENTS) diff --git a/tests/test_run.py b/tests/test_run.py index 2f9ef57..8c67452 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -535,6 +535,179 @@ def test_auto_pull_per_agent_none_falls_back_to_global(monkeypatch, tmp_path: Pa assert len(stub.pulled) == 1 +def test_ikwid_appends_args_for_claude(monkeypatch, tmp_path: Path) -> None: + """--ikwid appends --dangerously-skip-permissions to claude command.""" + captured: dict = {} + + class _CapturingDockerManager: + def ensure_network(self, name: str) -> None: + pass + + def networks_with_running_containers(self) -> list[str]: + return [] + + def pull_image(self, image: str) -> None: + pass + + def ensure_proxy(self, **kwargs) -> None: # type: ignore[no-untyped-def] + pass + + def run_agent(self, **kwargs) -> object: # type: ignore[no-untyped-def] + captured.update(kwargs) + container = type( + "_Container", + (), + { + "name": "vibepod-claude-test", + "id": "abc123", + "status": "running", + "attrs": {"NetworkSettings": {"Networks": {}}}, + "reload": lambda self: None, + "labels": {}, + "logs": lambda self, **kw: b"", + }, + )() + return container + + monkeypatch.setattr(run_cmd, "get_config", lambda: _make_config()) + monkeypatch.setattr(run_cmd, "DockerManager", _CapturingDockerManager) + + run_cmd.run(agent="claude", workspace=tmp_path, detach=True, ikwid=True) + + assert captured["command"] == ["claude", "--dangerously-skip-permissions"] + + +def test_ikwid_appends_args_for_codex(monkeypatch, tmp_path: Path) -> None: + """--ikwid appends --full-auto to codex command.""" + captured: dict = {} + + class _CapturingDockerManager: + def ensure_network(self, name: str) -> None: + pass + + def networks_with_running_containers(self) -> list[str]: + return [] + + def pull_image(self, image: str) -> None: + pass + + def ensure_proxy(self, **kwargs) -> None: # type: ignore[no-untyped-def] + pass + + def run_agent(self, **kwargs) -> object: # type: ignore[no-untyped-def] + captured.update(kwargs) + container = type( + "_Container", + (), + { + "name": "vibepod-codex-test", + "id": "abc123", + "status": "running", + "attrs": {"NetworkSettings": {"Networks": {}}}, + "reload": lambda self: None, + "labels": {}, + "logs": lambda self, **kw: b"", + }, + )() + return container + + cfg = _make_config() + cfg["agents"]["codex"] = {"env": {}, "init": []} + monkeypatch.setattr(run_cmd, "get_config", lambda: cfg) + monkeypatch.setattr(run_cmd, "DockerManager", _CapturingDockerManager) + + run_cmd.run(agent="codex", workspace=tmp_path, detach=True, ikwid=True) + + assert captured["command"] == ["codex", "--full-auto"] + + +def test_ikwid_ignored_for_unsupported_agent(monkeypatch, tmp_path: Path) -> None: + """--ikwid logs warning and proceeds for agents without ikwid_args.""" + captured: dict = {} + + class _CapturingDockerManager: + def ensure_network(self, name: str) -> None: + pass + + def networks_with_running_containers(self) -> list[str]: + return [] + + def pull_image(self, image: str) -> None: + pass + + def ensure_proxy(self, **kwargs) -> None: # type: ignore[no-untyped-def] + pass + + def run_agent(self, **kwargs) -> object: # type: ignore[no-untyped-def] + captured.update(kwargs) + container = type( + "_Container", + (), + { + "name": "vibepod-gemini-test", + "id": "abc123", + "status": "running", + "attrs": {"NetworkSettings": {"Networks": {}}}, + "reload": lambda self: None, + "labels": {}, + "logs": lambda self, **kw: b"", + }, + )() + return container + + cfg = _make_config() + cfg["agents"]["gemini"] = {"env": {}, "init": []} + monkeypatch.setattr(run_cmd, "get_config", lambda: cfg) + monkeypatch.setattr(run_cmd, "DockerManager", _CapturingDockerManager) + + run_cmd.run(agent="gemini", workspace=tmp_path, detach=True, ikwid=True) + + # Command should be unchanged (no ikwid args appended) + assert captured["command"] == ["gemini"] + + +def test_ikwid_false_does_not_modify_command(monkeypatch, tmp_path: Path) -> None: + """Without --ikwid, command is unchanged.""" + captured: dict = {} + + class _CapturingDockerManager: + def ensure_network(self, name: str) -> None: + pass + + def networks_with_running_containers(self) -> list[str]: + return [] + + def pull_image(self, image: str) -> None: + pass + + def ensure_proxy(self, **kwargs) -> None: # type: ignore[no-untyped-def] + pass + + def run_agent(self, **kwargs) -> object: # type: ignore[no-untyped-def] + captured.update(kwargs) + container = type( + "_Container", + (), + { + "name": "vibepod-claude-test", + "id": "abc123", + "status": "running", + "attrs": {"NetworkSettings": {"Networks": {}}}, + "reload": lambda self: None, + "labels": {}, + "logs": lambda self, **kw: b"", + }, + )() + return container + + monkeypatch.setattr(run_cmd, "get_config", lambda: _make_config()) + monkeypatch.setattr(run_cmd, "DockerManager", _CapturingDockerManager) + + run_cmd.run(agent="claude", workspace=tmp_path, detach=True, ikwid=False) + + assert captured["command"] == ["claude"] + + def test_run_accepts_short_agent_name(monkeypatch, tmp_path: Path) -> None: class _UnavailableDockerManager: def __init__(self) -> None: