From efce42b967f9b220f79bedaad4809d3eb4fa9e09 Mon Sep 17 00:00:00 2001 From: Christian Meyer Date: Thu, 27 Aug 2026 18:17:39 +0200 Subject: [PATCH] Isolate database credentials for agent tests --- README.md | 6 ++++++ docker-compose.yml | 7 +++++++ src/lai_automation/config.py | 16 ++++++++++++++++ src/lai_automation/git.py | 6 +++++- src/lai_automation/worker.py | 29 ++++++++++++++++++++++++++++- tests/test_agent.py | 25 ++++++++++++++++++++++++- tests/test_config.py | 7 +++++++ tests/test_worker.py | 17 +++++++++++++++++ 8 files changed, 110 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0b387a..68de25e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,11 @@ als privilegierter Containerprozess, startet Aider aber als separaten Benutzer ` Dieser Unterprozess erhaelt nur Modell-Credentials, nicht die Failure-DB- oder Gitea-Zugangsdaten. +Die nachgelagerten Testprozesse erhalten separat konfigurierte `TEST_DB_*`-Zugangsdaten. +Dieser Benutzer darf ausschliesslich die isolierten Testschemas bearbeiten. Fuer Laravel +werden daraus `DB_*`, `ERP_DB_*` und `COCKPIT_CONFIG_DATABASE` gesetzt; Aider selbst sieht +diese Werte nie. Produktive ERP- oder Kundenzugangsdaten duerfen hier nicht verwendet werden. + Laravel-Tests laufen mit PHP und Composer direkt im isolierten Worker. Der Docker-Socket des Hosts wird nicht eingebunden. Abgebrochene Claims werden nach standardmaessig 90 Minuten wieder freigegeben; `STALE_CLAIM_MINUTES` passt dieses Intervall an. @@ -48,6 +53,7 @@ Minuten wieder freigegeben; `STALE_CLAIM_MINUTES` passt dieses Intervall an. ```bash cp .env.example .env # FAILURE_DB_PASSWORD und bei Bedarf MISTRAL_API_KEY eintragen +# TEST_DB_PASSWORD fuer den Benutzer der isolierten Testschemas eintragen docker compose build docker compose run --rm automation check docker compose run --rm automation once diff --git a/docker-compose.yml b/docker-compose.yml index b23c8e3..91c63bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,13 @@ services: POLL_SECONDS: ${POLL_SECONDS:-60} STALE_CLAIM_MINUTES: ${STALE_CLAIM_MINUTES:-90} TEST_COMMANDS: ${TEST_COMMANDS:-composer install --no-interaction --prefer-dist;;php artisan test} + TEST_DB_HOST: ${TEST_DB_HOST:-host.docker.internal} + TEST_DB_PORT: ${TEST_DB_PORT:-33060} + TEST_DB_NAME: ${TEST_DB_NAME:-laravel_ai_chat_agent_test} + TEST_DB_USER: ${TEST_DB_USER:-lai_test_runner} + TEST_DB_PASSWORD: ${TEST_DB_PASSWORD:-agent_test_secret} + TEST_CUSTOMER_DB: ${TEST_CUSTOMER_DB:-customer_agent_test} + TEST_CONFIG_DB: ${TEST_CONFIG_DB:-cockpit_agent_test} extra_hosts: - host.docker.internal:host-gateway volumes: diff --git a/src/lai_automation/config.py b/src/lai_automation/config.py index a4e3888..122e249 100644 --- a/src/lai_automation/config.py +++ b/src/lai_automation/config.py @@ -33,6 +33,13 @@ class Settings: agent_command: str agent_model: str test_commands: tuple[str, ...] + test_database_host: str + test_database_port: int + test_database_name: str + test_database_user: str + test_database_password: str + test_customer_database: str + test_config_database: str poll_seconds: int max_changed_files: int max_changed_lines: int @@ -71,6 +78,13 @@ class Settings: "composer install --no-interaction --prefer-dist;;php artisan test", ) ), + test_database_host=os.getenv("TEST_DB_HOST", "host.docker.internal"), + test_database_port=int(os.getenv("TEST_DB_PORT", "33060")), + test_database_name=os.getenv("TEST_DB_NAME", "laravel_ai_chat_agent_test"), + test_database_user=os.getenv("TEST_DB_USER", "lai_test_runner"), + test_database_password=os.getenv("TEST_DB_PASSWORD", ""), + test_customer_database=os.getenv("TEST_CUSTOMER_DB", "customer_agent_test"), + test_config_database=os.getenv("TEST_CONFIG_DB", "cockpit_agent_test"), poll_seconds=int(os.getenv("POLL_SECONDS", "60")), max_changed_files=int(os.getenv("MAX_CHANGED_FILES", "12")), max_changed_lines=int(os.getenv("MAX_CHANGED_LINES", "800")), @@ -87,3 +101,5 @@ class Settings: raise ValueError(f"{self.mode} requires GITEA_TOKEN") if not self.database_password: raise ValueError("FAILURE_DB_PASSWORD is required") + if not self.test_database_password: + raise ValueError("TEST_DB_PASSWORD is required") diff --git a/src/lai_automation/git.py b/src/lai_automation/git.py index d26e605..4f68059 100644 --- a/src/lai_automation/git.py +++ b/src/lai_automation/git.py @@ -5,6 +5,7 @@ import pwd import shlex import shutil import subprocess +from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path from urllib.parse import urlparse @@ -46,8 +47,9 @@ class GitWorkspace: command: str, check: bool = True, username: str | None = None, + extra_environment: Mapping[str, str] | None = None, ) -> subprocess.CompletedProcess[str]: - environment = None + environment = dict(os.environ) drop_privileges = None if username: account = pwd.getpwnam(username) @@ -61,6 +63,8 @@ class GitWorkspace: os.setgid(account.pw_gid) os.setuid(account.pw_uid) + environment.update(extra_environment or {}) + return subprocess.run( shlex.split(command), cwd=self.path, diff --git a/src/lai_automation/worker.py b/src/lai_automation/worker.py index 0849ee1..7575591 100644 --- a/src/lai_automation/worker.py +++ b/src/lai_automation/worker.py @@ -99,8 +99,35 @@ class AutomationWorker: def _run_tests(self, workspace: GitWorkspace) -> list[dict]: results = [] + test_environment = { + "AGENT_TEST_DB_ENABLED": "true", + "TEST_DB_HOST": self.settings.test_database_host, + "TEST_DB_PORT": str(self.settings.test_database_port), + "TEST_DB_NAME": self.settings.test_database_name, + "TEST_DB_USER": self.settings.test_database_user, + "TEST_DB_PASSWORD": self.settings.test_database_password, + "TEST_CUSTOMER_DB": self.settings.test_customer_database, + "TEST_CONFIG_DB": self.settings.test_config_database, + "DB_CONNECTION": "mysql", + "DB_HOST": self.settings.test_database_host, + "DB_PORT": str(self.settings.test_database_port), + "DB_DATABASE": self.settings.test_database_name, + "DB_USERNAME": self.settings.test_database_user, + "DB_PASSWORD": self.settings.test_database_password, + "ERP_DB_HOST": self.settings.test_database_host, + "ERP_DB_PORT": str(self.settings.test_database_port), + "ERP_DATABASE": self.settings.test_customer_database, + "ERP_DB_USERNAME": self.settings.test_database_user, + "ERP_DB_PASSWORD": self.settings.test_database_password, + "COCKPIT_CONFIG_DATABASE": self.settings.test_config_database, + } for command in self.settings.test_commands: - result = workspace.run(command, check=False, username="coder") + result = workspace.run( + command, + check=False, + username="coder", + extra_environment=test_environment, + ) results.append({ "command": command, "returncode": result.returncode, diff --git a/tests/test_agent.py b/tests/test_agent.py index cf9c7ee..9b54212 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -12,4 +12,27 @@ def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> No assert "--no-analytics" in command assert "--no-auto-commits" in command assert command.count("/dev/null") == 2 - assert command[-4:] == ["--edit-format", "diff", "--timeout", "60"] \ No newline at end of file + assert command[-4:] == ["--edit-format", "diff", "--timeout", "60"] + + +def test_aider_environment_does_not_include_database_credentials(tmp_path, monkeypatch) -> None: + config = settings(workspace_root=tmp_path) + workspace = tmp_path / "repo" + (workspace / ".git").mkdir(parents=True) + monkeypatch.setenv("TEST_DB_PASSWORD", "must-not-reach-aider") + captured = {} + + def run(command, **kwargs): + captured.update(kwargs["env"]) + return type("Result", (), {"returncode": 0, "stdout": "ok", "stderr": ""})() + + monkeypatch.setattr("lai_automation.agent.subprocess.run", run) + monkeypatch.setattr("lai_automation.agent.os.chown", lambda *args, **kwargs: None) + monkeypatch.setattr("lai_automation.agent.pwd.getpwnam", lambda _: type( + "Account", (), {"pw_dir": str(tmp_path), "pw_uid": 10001, "pw_gid": 10001} + )()) + + AiderAgent(config).run(workspace, "fix it") + + assert "TEST_DB_PASSWORD" not in captured + assert "DB_PASSWORD" not in captured \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index df8690f..642c72b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -25,6 +25,13 @@ def settings(**overrides) -> Settings: "agent_command": "aider", "agent_model": "mistral/test", "test_commands": ("php artisan test",), + "test_database_host": "test-db", + "test_database_port": 3306, + "test_database_name": "lai_agent_test", + "test_database_user": "test_runner", + "test_database_password": "test-secret", + "test_customer_database": "customer_agent_test", + "test_config_database": "cockpit_agent_test", "poll_seconds": 60, "max_changed_files": 12, "max_changed_lines": 800, diff --git a/tests/test_worker.py b/tests/test_worker.py index 084bb51..c50bf35 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -35,3 +35,20 @@ def test_stop_signal_interrupts_worker_wait() -> None: worker._stop(15, None) assert worker.stop_event.is_set() + + +def test_database_credentials_are_only_passed_to_test_process() -> None: + worker = AutomationWorker(settings()) + workspace = Mock() + workspace.run.return_value = Mock(returncode=0, stdout="ok", stderr="") + + results = worker._run_tests(workspace) + + assert results[0]["returncode"] == 0 + environment = workspace.run.call_args.kwargs["extra_environment"] + assert environment["AGENT_TEST_DB_ENABLED"] == "true" + assert environment["TEST_DB_USER"] == "test_runner" + assert environment["TEST_DB_PASSWORD"] == "test-secret" + assert environment["DB_DATABASE"] == "lai_agent_test" + assert environment["ERP_DATABASE"] == "customer_agent_test" + assert environment["COCKPIT_CONFIG_DATABASE"] == "cockpit_agent_test"