From 0af5a8b00fbb47fd437e9d213c588cc747d57ef1 Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Mon, 20 Jul 2026 20:55:57 +0200 Subject: [PATCH] fix: make dev tools executable --- install-scripts/README.md | 6 ++++-- src/agentci/adapters/development.py | 5 ++--- tests/test_development.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/install-scripts/README.md b/install-scripts/README.md index 69a6383..5cf17dc 100644 --- a/install-scripts/README.md +++ b/install-scripts/README.md @@ -3,7 +3,9 @@ This directory supplies the ready-made `python` and `dotnet` scripts. They are not reserved: modify, replace, or remove them like any other script. Place other trusted executable install scripts here and add the desired file names to `AGENTCI_INSTALL_SCRIPTS`. Compose mounts the -directory read-only at `/etc/agentci/install-scripts`. +directory read-only at `/etc/agentci/install-scripts`. Executable files run directly; files without +executable mode run as POSIX shell scripts through `/bin/sh` so bind mounts do not depend on host +file-mode preservation. Scripts run from the cloned repository with a sanitized environment. They receive: @@ -19,4 +21,4 @@ not persist into implementation turns. The configured scripts run in list order after the repository is cloned (or an existing agent PR branch is synchronized) and before the first implementation turn for `implement`, `iterate`, and `fix`. They do not rerun between implementation and review-revision turns within one job. Scripts -must be executable, idempotent, and must not expect AgentCI or Gitea credentials. +must be idempotent and must not expect AgentCI or Gitea credentials. diff --git a/src/agentci/adapters/development.py b/src/agentci/adapters/development.py index 9449cc6..d30faec 100644 --- a/src/agentci/adapters/development.py +++ b/src/agentci/adapters/development.py @@ -55,19 +55,18 @@ class DevelopmentEnvironment: ) from exc if resolved.parent != self.scripts_dir.resolve() or not resolved.is_file(): raise DevelopmentEnvironmentError(f"Install script {name!r} is not a regular file") - if not os.access(resolved, os.X_OK): - raise DevelopmentEnvironmentError(f"Install script {name!r} is not executable") return resolved async def _run(self, name: str, script: Path, workspace: Path) -> None: started = monotonic() + command = [str(script)] if os.access(script, os.X_OK) else ["/bin/sh", str(script)] log.info( "development install script started", extra={"operation": "development.install", "script": name}, ) try: process = await asyncio.create_subprocess_exec( - str(script), + *command, cwd=workspace, env=self._environment(), stdout=asyncio.subprocess.PIPE, diff --git a/tests/test_development.py b/tests/test_development.py index d2480ba..3f78d0c 100644 --- a/tests/test_development.py +++ b/tests/test_development.py @@ -61,6 +61,19 @@ async def test_supplied_script_names_use_the_same_directory(tmp_path) -> None: assert (workspace / "selected").read_text() == "python\n" +async def test_runs_non_executable_shell_script_from_bind_mount(tmp_path) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + development = environment(tmp_path, ["mounted"]) + mounted = development.scripts_dir / "mounted" + mounted.write_text("#!/bin/sh\nprintf 'mounted\\n' > selected\n") + mounted.chmod(0o644) + + await development.prepare(workspace) + + assert (workspace / "selected").read_text() == "mounted\n" + + async def test_reports_script_failure_output(tmp_path) -> None: workspace = tmp_path / "workspace" workspace.mkdir()