Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/vibepod/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions src/vibepod/core/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand All @@ -32,6 +33,7 @@ class AgentSpec:
["claude"],
"/claude",
{"CLAUDE_CONFIG_DIR": "/claude"},
ikwid_args=["--dangerously-skip-permissions"],
),
"gemini": AgentSpec(
"gemini",
Expand Down Expand Up @@ -88,6 +90,7 @@ class AgentSpec:
["codex"],
"/config",
{"HOME": "/config"},
ikwid_args=["--full-auto"],
),
}

Expand Down
16 changes: 16 additions & 0 deletions tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
173 changes: 173 additions & 0 deletions tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment thread
nezhar marked this conversation as resolved.
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:
Expand Down
Loading