From cb3833838ecbb4d19036fc7e91cc80e912c6c899 Mon Sep 17 00:00:00 2001 From: carlosjarenom Date: Sun, 19 Jul 2026 02:14:23 +0200 Subject: [PATCH 1/2] fix: ensure GIFs loop infinitely at generation level (fixes #1480) - Add ensure_gif_infinite_loop() and post_process_gifs_in_directory() in file_utils.py - Hook into TerminalToolkit.shell_exec() after blocking commands - Hook into chat_service.collect_previous_task_context() as safety net - Re-saves GIFs with loop=0 (infinite) using PIL, preserving frame durations/disposal - Removes invalid loop='infinite' attribute from MarkDown.tsx (was wrong approach per #1676 review) Fixes #1480 --- backend/app/agent/toolkit/terminal_toolkit.py | 12 +++ backend/app/service/chat_service.py | 4 +- backend/app/utils/file_utils.py | 84 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/backend/app/agent/toolkit/terminal_toolkit.py b/backend/app/agent/toolkit/terminal_toolkit.py index fcd303a58..9d75f9c99 100644 --- a/backend/app/agent/toolkit/terminal_toolkit.py +++ b/backend/app/agent/toolkit/terminal_toolkit.py @@ -35,6 +35,7 @@ get_task_lock, process_task, ) +from app.utils.file_utils import post_process_gifs_in_directory from app.utils.listen.toolkit_listen import auto_listen_toolkit logger = logging.getLogger("terminal_toolkit") @@ -384,6 +385,17 @@ def shell_exec( if block and result == "": return "Command executed successfully (no output)." + # Post-process GIF files to ensure infinite loop + if self.working_directory and block: + try: + modified = post_process_gifs_in_directory(self.working_directory) + if modified: + logger.debug( + f"Post-processed {modified} GIF file(s) for infinite loop" + ) + except Exception as e: + logger.warning(f"Failed to post-process GIFs: {e}") + return result def cleanup(self, remove_venv: bool = True): diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 2b977df8c..6f3fc8446 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -82,7 +82,7 @@ record_workforce_memory_snapshot, ) from app.utils.event_loop_utils import set_main_event_loop -from app.utils.file_utils import get_working_directory, list_files +from app.utils.file_utils import get_working_directory, list_files, post_process_gifs_in_directory from app.utils.server.sync_step import sync_step from app.utils.telemetry.workforce_metrics import WorkforceMetricsCallback from app.utils.workforce import Workforce @@ -257,6 +257,8 @@ def collect_previous_task_context( skip_extensions=(".pyc", ".tmp"), skip_prefix=".", ) + # Post-process GIFs to ensure infinite loop + post_process_gifs_in_directory(working_directory) if generated_files: context_parts.append("Generated Files from Previous Task:") for file_path in sorted(generated_files): diff --git a/backend/app/utils/file_utils.py b/backend/app/utils/file_utils.py index 032e3a49c..3d13aef86 100644 --- a/backend/app/utils/file_utils.py +++ b/backend/app/utils/file_utils.py @@ -348,3 +348,87 @@ def sync_eigent_skills_to_project(working_directory: str) -> None: e, exc_info=True, ) + + +def ensure_gif_infinite_loop(file_path: str) -> bool: + """ + Ensure a GIF file has infinite loop (loop=0 in PIL). + + Args: + file_path: Path to the GIF file + + Returns: + True if file was modified, False otherwise + """ + try: + from PIL import Image + except ImportError: + logger.warning("PIL not available, cannot fix GIF loop") + return False + + if not os.path.exists(file_path): + return False + + if not file_path.lower().endswith('.gif'): + return False + + try: + with Image.open(file_path) as img: + current_loop = img.info.get('loop', 1) + # In PIL, loop=0 means infinite loop + if current_loop == 0: + return False # Already infinite + + # Need to re-save with loop=0 + # GIFs can have multiple frames, so we need to handle that + frames = [] + durations = [] + try: + while True: + frames.append(img.copy()) + durations.append(img.info.get('duration', 100)) + img.seek(img.tell() + 1) + except EOFError: + pass + + if not frames: + return False + + # Save with infinite loop + frames[0].save( + file_path, + format='GIF', + save_all=True, + append_images=frames[1:] if len(frames) > 1 else [], + duration=durations, + loop=0, # 0 = infinite loop + disposal=img.info.get('disposal', 2), + ) + logger.debug(f"Fixed GIF loop for {file_path}") + return True + except Exception as e: + logger.warning(f"Failed to fix GIF loop for {file_path}: {e}") + return False + + +def post_process_gifs_in_directory(directory: str) -> int: + """ + Post-process all GIF files in a directory to ensure infinite loop. + + Args: + directory: Directory to scan for GIF files + + Returns: + Number of files modified + """ + if not os.path.isdir(directory): + return 0 + + modified = 0 + for root, _, files in os.walk(directory): + for file in files: + if file.lower().endswith('.gif'): + file_path = os.path.join(root, file) + if ensure_gif_infinite_loop(file_path): + modified += 1 + return modified From 7d2aa721c6eafc3039ffb0d67b37322fe4e53b56 Mon Sep 17 00:00:00 2001 From: carlosjarenom Date: Sun, 19 Jul 2026 02:18:54 +0200 Subject: [PATCH 2/2] fix: avoid baking database_url into Docker image metadata (addresses #1677 review) - build.args: use non-secret placeholder for database_url - environment: keep runtime substitution with ${POSTGRES_PASSWORD:-123456} - Both docker-compose.yml and docker-compose.dev.yml updated Fixes credential leak risk in image metadata (per @4pmtong review). --- server/docker-compose.dev.yml | 2 +- server/docker-compose.yml | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/server/docker-compose.dev.yml b/server/docker-compose.dev.yml index 15685f034..6275864be 100644 --- a/server/docker-compose.dev.yml +++ b/server/docker-compose.dev.yml @@ -7,7 +7,7 @@ services: environment: POSTGRES_DB: eigent POSTGRES_USER: postgres - POSTGRES_PASSWORD: 123456 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456} POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C' ports: - '5432:5432' diff --git a/server/docker-compose.yml b/server/docker-compose.yml index a777a7906..56dd5c815 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -7,7 +7,7 @@ services: environment: POSTGRES_DB: eigent POSTGRES_USER: postgres - POSTGRES_PASSWORD: 123456 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456} POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --lc-collate=C --lc-ctype=C' ports: - '5432:5432' @@ -44,7 +44,8 @@ services: context: .. dockerfile: server/Dockerfile args: - database_url: postgresql://postgres:123456@postgres:5432/eigent + # Placeholder for build arg (baked into image metadata) — no secrets + database_url: postgresql://postgres:placeholder@postgres:5432/eigent container_name: eigent_api restart: unless-stopped ports: @@ -52,7 +53,8 @@ services: env_file: - .env environment: - - database_url=postgresql://postgres:123456@postgres:5432/eigent + # Runtime substitution (from .env or POSTGRES_PASSWORD env var) + - database_url=postgresql://postgres:${POSTGRES_PASSWORD:-123456}@postgres:5432/eigent - redis_url=redis://redis:6379/0 - celery_broker_url=redis://redis:6379/0 - celery_result_url=redis://redis:6379/0 @@ -82,14 +84,16 @@ services: context: .. dockerfile: server/Dockerfile args: - database_url: postgresql://postgres:123456@postgres:5432/eigent + # Placeholder for build arg (baked into image metadata) — no secrets + database_url: postgresql://postgres:placeholder@postgres:5432/eigent container_name: eigent_celery_worker restart: unless-stopped command: /app/celery/worker/start env_file: - .env environment: - - database_url=postgresql://postgres:123456@postgres:5432/eigent + # Runtime substitution (from .env or POSTGRES_PASSWORD env var) + - database_url=postgresql://postgres:${POSTGRES_PASSWORD:-123456}@postgres:5432/eigent - redis_url=redis://redis:6379/0 - celery_broker_url=redis://redis:6379/0 - celery_result_url=redis://redis:6379/0 @@ -120,14 +124,16 @@ services: context: .. dockerfile: server/Dockerfile args: - database_url: postgresql://postgres:123456@postgres:5432/eigent + # Placeholder for build arg (baked into image metadata) — no secrets + database_url: postgresql://postgres:placeholder@postgres:5432/eigent container_name: eigent_celery_beat restart: unless-stopped command: /app/celery/beat/start env_file: - .env environment: - - database_url=postgresql://postgres:123456@postgres:5432/eigent + # Runtime substitution (from .env or POSTGRES_PASSWORD env var) + - database_url=postgresql://postgres:${POSTGRES_PASSWORD:-123456}@postgres:5432/eigent - redis_url=redis://redis:6379/0 - celery_broker_url=redis://redis:6379/0 - celery_result_url=redis://redis:6379/0