From ed7b88ade4e1fc32f1d43ff7acdcce3305033dd3 Mon Sep 17 00:00:00 2001 From: Christian Meyer Date: Fri, 21 Aug 2026 23:17:43 +0200 Subject: [PATCH] Harden Aider workspace handoff --- docker-compose.yml | 1 + src/lai_automation/agent.py | 55 +++++++++++++++++++++++-------------- src/lai_automation/git.py | 2 ++ tests/test_agent.py | 14 ++++++++++ 4 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 tests/test_agent.py diff --git a/docker-compose.yml b/docker-compose.yml index e01f958..3e23f5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: GITEA_REPOSITORY: ${GITEA_REPOSITORY:-lai} GITEA_USERNAME: ${GITEA_USERNAME:-automation-bot} GITEA_TOKEN: ${GITEA_TOKEN:-} + AGENT_COMMAND: ${AGENT_COMMAND:-aider} AGENT_MODEL: ${AGENT_MODEL:-mistral/mistral-medium-latest} MISTRAL_API_KEY: ${MISTRAL_API_KEY:-} POLL_SECONDS: ${POLL_SECONDS:-60} diff --git a/src/lai_automation/agent.py b/src/lai_automation/agent.py index 02547c5..d18b0dc 100644 --- a/src/lai_automation/agent.py +++ b/src/lai_automation/agent.py @@ -16,17 +16,7 @@ class AiderAgent: task_file = workspace / ".git" / "failure-task.txt" task_file.write_text(task) task_file.chmod(0o600) - command = [ - self.settings.agent_command, - "--model", - self.settings.agent_model, - "--message-file", - str(task_file), - "--yes", - "--no-auto-commits", - "--no-dirty-commits", - "--no-stream", - ] + command = self.command(task_file) coder = pwd.getpwnam("coder") os.chown(task_file, coder.pw_uid, coder.pw_gid) for path in workspace.rglob("*"): @@ -47,16 +37,39 @@ class AiderAgent: os.setgid(coder.pw_gid) os.setuid(coder.pw_uid) - result = subprocess.run( - command, - cwd=workspace, - env=environment, - text=True, - capture_output=True, - check=False, - preexec_fn=drop_privileges, - timeout=3600, - ) + try: + result = subprocess.run( + command, + cwd=workspace, + env=environment, + text=True, + capture_output=True, + check=False, + preexec_fn=drop_privileges, + timeout=900, + ) + finally: + os.chown(workspace, 0, coder.pw_gid, follow_symlinks=False) + workspace.chmod(0o775) if result.returncode != 0: raise RuntimeError(f"Aider failed ({result.returncode}): {result.stderr[-4000:]}") return result.stdout[-12_000:] + + def command(self, task_file: Path) -> list[str]: + return [ + self.settings.agent_command, + "--model", + self.settings.agent_model, + "--message-file", + str(task_file), + "--yes", + "--no-auto-commits", + "--no-dirty-commits", + "--no-stream", + "--no-gitignore", + "--chat-history-file", + "/dev/null", + "--input-history-file", + "/dev/null", + "--no-analytics", + ] diff --git a/src/lai_automation/git.py b/src/lai_automation/git.py index 6af40d7..d26e605 100644 --- a/src/lai_automation/git.py +++ b/src/lai_automation/git.py @@ -37,6 +37,8 @@ class GitWorkspace: shutil.rmtree(self.path, ignore_errors=True) self.path.parent.mkdir(parents=True, exist_ok=True) self._git("clone", "--branch", self.settings.source_branch, "--single-branch", self.settings.source_repository, str(self.path), cwd=self.path.parent) + self._git("config", "user.name", "LAI Automation") + self._git("config", "user.email", "automation@localhost") self._git("checkout", "-b", self.branch) def run( diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000..2148658 --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from test_config import settings + +from lai_automation.agent import AiderAgent + + +def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> None: + command = AiderAgent(settings()).command(Path("/repo/.git/task.txt")) + + assert "--no-gitignore" in command + assert "--no-analytics" in command + assert "--no-auto-commits" in command + assert command.count("/dev/null") == 2 \ No newline at end of file