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
10 changes: 10 additions & 0 deletions clawteam/spawn/subprocess_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def spawn(
final_command.append("--dangerously-skip-permissions")
elif _is_codex_command(normalized_command):
final_command.append("--dangerously-bypass-approvals-and-sandbox")
elif _is_gemini_command(normalized_command):
final_command.append("--yolo")
if _is_nanobot_command(normalized_command):
if cwd and not _command_has_workspace_arg(normalized_command):
final_command.extend(["-w", cwd])
Expand Down Expand Up @@ -143,6 +145,14 @@ def _is_nanobot_command(command: list[str]) -> bool:
return cmd == "nanobot"


def _is_gemini_command(command: list[str]) -> bool:
"""Check if the command is a Gemini CLI invocation."""
if not command:
return False
cmd = command[0].rsplit("/", 1)[-1]
return cmd == "gemini"


def _command_has_workspace_arg(command: list[str]) -> bool:
"""Return True when a command already specifies a nanobot workspace."""
return "-w" in command or "--workspace" in command
26 changes: 23 additions & 3 deletions clawteam/spawn/tmux_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def spawn(
final_command.append("--dangerously-skip-permissions")
elif _is_codex_command(normalized_command):
final_command.append("--dangerously-bypass-approvals-and-sandbox")
elif _is_gemini_command(normalized_command):
final_command.append("--yolo")

if _is_nanobot_command(normalized_command):
if cwd and not _command_has_workspace_arg(normalized_command):
Expand All @@ -87,6 +89,8 @@ def spawn(
final_command.extend(["-m", prompt])
elif prompt and _is_codex_command(normalized_command):
final_command.append(prompt)
elif prompt and _is_gemini_command(normalized_command):
final_command.extend(["-p", prompt])

cmd_str = " ".join(shlex.quote(c) for c in final_command)
# Append on-exit hook: runs immediately when agent process exits
Expand Down Expand Up @@ -187,7 +191,7 @@ def spawn(
stderr=subprocess.PIPE,
)
os.unlink(tmp_path)
elif prompt and not _is_codex_command(normalized_command) and not _is_nanobot_command(normalized_command):
elif prompt and not _is_codex_command(normalized_command) and not _is_nanobot_command(normalized_command) and not _is_gemini_command(normalized_command):
time.sleep(1)
subprocess.run(
["tmux", "send-keys", "-t", target, prompt, "Enter"],
Expand Down Expand Up @@ -325,6 +329,14 @@ def _is_nanobot_command(command: list[str]) -> bool:
return cmd == "nanobot"


def _is_gemini_command(command: list[str]) -> bool:
"""Check if the command is a Gemini CLI invocation."""
if not command:
return False
cmd = command[0].rsplit("/", 1)[-1]
return cmd == "gemini"


def _command_has_workspace_arg(command: list[str]) -> bool:
"""Return True when a command already specifies a nanobot workspace."""
return "-w" in command or "--workspace" in command
Expand All @@ -343,7 +355,7 @@ def _confirm_workspace_trust_if_prompted(
injection and accept it with a single Enter so the interactive TUI remains
intact.
"""
if not (_is_claude_command(command) or _is_codex_command(command)):
if not (_is_claude_command(command) or _is_codex_command(command) or _is_gemini_command(command)):
return False

deadline = time.monotonic() + timeout_seconds
Expand Down Expand Up @@ -384,12 +396,20 @@ def _looks_like_workspace_trust_prompt(command: list[str], pane_text: str) -> bo
and "press enter to continue" in pane_text
)

if _is_gemini_command(command):
return "trust folder" in pane_text or "trust parent folder" in pane_text

return False


def _is_interactive_cli(command: list[str]) -> bool:
"""Check if the command is an interactive AI CLI."""
return _is_claude_command(command) or _is_codex_command(command) or _is_nanobot_command(command)
return (
_is_claude_command(command)
or _is_codex_command(command)
or _is_nanobot_command(command)
or _is_gemini_command(command)
)


def _wait_for_claude_ready(
Expand Down
119 changes: 119 additions & 0 deletions tests/test_spawn_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,125 @@ def fake_popen(cmd, **kwargs):
assert "nanobot agent -w /tmp/demo -m 'do work'" in captured["cmd"]


def test_tmux_backend_gemini_skip_permissions_and_prompt(monkeypatch, tmp_path):
"""Gemini gets --yolo for permissions and -p for prompt."""
monkeypatch.setenv("PATH", "/usr/bin:/bin")
clawteam_bin = tmp_path / "venv" / "bin" / "clawteam"
clawteam_bin.parent.mkdir(parents=True)
clawteam_bin.write_text("#!/bin/sh\n")
monkeypatch.setattr(sys, "argv", [str(clawteam_bin)])

run_calls: list[list[str]] = []

class Result:
def __init__(self, returncode: int = 0, stdout: str = ""):
self.returncode = returncode
self.stdout = stdout
self.stderr = ""

def fake_run(args, **kwargs):
run_calls.append(args)
if args[:3] == ["tmux", "has-session", "-t"]:
return Result(returncode=1)
if args[:3] == ["tmux", "list-panes", "-t"]:
return Result(returncode=0, stdout="9876\n")
return Result(returncode=0)

def fake_which(name, path=None):
if name == "tmux":
return "/usr/bin/tmux"
if name == "gemini":
return "/usr/bin/gemini"
return None

monkeypatch.setattr("clawteam.spawn.tmux_backend.shutil.which", fake_which)
monkeypatch.setattr("clawteam.spawn.command_validation.shutil.which", fake_which)
monkeypatch.setattr("clawteam.spawn.tmux_backend.subprocess.run", fake_run)
monkeypatch.setattr("clawteam.spawn.tmux_backend.time.sleep", lambda *_: None)
monkeypatch.setattr("clawteam.spawn.registry.register_agent", lambda **_: None)

backend = TmuxBackend()
backend.spawn(
command=["gemini"],
agent_name="researcher",
agent_id="agent-2",
agent_type="general-purpose",
team_name="demo-team",
prompt="analyze this repo",
cwd="/tmp/demo",
skip_permissions=True,
)

new_session = next(call for call in run_calls if call[:3] == ["tmux", "new-session", "-d"])
full_cmd = new_session[-1]
assert " gemini --yolo -p 'analyze this repo';" in full_cmd


def test_subprocess_backend_gemini_skip_permissions_and_prompt(monkeypatch, tmp_path):
"""Gemini subprocess uses --yolo and -p flags."""
monkeypatch.setenv("PATH", "/usr/bin:/bin")
clawteam_bin = tmp_path / "venv" / "bin" / "clawteam"
clawteam_bin.parent.mkdir(parents=True)
clawteam_bin.write_text("#!/bin/sh\n")
monkeypatch.setattr(sys, "argv", [str(clawteam_bin)])

captured: dict[str, object] = {}

def fake_popen(cmd, **kwargs):
captured["cmd"] = cmd
return DummyProcess()

monkeypatch.setattr(
"clawteam.spawn.command_validation.shutil.which",
lambda name, path=None: "/usr/bin/gemini" if name == "gemini" else None,
)
monkeypatch.setattr("clawteam.spawn.subprocess_backend.subprocess.Popen", fake_popen)
monkeypatch.setattr("clawteam.spawn.registry.register_agent", lambda **_: None)

backend = SubprocessBackend()
backend.spawn(
command=["gemini"],
agent_name="researcher",
agent_id="agent-2",
agent_type="general-purpose",
team_name="demo-team",
prompt="analyze this repo",
cwd="/tmp/demo",
skip_permissions=True,
)

assert "gemini --yolo -p 'analyze this repo'" in captured["cmd"]


def test_tmux_backend_confirms_gemini_workspace_trust_prompt(monkeypatch):
run_calls: list[list[str]] = []

class Result:
def __init__(self, returncode: int = 0, stdout: str = ""):
self.returncode = returncode
self.stdout = stdout
self.stderr = ""

def fake_run(args, **kwargs):
run_calls.append(args)
if args[:4] == ["tmux", "capture-pane", "-p", "-t"]:
return Result(
stdout=(
"Gemini CLI\n"
"Trust folder: /tmp/demo\n"
)
)
return Result()

monkeypatch.setattr("clawteam.spawn.tmux_backend.subprocess.run", fake_run)
monkeypatch.setattr("clawteam.spawn.tmux_backend.time.sleep", lambda *_: None)

confirmed = _confirm_workspace_trust_if_prompted("demo:agent", ["gemini"])

assert confirmed is True
assert ["tmux", "send-keys", "-t", "demo:agent", "Enter"] in run_calls


def test_resolve_clawteam_executable_ignores_unrelated_argv0(monkeypatch, tmp_path):
unrelated = tmp_path / "not-clawteam-review"
unrelated.write_text("#!/bin/sh\n")
Expand Down
Loading