diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index e9a0c4ee..c45e22d7 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -50,7 +50,6 @@ from .utils.github import ( git_has_uncommitted_changes, is_valid_git_repo, remove_directory, - repo_directory_exists, ) @@ -82,43 +81,62 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 repo_dir = f"/workspace/{repo}" - # Check if directory exists - logger.debug("Checking if repo directory exists at %s", repo_dir) - dir_exists = await loop.run_in_executor(None, repo_directory_exists, sandbox_backend, repo_dir) + is_git_repo = await loop.run_in_executor(None, is_valid_git_repo, sandbox_backend, repo_dir) - if dir_exists: - is_git_repo = await loop.run_in_executor(None, is_valid_git_repo, sandbox_backend, repo_dir) + if not is_git_repo: + # Directory missing or not a valid git repo - remove and clone + logger.warning( + "Repo directory missing or not a valid git repo at %s, removing", repo_dir + ) + try: + removed = await loop.run_in_executor(None, remove_directory, sandbox_backend, repo_dir) + if not removed: + msg = f"Failed to remove invalid directory at {repo_dir}" + logger.error(msg) + raise RuntimeError(msg) + logger.info("Removed invalid directory, will clone fresh repo") + except Exception: + logger.exception("Failed to remove invalid directory") + raise + # Fall through to clone below + else: + # Valid git repo exists, check for uncommitted changes + logger.info("Repo exists at %s, checking for uncommitted changes", repo_dir) + has_changes = await loop.run_in_executor( + None, git_has_uncommitted_changes, sandbox_backend, repo_dir + ) - if not is_git_repo: - # Directory exists but is not a valid git repo - remove and clone - logger.warning("Directory exists but is not a valid git repo at %s, removing", repo_dir) - try: - removed = await loop.run_in_executor(None, remove_directory, sandbox_backend, repo_dir) - if not removed: - msg = f"Failed to remove invalid directory at {repo_dir}" - logger.error(msg) - raise RuntimeError(msg) - logger.info("Removed invalid directory, will clone fresh repo") - except Exception: - logger.exception("Failed to remove invalid directory") - raise - # Fall through to clone below - else: - # Valid git repo exists, check for uncommitted changes - logger.info("Repo exists at %s, checking for uncommitted changes", repo_dir) - has_changes = await loop.run_in_executor( - None, git_has_uncommitted_changes, sandbox_backend, repo_dir + if has_changes: + logger.warning("Repo has uncommitted changes at %s, skipping pull", repo_dir) + return repo_dir + + # No uncommitted changes, safe to pull + logger.info("Repo is clean, pulling latest changes from %s/%s", owner, repo) + + auth_url = f"https://git:{token}@github.com/{owner}/{repo}.git" + clean_url = f"https://github.com/{owner}/{repo}.git" + + # Set authenticated URL for private repos, then restore clean URL. + try: + await loop.run_in_executor( + None, + sandbox_backend.execute, + f"cd {repo_dir} && git remote set-url origin {auth_url}", ) - - if has_changes: - logger.warning("Repo has uncommitted changes at %s, skipping pull", repo_dir) - return repo_dir - - # No uncommitted changes, safe to pull - logger.info("Repo is clean, pulling latest changes from %s/%s", owner, repo) - - # CRITICAL: Ensure remote URL doesn't contain token (clean up from previous runs) - clean_url = f"https://github.com/{owner}/{repo}.git" + pull_result = await loop.run_in_executor( + None, sandbox_backend.execute, f"cd {repo_dir} && git pull origin" + ) + logger.debug("Git pull result: exit_code=%s", pull_result.exit_code) + if pull_result.exit_code != 0: + logger.warning( + "Git pull failed with exit code %s: %s", + pull_result.exit_code, + pull_result.output[:200] if pull_result.output else "", + ) + except Exception: + logger.exception("Failed to execute git pull") + raise + finally: try: await loop.run_in_executor( None, @@ -126,28 +144,11 @@ async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915 f"cd {repo_dir} && git remote set-url origin {clean_url}", ) except Exception: - logger.exception("Failed to set remote URL") + logger.exception("Failed to restore clean remote URL") raise - # Pull with authenticated URL - auth_url = f"https://git:{token}@github.com/{owner}/{repo}.git" - try: - pull_result = await loop.run_in_executor( - None, sandbox_backend.execute, f"cd {repo_dir} && git pull {auth_url}" - ) - logger.debug("Git pull result: exit_code=%s", pull_result.exit_code) - if pull_result.exit_code != 0: - logger.warning( - "Git pull failed with exit code %s: %s", - pull_result.exit_code, - pull_result.output[:200] if pull_result.output else "", - ) - except Exception: - logger.exception("Failed to execute git pull") - raise - - logger.info("Repo updated at %s", repo_dir) - return repo_dir + logger.info("Repo updated at %s", repo_dir) + return repo_dir # Directory doesn't exist or was removed - clone it logger.info("Cloning repo %s/%s to %s", owner, repo, repo_dir) diff --git a/apps/agent/agent/utils/github.py b/apps/agent/agent/utils/github.py index 79269a23..cee7eda5 100644 --- a/apps/agent/agent/utils/github.py +++ b/apps/agent/agent/utils/github.py @@ -4,9 +4,9 @@ from __future__ import annotations import logging import shlex -from typing import Any import httpx +from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol logger = logging.getLogger(__name__) @@ -15,19 +15,14 @@ HTTP_CREATED = 201 HTTP_UNPROCESSABLE_ENTITY = 422 -def _run_git(sandbox_backend: Any, repo_dir: str, command: str) -> Any: +def _run_git( + sandbox_backend: SandboxBackendProtocol, repo_dir: str, command: str +) -> ExecuteResponse: """Run a git command in the sandbox repo directory.""" return sandbox_backend.execute(f"cd {repo_dir} && {command}") -def repo_directory_exists(sandbox_backend: Any, repo_dir: str) -> bool: - """Check if repository directory exists.""" - safe_repo_dir = shlex.quote(repo_dir) - result = sandbox_backend.execute(f"test -d {safe_repo_dir} && echo exists") - return result.exit_code == 0 and "exists" in result.output - - -def is_valid_git_repo(sandbox_backend: Any, repo_dir: str) -> bool: +def is_valid_git_repo(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool: """Check if directory is a valid git repository.""" git_dir = f"{repo_dir}/.git" safe_git_dir = shlex.quote(git_dir) @@ -35,25 +30,31 @@ def is_valid_git_repo(sandbox_backend: Any, repo_dir: str) -> bool: return result.exit_code == 0 and "exists" in result.output -def remove_directory(sandbox_backend: Any, repo_dir: str) -> bool: +def remove_directory(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool: """Remove a directory and all its contents.""" safe_repo_dir = shlex.quote(repo_dir) result = sandbox_backend.execute(f"rm -rf {safe_repo_dir}") return result.exit_code == 0 -def git_has_uncommitted_changes(sandbox_backend: Any, repo_dir: str) -> bool: +def git_has_uncommitted_changes( + sandbox_backend: SandboxBackendProtocol, repo_dir: str +) -> bool: """Check whether the repo has uncommitted changes.""" result = _run_git(sandbox_backend, repo_dir, "git status --porcelain") return result.exit_code == 0 and bool(result.output.strip()) -def git_fetch_origin(sandbox_backend: Any, repo_dir: str) -> Any: +def git_fetch_origin( + sandbox_backend: SandboxBackendProtocol, repo_dir: str +) -> ExecuteResponse: """Fetch latest from origin (best-effort).""" return _run_git(sandbox_backend, repo_dir, "git fetch origin 2>/dev/null || true") -def git_has_unpushed_commits(sandbox_backend: Any, repo_dir: str) -> bool: +def git_has_unpushed_commits( + sandbox_backend: SandboxBackendProtocol, repo_dir: str +) -> bool: """Check whether there are commits not pushed to upstream.""" git_log_cmd = ( "git log --oneline @{upstream}..HEAD 2>/dev/null " @@ -63,13 +64,15 @@ def git_has_unpushed_commits(sandbox_backend: Any, repo_dir: str) -> bool: return result.exit_code == 0 and bool(result.output.strip()) -def git_current_branch(sandbox_backend: Any, repo_dir: str) -> str: +def git_current_branch(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> str: """Get the current git branch name.""" result = _run_git(sandbox_backend, repo_dir, "git rev-parse --abbrev-ref HEAD") return result.output.strip() if result.exit_code == 0 else "" -def git_checkout_branch(sandbox_backend: Any, repo_dir: str, branch: str) -> bool: +def git_checkout_branch( + sandbox_backend: SandboxBackendProtocol, repo_dir: str, branch: str +) -> bool: """Checkout branch, creating it if needed.""" safe_branch = shlex.quote(branch) checkout_result = _run_git( @@ -82,7 +85,7 @@ def git_checkout_branch(sandbox_backend: Any, repo_dir: str, branch: str) -> boo def git_config_user( - sandbox_backend: Any, + sandbox_backend: SandboxBackendProtocol, repo_dir: str, name: str, email: str, @@ -94,18 +97,24 @@ def git_config_user( _run_git(sandbox_backend, repo_dir, f"git config user.email {safe_email}") -def git_add_all(sandbox_backend: Any, repo_dir: str) -> Any: +def git_add_all( + sandbox_backend: SandboxBackendProtocol, repo_dir: str +) -> ExecuteResponse: """Stage all changes.""" return _run_git(sandbox_backend, repo_dir, "git add -A") -def git_commit(sandbox_backend: Any, repo_dir: str, message: str) -> Any: +def git_commit( + sandbox_backend: SandboxBackendProtocol, repo_dir: str, message: str +) -> ExecuteResponse: """Commit staged changes with the given message.""" safe_message = shlex.quote(message) return _run_git(sandbox_backend, repo_dir, f"git commit -m {safe_message}") -def git_get_remote_url(sandbox_backend: Any, repo_dir: str) -> str | None: +def git_get_remote_url( + sandbox_backend: SandboxBackendProtocol, repo_dir: str +) -> str | None: """Get the origin remote URL.""" result = _run_git(sandbox_backend, repo_dir, "git remote get-url origin") if result.exit_code != 0: @@ -114,11 +123,11 @@ def git_get_remote_url(sandbox_backend: Any, repo_dir: str) -> str | None: def git_push( - sandbox_backend: Any, + sandbox_backend: SandboxBackendProtocol, repo_dir: str, branch: str, github_token: str | None = None, -) -> Any: +) -> ExecuteResponse: """Push the branch to origin, using a token if needed.""" safe_branch = shlex.quote(branch) remote_url = git_get_remote_url(sandbox_backend, repo_dir)