from __future__ import annotations import os import subprocess import sys from pathlib import Path import yaml ROOT = Path(__file__).resolve().parents[1] def test_pi_repair_launcher_recovers_when_invoked_with_sh(): completed = subprocess.run( ["sh", str(ROOT / "repair-rpi-with-codex.sh"), "--help"], cwd=ROOT, capture_output=True, text=True, timeout=10, check=False, ) assert completed.returncode == 0, completed.stdout + completed.stderr assert "Usage: ./repair-rpi-with-codex.sh" in completed.stdout assert completed.stderr == "" def test_pi_repair_launcher_enables_managed_worktrees_for_current_codex_cli(): launcher = (ROOT / "repair-rpi-with-codex.sh").read_text(encoding="utf-8") assert "--enable worktrees" in launcher assert launcher.index("--enable worktrees") < launcher.index("--worktree") def test_compose_only_publishes_nginx_on_configured_lan_port(): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8")) services = compose["services"] assert "ports" not in services["api"] assert "ports" not in services["django-web"] assert services["nginx"]["ports"] == ["${CODEXPI_BIND_ADDRESS:-0.0.0.0}:${CODEXPI_PROXY_PORT:-8765}:8080"] assert "uvicorn django_ui.asgi:application" in services["django-web"]["command"] assert "uvicorn codexpi.main:app" in services["api"]["command"] def test_nginx_waits_for_application_dns_and_health_after_an_abrupt_reboot(): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8")) nginx = compose["services"]["nginx"] command = nginx["command"][-1] assert "http://$${django_upstream}:8001/api/bootstrap" in command assert "http://$${api_upstream}:8000/api/health" in command assert "until" in command assert nginx["healthcheck"]["test"][-1] == "http://127.0.0.1:8080/proxy-health" def test_api_allows_nested_codex_sandbox_without_dropping_other_guards(): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8")) api = compose["services"]["api"] # Bubblewrap needs unprivileged namespace syscalls inside Docker. The API # remains non-root, read-only, capability-free, and cannot gain privileges. assert "seccomp:unconfined" in api["security_opt"] assert "no-new-privileges:true" in api["security_opt"] assert api["read_only"] is True assert api["cap_drop"] == ["ALL"] def test_api_receives_the_current_docker_socket_group_without_extra_capabilities(): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8")) api = compose["services"]["api"] assert api["group_add"] == ["${DOCKER_GID:-998}"] assert "/var/run/docker.sock:/var/run/docker.sock" in api["volumes"] assert api["user"] == "${PUID:-1000}:${PGID:-1000}" assert api["cap_drop"] == ["ALL"] def test_api_receives_host_submounts_without_propagating_container_mounts_back(): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8")) api = compose["services"]["api"] projects_mount = next( mount for mount in api["volumes"] if isinstance(mount, dict) and mount.get("target") == "/home/config" ) assert projects_mount == { "type": "bind", "source": "/home/config", "target": "/home/config", "bind": {"propagation": "rslave"}, } def test_boot_service_refreshes_docker_access_as_the_non_root_host_user(): unit = (ROOT / "deploy" / "systemd" / "codexpi.service").read_text(encoding="utf-8") repair_unit = (ROOT / "deploy" / "systemd" / "codexpi-docker-access.service").read_text(encoding="utf-8") repair_timer = (ROOT / "deploy" / "systemd" / "codexpi-docker-access.timer").read_text(encoding="utf-8") compose_wrapper = (ROOT / "scripts" / "compose.sh").read_text(encoding="utf-8") repair_script = (ROOT / "scripts" / "ensure-docker-access.sh").read_text(encoding="utf-8") installer = (ROOT / "scripts" / "install-boot-service.sh").read_text(encoding="utf-8") install_script = (ROOT / "scripts" / "install.sh").read_text(encoding="utf-8") assert "BindsTo=docker.service" in unit assert "PartOf=docker.service" in unit assert "User=__CODEXPI_UID__" in unit assert "Group=__CODEXPI_GID__" in unit assert "SupplementaryGroups=__DOCKER_GROUP__" in unit assert "ExecStart=/home/config/CodexPi/scripts/compose.sh up -d" in unit assert 'stat -c \'%g\' "$docker_socket"' in compose_wrapper assert 'export DOCKER_GID=' in compose_wrapper assert "User=__CODEXPI_UID__" in repair_unit assert "SupplementaryGroups=__DOCKER_GROUP__" in repair_unit assert "NoNewPrivileges=true" in repair_unit assert "ConditionPathIsSocket=/var/run/docker.sock" in repair_unit assert "OnUnitActiveSec=60s" in repair_timer assert "docker inspect --format '{{json .HostConfig.GroupAdd}}'" in repair_script assert "codexpi\\.session_worker" in repair_script assert 'up -d --no-deps --force-recreate api' in repair_script assert "stat -c '%u' \"$BASE\"" in installer assert "s/__CODEXPI_UID__/$codexpi_uid/g" in installer assert "s/__DOCKER_GROUP__/$docker_group/g" in installer assert "enable --now codexpi-docker-access.timer" in installer assert '"$BASE/scripts/install-boot-service.sh"' in install_script def test_image_has_no_virtualenv_or_root_runtime(): dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") assert "python3 -m venv" not in dockerfile assert "USER node" in dockerfile assert "@openai/codex" in dockerfile def test_non_root_nginx_uses_writable_tmpfs_paths(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") for directive in ("client_body", "proxy", "fastcgi", "uwsgi", "scgi"): assert f"{directive}_temp_path /tmp/{directive};" in nginx assert "/var/cache/nginx/" not in nginx assert "location ^~ /api/conversations/" in nginx assert "client_max_body_size 101m;" in nginx assert "proxy_request_buffering off;" in nginx def test_large_conversation_responses_are_not_buffered_in_small_nginx_tmpfs(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") conversations_location = nginx.split("location ^~ /api/conversations/ {", 1)[1].split("}", 1)[0] assert "proxy_buffering off;" in conversations_location def test_loutre_finance_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /loutre-finance { return 308 /loutre-finance/; }" in nginx finance = nginx.split("location ^~ /loutre-finance/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://loutre-finance:8000;" in finance assert "proxy_set_header Cookie $http_cookie;" in finance assert "proxy_set_header X-Forwarded-Proto $forwarded_proto;" in finance def test_smart_inventory_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /smart-inventory { return 308 /smart-inventory/; }" in nginx inventory = nginx.split("location ^~ /smart-inventory/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://smart-inventory:8000/;" in inventory assert "proxy_set_header Cookie $http_cookie;" in inventory assert "proxy_set_header X-Forwarded-Proto $forwarded_proto;" in inventory def test_auto_care_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /auto-care { return 308 /auto-care/; }" in nginx auto_care = nginx.split("location ^~ /auto-care/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://auto-care:8000;" in auto_care assert "proxy_set_header Cookie $http_cookie;" in auto_care assert "proxy_set_header X-Forwarded-Proto $forwarded_proto;" in auto_care def test_print_pilot_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /print-pilot { return 308 /print-pilot/; }" in nginx print_pilot = nginx.split("location ^~ /print-pilot/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://print-pilot:8000;" in print_pilot assert "proxy_set_header Cookie $http_cookie;" in print_pilot assert 'proxy_set_header X-User "";' in print_pilot assert 'proxy_set_header X-Role "";' in print_pilot def test_loutre_home_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /loutre-home { return 308 /loutre-home/; }" in nginx loutre_home = nginx.split("location ^~ /loutre-home/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://loutre-home:8017;" in loutre_home assert "proxy_set_header Cookie $http_cookie;" in loutre_home assert 'proxy_set_header X-User "";' in loutre_home assert 'proxy_set_header X-Role "";' in loutre_home def test_network_guardian_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /network-guardian { return 308 /network-guardian/; }" in nginx guardian = nginx.split("location ^~ /network-guardian/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://network-guardian:8897/;" in guardian assert "proxy_set_header Cookie $http_cookie;" in guardian assert 'proxy_set_header X-User "";' in guardian assert 'proxy_set_header X-Role "";' in guardian def test_reverse_proxy_manager_has_a_dedicated_private_application_route(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location = /reverse-proxy-manager { return 308 /reverse-proxy-manager/; }" in nginx manager = nginx.split("location ^~ /reverse-proxy-manager/ {", 1)[1].split("}", 1)[0] assert "proxy_pass http://reverse-proxy-manager:8000;" in manager assert "proxy_set_header Cookie $http_cookie;" in manager assert 'proxy_set_header X-User "";' in manager assert 'proxy_set_header X-Role "";' in manager assert "proxy_hide_header Set-Cookie;" in manager def test_application_routes_use_independent_app_store_icon_namespace(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") assert "location ^~ /loutre-app-store/app-icons/ {" in nginx for slug in ( "loutre-os", "loutre-finance", "smart-inventory", "auto-care", "print-pilot", "loutre-home", "network-guardian", "reverse-proxy-manager", "loutre-brain", "knowledge-graph", "trip-organizer", ): assert f"location ^~ /{slug}/ {{" in nginx def test_last_three_ecosystem_apps_have_private_application_routes(): nginx = (ROOT / "deploy" / "nginx" / "nginx.conf").read_text(encoding="utf-8") for slug, upstream in ( ("loutre-brain", "http://loutre-brain:8000"), ("knowledge-graph", "http://knowledge-graph:8017"), ("trip-organizer", "http://trip-organizer:8000"), ): assert f"location = /{slug} {{ return 308 /{slug}/; }}" in nginx route = nginx.split(f"location ^~ /{slug}/ {{", 1)[1].split("}", 1)[0] assert f"proxy_pass {upstream};" in route assert "proxy_set_header Cookie $http_cookie;" in route assert 'proxy_set_header X-User "";' in route assert 'proxy_set_header X-Role "";' in route def test_django_login_cookie_and_fastapi_session_bridge(tmp_path: Path): script = r'''import json import os import django django.setup() from pathlib import Path from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import Client from django_ui.identity.models import ApplicationGrant from fastapi.testclient import TestClient from starlette.websockets import WebSocketDisconnect from codexpi.config import Settings from codexpi.django_session import DjangoSessionAuth from codexpi.main import create_app call_command("migrate", interactive=False, verbosity=0) get_user_model().objects.create_superuser("admin", "admin@example.invalid", "a-very-long-test-password") client = Client(enforce_csrf_checks=True) page = client.get("/", secure=True, HTTP_HOST="codexpi.example-tailnet.ts.net") assert page.status_code == 200, page.content assert "img-src 'self' data: blob:" in page["Content-Security-Policy"] bootstrap = client.get("/api/bootstrap", secure=True, HTTP_HOST="testserver") assert bootstrap.status_code == 200, bootstrap.content csrf = bootstrap.json()["csrf_token"] login = client.post( "/api/auth/login", data=json.dumps({"username": "admin", "password": "a-very-long-test-password"}), content_type="application/json", secure=True, HTTP_HOST="testserver", HTTP_ORIGIN="https://testserver", HTTP_X_CSRF_TOKEN=csrf, ) assert login.status_code == 200, login.content ApplicationGrant.objects.create(user=get_user_model().objects.get(username="admin"), application="auto-care", role="admin") identity = client.get( "/api/auth/applications/auto-care", secure=True, HTTP_HOST="testserver", ) assert identity.status_code == 200, identity.content assert identity.json()["permissions"] == ["read", "write", "admin"] assert client.get( "/api/auth/applications/print-pilot", secure=True, HTTP_HOST="testserver", ).status_code == 403 assert client.get( "/api/auth/applications/not-an-app", secure=True, HTTP_HOST="testserver", ).status_code == 404 cookie = client.cookies["codexpi_session"] assert cookie["secure"] assert cookie["httponly"] assert cookie["samesite"] == "Strict" session = DjangoSessionAuth().session_user(cookie.value) assert session is not None assert session[0]["username"] == "admin" assert DjangoSessionAuth.csrf_matches(login.json()["csrf_token"], session[1]) audit = (os.path.join(os.environ["CODEXPI_DATA_DIR"], "security.log")) assert cookie.value not in open(audit, encoding="utf-8").read() data_dir = Path(os.environ["CODEXPI_DATA_DIR"]) fastapi_app = create_app(Settings( base_dir=data_dir, data_dir=data_dir, database_path=data_dir / "fastapi.sqlite3", host="127.0.0.1", port=8000, cookie_name="codexpi_session", cookie_secure=True, session_hours=12, allowed_roots=(data_dir,), forbidden_roots=(), codex_bin="definitely-missing-codex", frontend_dist=data_dir / "missing-frontend", trusted_origins=("https://codexpi.example-tailnet.ts.net",), auth_backend="django", containerized=True, )) with TestClient(fastapi_app) as api_client: api_client.cookies.set("codexpi_session", cookie.value) app_store = api_client.get("/loutre-app-store/") assert app_store.status_code == 200, app_store.content assert "Loutre App Store" in app_store.text try: with api_client.websocket_connect( "/ws/conversations/does-not-exist", headers={"origin": "https://codexpi.example-tailnet.ts.net"}, ): raise AssertionError("Missing conversation socket unexpectedly opened") except WebSocketDisconnect as exc: assert exc.code == 4404, exc.code logout = client.post( "/api/auth/logout", data="{}", content_type="application/json", secure=True, HTTP_HOST="testserver", HTTP_ORIGIN="https://testserver", HTTP_X_CSRF_TOKEN=login.json()["csrf_token"], ) assert logout.status_code == 200, logout.content ''' env = os.environ.copy() env.update( { "DJANGO_SETTINGS_MODULE": "django_ui.settings", "CODEXPI_DATA_DIR": str(tmp_path), "CODEXPI_COOKIE_SECURE": "true", "CODEXPI_DJANGO_ALLOWED_HOSTS": "testserver,.ts.net", "PYTHONPATH": os.pathsep.join((str(ROOT), str(ROOT / "backend"))), } ) completed = subprocess.run( [sys.executable, "-c", script], cwd=ROOT, env=env, capture_output=True, text=True, timeout=60, check=False, ) assert completed.returncode == 0, completed.stdout + completed.stderr