From e1a55759fa42c2be6a94555fc45ea3a03121e7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Fri, 8 May 2026 18:23:39 +0900 Subject: [PATCH 01/17] Potential fix for code scanning alert no. 2: Binding a socket to all network interfaces (#5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Potential fix for code scanning alert no. 2: Binding a socket to all network interfaces 수정해줘 Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Fix profiler backend default host to loopback --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- run_profiler_backend.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/run_profiler_backend.py b/run_profiler_backend.py index efceebd..fa3869f 100644 --- a/run_profiler_backend.py +++ b/run_profiler_backend.py @@ -24,8 +24,6 @@ def _default_profiler_host() -> str: - if os.path.exists("/.dockerenv") or os.getenv("DOTNET_RUNNING_IN_CONTAINER") == "true": - return "0.0.0.0" return "127.0.0.1" From 5b48abef7475590a706a717ab6ff64b9a9c96ae4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 18:35:46 +0900 Subject: [PATCH 02/17] fix: harden auth recovery security (#6) Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/3d52b2c5-9c5a-43f7-91a5-9587bc9ce9a5 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- backend/auth_router.py | 71 ++++++++++-- tests/test_auth_router_security.py | 170 +++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 tests/test_auth_router_security.py diff --git a/backend/auth_router.py b/backend/auth_router.py index d104cf8..0fb4767 100644 --- a/backend/auth_router.py +++ b/backend/auth_router.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta -from secrets import token_urlsafe +from secrets import compare_digest, randbelow, token_urlsafe import base64 import os from urllib.parse import urlparse from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm -from typing import Optional +from typing import Optional, cast from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session from webauthn import ( @@ -39,10 +39,19 @@ from backend.models import User router = APIRouter() +_PASSWORD_RECOVERY_ALLOWED_SCOPES = {"admin", "user"} +_PASSWORD_RECOVERY_MAX_VERIFY_ATTEMPTS = max( + 1, + int(os.getenv("PASSWORD_RECOVERY_MAX_VERIFY_ATTEMPTS", "5")), +) def _should_issue_non_expiring_admin_token(user: User) -> bool: - return bool( + allow_non_expiring_admin_tokens = ( + str(os.getenv("ALLOW_NON_EXPIRING_ADMIN_TOKENS") or "").strip().lower() + in {"1", "true", "yes", "on"} + ) + return allow_non_expiring_admin_tokens and bool( getattr(user, "is_admin", False) or getattr(user, "is_superuser", False) ) @@ -245,12 +254,36 @@ class PasswordRecoveryResetResponse(BaseModel): def _issue_recovery_token(prefix: str) -> tuple[str, datetime]: - from secrets import token_urlsafe - expires_at = datetime.utcnow() + timedelta(minutes=10) return f"{prefix}_{token_urlsafe(24)}", expires_at +def _issue_recovery_verification_code() -> str: + return f"{randbelow(1_000_000):06d}" + + +def _normalize_password_recovery_scope(scope: str) -> str: + normalized = str(scope or "admin").strip().lower() + if normalized not in _PASSWORD_RECOVERY_ALLOWED_SCOPES: + raise HTTPException( + status_code=400, + detail="복구 범위는 admin 또는 user만 지원합니다", + ) + return normalized + + +def _purge_expired_password_recovery_sessions() -> None: + now = datetime.utcnow() + expired_tokens = [ + session_token + for session_token, session_state in _password_recovery_store.items() + if not isinstance(session_state.get("expires_at"), datetime) + or cast(datetime, session_state["expires_at"]) <= now + ] + for session_token in expired_tokens: + _password_recovery_store.pop(session_token, None) + + # ---------- 엔드포인트 ---------- @router.post("/signup", response_model=UserResponse, status_code=201) def signup(payload: UserCreate, db: Session = Depends(get_db)): @@ -511,6 +544,8 @@ def start_password_recovery( payload: PasswordRecoveryStartRequest, db: Session = Depends(get_db), ): + _purge_expired_password_recovery_sessions() + scope = _normalize_password_recovery_scope(payload.scope) user = db.query(User).filter( (User.email == payload.user_hint) | (User.username == payload.user_hint) @@ -519,15 +554,16 @@ def start_password_recovery( if user is None: raise HTTPException(status_code=404, detail="일치하는 계정을 찾을 수 없습니다") - if payload.scope == "admin" and not (getattr(user, "is_admin", False) or getattr(user, "is_superuser", False)): + if scope == "admin" and not (getattr(user, "is_admin", False) or getattr(user, "is_superuser", False)): raise HTTPException(status_code=403, detail="관리자 계정만 이 복구 경로를 사용할 수 있습니다") recovery_session_token, expires_at = _issue_recovery_token("recovery") _password_recovery_store[recovery_session_token] = { "user_id": int(user.id), - "scope": payload.scope, + "scope": scope, "verified": False, - "verification_code": "000000", + "verification_code": _issue_recovery_verification_code(), + "verification_attempts": 0, "expires_at": expires_at, } return { @@ -539,6 +575,7 @@ def start_password_recovery( @router.post("/recovery/verify-identity", response_model=PasswordRecoveryVerifyIdentityResponse) def verify_password_recovery_identity(payload: PasswordRecoveryVerifyIdentityRequest): + _purge_expired_password_recovery_sessions() session_state = _password_recovery_store.get(payload.recovery_session_token) if not session_state: raise HTTPException(status_code=404, detail="복구 세션을 찾을 수 없습니다") @@ -549,7 +586,12 @@ def verify_password_recovery_identity(payload: PasswordRecoveryVerifyIdentityReq raise HTTPException(status_code=410, detail="복구 세션이 만료되었습니다") expected_code = str(session_state.get("verification_code") or "") - if payload.verification_code.strip() != expected_code: + if not compare_digest(payload.verification_code.strip(), expected_code): + attempts = int(session_state.get("verification_attempts") or 0) + 1 + session_state["verification_attempts"] = attempts + if attempts >= _PASSWORD_RECOVERY_MAX_VERIFY_ATTEMPTS: + _password_recovery_store.pop(payload.recovery_session_token, None) + raise HTTPException(status_code=429, detail="본인확인 시도 횟수를 초과했습니다") raise HTTPException(status_code=401, detail="본인확인 코드가 올바르지 않습니다") identity_session_token = payload.identity_session_token.strip() @@ -558,6 +600,7 @@ def verify_password_recovery_identity(payload: PasswordRecoveryVerifyIdentityReq reset_token, reset_expires_at = _issue_recovery_token("reset") session_state["verified"] = True + session_state["verification_attempts"] = 0 session_state["identity_session_token"] = identity_session_token session_state["reset_token"] = reset_token session_state["reset_expires_at"] = reset_expires_at @@ -573,12 +616,17 @@ def reset_password_via_recovery( payload: PasswordRecoveryResetRequest, db: Session = Depends(get_db), ): + _purge_expired_password_recovery_sessions() if len(payload.new_password or "") < 8: raise HTTPException(status_code=400, detail="비밀번호는 8자 이상이어야 합니다") + scope = _normalize_password_recovery_scope(payload.scope) matched_session = None for session_token, session_state in _password_recovery_store.items(): - if session_state.get("reset_token") == payload.reset_token and session_state.get("scope") == payload.scope: + if ( + session_state.get("scope") == scope + and compare_digest(str(session_state.get("reset_token") or ""), payload.reset_token) + ): matched_session = (session_token, session_state) break @@ -586,6 +634,9 @@ def reset_password_via_recovery( raise HTTPException(status_code=404, detail="재설정 토큰을 찾을 수 없습니다") session_token, session_state = matched_session + if not bool(session_state.get("verified")) or not str(session_state.get("identity_session_token") or "").strip(): + raise HTTPException(status_code=403, detail="본인확인 검증이 완료되지 않았습니다") + reset_expires_at = session_state.get("reset_expires_at") if not isinstance(reset_expires_at, datetime) or reset_expires_at <= datetime.utcnow(): _password_recovery_store.pop(session_token, None) diff --git a/tests/test_auth_router_security.py b/tests/test_auth_router_security.py new file mode 100644 index 0000000..5b861c5 --- /dev/null +++ b/tests/test_auth_router_security.py @@ -0,0 +1,170 @@ +import importlib +import sys +import types +from datetime import datetime, timedelta +from types import SimpleNamespace + +import pytest +from fastapi import HTTPException + + +class _FakeQuery: + def __init__(self, user): + self._user = user + + def filter(self, *args, **kwargs): + return self + + def first(self): + return self._user + + +class _FakeDB: + def __init__(self, user): + self._user = user + self.added = [] + self.committed = False + + def query(self, model): + return _FakeQuery(self._user) + + def add(self, value): + self.added.append(value) + + def commit(self): + self.committed = True + + +def _load_auth_router(monkeypatch): + sys.modules.pop("backend.auth_router", None) + + fake_database = types.ModuleType("backend.database") + fake_database.get_db = lambda: None + + fake_models = types.ModuleType("backend.models") + + class _StubUser: + id = "id" + email = "email" + username = "username" + + fake_models.User = _StubUser + + monkeypatch.setitem(sys.modules, "backend.database", fake_database) + monkeypatch.setitem(sys.modules, "backend.models", fake_models) + return importlib.import_module("backend.auth_router") + + +def test_admin_tokens_expire_by_default(monkeypatch): + monkeypatch.delenv("ALLOW_NON_EXPIRING_ADMIN_TOKENS", raising=False) + auth_router = _load_auth_router(monkeypatch) + admin_user = SimpleNamespace(is_admin=True, is_superuser=False) + + assert auth_router._should_issue_non_expiring_admin_token(admin_user) is False + + monkeypatch.setenv("ALLOW_NON_EXPIRING_ADMIN_TOKENS", "true") + auth_router = _load_auth_router(monkeypatch) + assert auth_router._should_issue_non_expiring_admin_token(admin_user) is True + + +def test_start_password_recovery_uses_random_verification_code(monkeypatch): + auth_router = _load_auth_router(monkeypatch) + auth_router._password_recovery_store.clear() + monkeypatch.setattr(auth_router, "randbelow", lambda upper_bound: 123456) + db = _FakeDB(SimpleNamespace(id=7, is_admin=True, is_superuser=False)) + + response = auth_router.start_password_recovery( + auth_router.PasswordRecoveryStartRequest( + scope="admin", + user_hint="admin@example.com", + ), + db, + ) + + session_state = auth_router._password_recovery_store[response["recovery_session_token"]] + assert session_state["verification_code"] == "123456" + assert session_state["verification_code"] != "000000" + + +def test_password_recovery_verify_identity_limits_failed_attempts(monkeypatch): + auth_router = _load_auth_router(monkeypatch) + auth_router._password_recovery_store.clear() + recovery_session_token = "recovery_test" + auth_router._password_recovery_store[recovery_session_token] = { + "user_id": 1, + "scope": "admin", + "verified": False, + "verification_code": "654321", + "verification_attempts": 0, + "expires_at": datetime.utcnow() + timedelta(minutes=5), + } + payload = auth_router.PasswordRecoveryVerifyIdentityRequest( + recovery_session_token=recovery_session_token, + identity_session_token="identity-proof", + verification_code="000000", + ) + + for _ in range(auth_router._PASSWORD_RECOVERY_MAX_VERIFY_ATTEMPTS - 1): + with pytest.raises(HTTPException) as exc_info: + auth_router.verify_password_recovery_identity(payload) + assert exc_info.value.status_code == 401 + + with pytest.raises(HTTPException) as exc_info: + auth_router.verify_password_recovery_identity(payload) + assert exc_info.value.status_code == 429 + assert recovery_session_token not in auth_router._password_recovery_store + + +def test_reset_password_requires_verified_identity(monkeypatch): + auth_router = _load_auth_router(monkeypatch) + auth_router._password_recovery_store.clear() + auth_router._password_recovery_store["recovery_test"] = { + "user_id": 1, + "scope": "admin", + "verified": False, + "reset_token": "reset_token", + "reset_expires_at": datetime.utcnow() + timedelta(minutes=5), + "expires_at": datetime.utcnow() + timedelta(minutes=5), + } + + with pytest.raises(HTTPException) as exc_info: + auth_router.reset_password_via_recovery( + auth_router.PasswordRecoveryResetRequest( + scope="admin", + reset_token="reset_token", + new_password="new-password-123", + ), + _FakeDB(SimpleNamespace(id=1, hashed_password="old")), + ) + + assert exc_info.value.status_code == 403 + + +def test_reset_password_updates_hash_and_clears_session(monkeypatch): + auth_router = _load_auth_router(monkeypatch) + auth_router._password_recovery_store.clear() + user = SimpleNamespace(id=1, hashed_password="old") + db = _FakeDB(user) + auth_router._password_recovery_store["recovery_test"] = { + "user_id": 1, + "scope": "admin", + "verified": True, + "identity_session_token": "identity-proof", + "reset_token": "reset_token", + "reset_expires_at": datetime.utcnow() + timedelta(minutes=5), + "expires_at": datetime.utcnow() + timedelta(minutes=5), + } + + response = auth_router.reset_password_via_recovery( + auth_router.PasswordRecoveryResetRequest( + scope="admin", + reset_token="reset_token", + new_password="new-password-123", + ), + db, + ) + + assert response == {"reset": True, "must_relogin": True} + assert user.hashed_password != "old" + assert db.committed is True + assert "recovery_test" not in auth_router._password_recovery_store From 7852209dee82dc582648335dd11ed7f8e24bcf4e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 18:50:34 +0900 Subject: [PATCH 03/17] Harden Pillow dependency floor to patched range for active image parsing CVEs (#7) * chore: raise Pillow minimum version to 12.2 Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/9ec743ae-a698-4cc0-aa87-8825771cb8d6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: remove accidental pycache artifacts Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/9ec743ae-a698-4cc0-aa87-8825771cb8d6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ceecb9..1bd5e2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "httpx>=0.28,<1.0", "requests>=2.32,<3.0", "redis>=5.2,<6.0", - "Pillow>=10.4,<12.0", + "Pillow>=12.2,<13.0", "torch>=2.6,<3.0", "qdrant-client>=1.12,<2.0", "grpcio>=1.71,<2.0", From b9229e122f5125496c92905c11c4cf1f3a011485 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 19:03:04 +0900 Subject: [PATCH 04/17] Harden orchestrator/auth error surfaces and remove CodeQL-flagged unsafe patterns (#8) * chore: start codeql alert remediation plan Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: remediate CodeQL security and quality findings Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: finalize CodeQL remediation hardening updates Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- backend/auth.py | 33 ++++++++------- backend/llm/orchestrator.py | 41 ++++++++++++++----- backend/main.py | 6 ++- backend/marketplace/router.py | 15 ++++--- .../hooks/use-feature-orchestrator.ts | 4 +- gpu-llm-server/custom-server/server.py | 2 +- 6 files changed, 63 insertions(+), 38 deletions(-) diff --git a/backend/auth.py b/backend/auth.py index 3e4a6bf..511f234 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -10,29 +10,29 @@ from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer +logger = logging.getLogger(__name__) + def _resolve_secret_key() -> tuple[str, bool]: configured = str(os.getenv("SECRET_KEY") or "").strip() if configured: return configured, False - fallback_path = Path( - os.getenv( - "SECRET_KEY_FILE", - str(Path(os.getenv("TEMP") or "/tmp") / "codeai_jwt_secret.key"), - ) + configured_file = str(os.getenv("SECRET_KEY_FILE") or "").strip() + if configured_file: + fallback_path = Path(configured_file).expanduser() + try: + if fallback_path.exists() and fallback_path.is_file(): + cached_secret = fallback_path.read_text(encoding="utf-8").strip() + if cached_secret: + return cached_secret, True + except Exception: + pass + + logger.error( + "SECRET_KEY/SECRET_KEY_FILE is not configured; generating ephemeral runtime secret that invalidates tokens on restart." ) - try: - fallback_path.parent.mkdir(parents=True, exist_ok=True) - if fallback_path.exists(): - cached_secret = fallback_path.read_text(encoding="utf-8").strip() - if cached_secret: - return cached_secret, True - generated_secret = secrets.token_urlsafe(48) - fallback_path.write_text(generated_secret, encoding="utf-8") - return generated_secret, True - except Exception: - return secrets.token_urlsafe(48), True + return secrets.token_urlsafe(48), True SECRET_KEY, SECRET_KEY_IS_RUNTIME_FALLBACK = _resolve_secret_key() @@ -42,7 +42,6 @@ def _resolve_secret_key() -> tuple[str, bool]: ) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") -logger = logging.getLogger(__name__) def get_password_hash(password: str) -> str: diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index 0bf7c48..55d5a0f 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2684,6 +2684,7 @@ class OrchestrationAcceptedResponse(BaseModel): _ORCHESTRATION_PROGRESS_STORE: Dict[str, Dict[str, Any]] = {} +_ORCHESTRATION_PROGRESS_FILE_LOCK = threading.Lock() def _runtime_progress_root() -> Path: @@ -2694,9 +2695,8 @@ def _runtime_progress_root() -> Path: return progress_root -def _orchestration_progress_path(run_id: str) -> Path: - safe_run_id = re.sub(r"[^a-zA-Z0-9_.-]+", "-", str(run_id or "unknown")).strip("-") or "unknown" - return _runtime_progress_root() / f"{safe_run_id}.json" +def _orchestration_progress_store_path() -> Path: + return _runtime_progress_root() / "progress_store.json" def _build_progress_poll_url(run_id: str) -> str: @@ -2712,8 +2712,23 @@ def _save_orchestration_progress(run_id: str, payload: Dict[str, Any]) -> Dict[s normalized["run_id"] = str(run_id or normalized.get("run_id") or "") normalized.setdefault("updated_at", datetime.utcnow().isoformat() + "Z") _ORCHESTRATION_PROGRESS_STORE[normalized["run_id"]] = normalized - progress_path = _orchestration_progress_path(normalized["run_id"]) - progress_path.write_text(json.dumps(normalized, ensure_ascii=False, indent=2), encoding="utf-8") + progress_path = _orchestration_progress_store_path() + with _ORCHESTRATION_PROGRESS_FILE_LOCK: + persisted_payload: Dict[str, Any] = {} + try: + if progress_path.exists() and progress_path.is_file(): + existing_payload = json.loads(progress_path.read_text(encoding="utf-8")) + if isinstance(existing_payload, dict): + persisted_payload = dict(existing_payload) + except Exception: + logger.warning( + "Failed to read orchestration progress store from %s before write", + str(progress_path), + exc_info=True, + ) + persisted_payload = {} + persisted_payload[normalized["run_id"]] = normalized + progress_path.write_text(json.dumps(persisted_payload, ensure_ascii=False, indent=2), encoding="utf-8") return normalized @@ -2721,14 +2736,18 @@ def _load_orchestration_progress(run_id: str) -> Dict[str, Any]: cached = _ORCHESTRATION_PROGRESS_STORE.get(str(run_id or "")) if isinstance(cached, dict) and cached: return dict(cached) - progress_path = _orchestration_progress_path(run_id) + progress_path = _orchestration_progress_store_path() try: - if progress_path.exists() and progress_path.is_file(): - payload = json.loads(progress_path.read_text(encoding="utf-8")) - if isinstance(payload, dict): - _ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(payload) - return dict(payload) + with _ORCHESTRATION_PROGRESS_FILE_LOCK: + if progress_path.exists() and progress_path.is_file(): + payload = json.loads(progress_path.read_text(encoding="utf-8")) + if isinstance(payload, dict): + stored = payload.get(str(run_id or "")) + if isinstance(stored, dict): + _ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(stored) + return dict(stored) except Exception: + logger.error("Failed to load orchestration progress for run_id=%s", str(run_id or ""), exc_info=True) return {} return {} diff --git a/backend/main.py b/backend/main.py index 65c89cc..f520ea4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1031,6 +1031,8 @@ def _runtime_health_payload() -> Dict[str, Any]: from backend.marketplace.router import get_ad_queue_runtime_status queue_runtime = get_ad_queue_runtime_status() except Exception as exc: + logger.exception("Failed to load ad queue runtime status") + safe_queue_error = "queue_runtime_unavailable" queue_runtime = { "redis_queue": { "available": False, @@ -1038,7 +1040,7 @@ def _runtime_health_payload() -> Dict[str, Any]: "note": "Redis queue 진단을 로드하지 못했습니다.", "connection_id": "redis:video_render_queue", "queue_name": "video_render_queue", - "error": str(exc), + "error": safe_queue_error, }, "ad_worker": { "available": False, @@ -1047,7 +1049,7 @@ def _runtime_health_payload() -> Dict[str, Any]: "connection_id": "redis:video_render_queue", "queue_name": "video_render_queue", "worker_id": "ad-render-worker-001", - "error": str(exc), + "error": safe_queue_error, }, } redis_queue = queue_runtime.get("redis_queue", {}) diff --git a/backend/marketplace/router.py b/backend/marketplace/router.py index fc322c7..6d8e5b4 100644 --- a/backend/marketplace/router.py +++ b/backend/marketplace/router.py @@ -595,15 +595,20 @@ def _persist_progress(*, percent: int, step: str, state: str, message: str) -> N ), ) except Exception as exc: + logger.exception( + "Marketplace feature orchestrate stream failed run_id=%s", + request.run_id, + ) + public_error_message = "라이브뷰 실행 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요." local_metadata["popup_state"] = "failed" local_metadata["last_event"] = "failed" - local_metadata["error"] = str(exc) + local_metadata["error"] = public_error_message local_metadata["updated_at"] = _utc_now_iso() - _persist_progress(percent=100, step="failed", state="failed", message=str(exc)) + _persist_progress(percent=100, step="failed", state="failed", message=public_error_message) local_stage_run = _set_feature_metadata(local_stage_run, local_metadata) - local_stage_run = _apply_feature_popup_state(local_stage_run, "failed", str(exc)) + local_stage_run = _apply_feature_popup_state(local_stage_run, "failed", public_error_message) save_stage_run(local_stage_run) - yield _build_feature_sse_event("failed", {"run_id": request.run_id, "state": "failed", "message": str(exc)}) + yield _build_feature_sse_event("failed", {"run_id": request.run_id, "state": "failed", "message": public_error_message}) yield _build_feature_sse_event( "progress", _build_feature_progress_payload( @@ -611,7 +616,7 @@ def _persist_progress(*, percent: int, step: str, state: str, message: str) -> N percent=100, step="failed", state="failed", - message=str(exc), + message=public_error_message, ), ) diff --git a/frontend/frontend/hooks/use-feature-orchestrator.ts b/frontend/frontend/hooks/use-feature-orchestrator.ts index ad39af5..7cb8b72 100644 --- a/frontend/frontend/hooks/use-feature-orchestrator.ts +++ b/frontend/frontend/hooks/use-feature-orchestrator.ts @@ -358,7 +358,7 @@ function buildDefaultCatalogItem(featureId: string): FeatureCatalogItem { const meta = FEATURE_EXPERIENCE_META[featureId] || FEATURE_EXPERIENCE_META['ai-sheet']; return { feature_id: featureId, - title: meta.popupKicker.replace('AI ', 'AI '), + title: meta.popupKicker, summary: meta.launcherSummary, popup_mode: preset.contextTags[1] || meta.outputKind, status: 'enabled', @@ -958,4 +958,4 @@ export function useFeatureOrchestrator() { progressSnapshot, progressHistory, }; -} \ No newline at end of file +} diff --git a/gpu-llm-server/custom-server/server.py b/gpu-llm-server/custom-server/server.py index 606bccf..1cb3cf0 100644 --- a/gpu-llm-server/custom-server/server.py +++ b/gpu-llm-server/custom-server/server.py @@ -176,7 +176,7 @@ def load_model(): logger.error(f"Failed to load model: {e}") model = None tokenizer = None - model_load_error = str(e) + model_load_error = "model_load_failed" logger.warning("Server will stay up without a loaded model.") From f163cd68544f55ede5cc43cc41d9550994d3654d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 21:15:56 +0900 Subject: [PATCH 05/17] Sanitize health diagnostic errors to avoid exception detail exposure (#9) * fix: redact health diagnostic exception details Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * test: make health sanitization checks portable Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: remove compiled test artifacts Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * refactor: normalize diagnostic error codes Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * test: share diagnostic error code fixture Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * refactor: simplify safe diagnostic code map Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- backend/main.py | 67 ++++++++--- tests/test_health_diagnostics_sanitization.py | 105 ++++++++++++++++++ 2 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 tests/test_health_diagnostics_sanitization.py diff --git a/backend/main.py b/backend/main.py index f520ea4..516b298 100644 --- a/backend/main.py +++ b/backend/main.py @@ -723,6 +723,24 @@ def _relative_percent(numerator: float, denominator: float) -> Optional[float]: return round((numerator / denominator) * 100, 1) +_SAFE_DIAGNOSTIC_ERROR_CODES = { + "cpu_load_unavailable", + "gpu_runtime_unavailable", + "memory_snapshot_unavailable", + "queue_runtime_unavailable", +} + + +def _sanitize_diagnostic_error(raw_error: Any, fallback: str) -> Optional[str]: + if raw_error is None: + return None + if isinstance(raw_error, str): + normalized = raw_error.strip().lower() + if normalized in _SAFE_DIAGNOSTIC_ERROR_CODES: + return normalized + return fallback + + def _linux_memory_snapshot() -> Optional[Dict[str, Any]]: meminfo_path = "/proc/meminfo" if not os.path.exists(meminfo_path): @@ -739,7 +757,12 @@ def _linux_memory_snapshot() -> Optional[Dict[str, Any]]: if number.isdigit(): values[key.strip()] = int(number) except Exception as exc: - return {"error": str(exc)} + return { + "error": _sanitize_diagnostic_error( + exc, + "memory_snapshot_unavailable", + ) + } total_kb = values.get("MemTotal", 0) available_kb = values.get("MemAvailable", values.get("MemFree", 0)) @@ -761,7 +784,12 @@ def _windows_memory_snapshot() -> Optional[Dict[str, Any]]: if not kernel32.GlobalMemoryStatusEx(ctypes.byref(status)): return None except Exception as exc: - return {"error": str(exc)} + return { + "error": _sanitize_diagnostic_error( + exc, + "memory_snapshot_unavailable", + ) + } total_bytes = int(status.ullTotalPhys) available_bytes = int(status.ullAvailPhys) @@ -778,12 +806,18 @@ def _memory_snapshot() -> Dict[str, Any]: snapshot = _linux_memory_snapshot() if snapshot is None and os.name == "nt": snapshot = _windows_memory_snapshot() - if not snapshot: - return { + if not snapshot or snapshot.get("error"): + payload: Dict[str, Any] = { "available": False, "state": "warning", "note": "메모리 사용량을 수집하지 못했습니다.", } + if snapshot: + payload["error"] = _sanitize_diagnostic_error( + snapshot.get("error"), + "memory_snapshot_unavailable", + ) + return payload usage_percent = snapshot.get("usage_percent") critical_percent = max( @@ -868,7 +902,7 @@ def _cpu_snapshot() -> Dict[str, Any]: usage_percent: Optional[float] = None note = "CPU 부하가 정상 범위입니다." state = "ok" - error_message = "" + error_code: Optional[str] = None warning_percent = min( SAFE_COMPUTE_USAGE_LIMIT_PERCENT, int(os.getenv("RUNTIME_CPU_WARNING_PERCENT", str(SAFE_MEMORY_OCCUPANCY_LIMIT_PERCENT)) or SAFE_MEMORY_OCCUPANCY_LIMIT_PERCENT), @@ -883,7 +917,7 @@ def _cpu_snapshot() -> Dict[str, Any]: getloadavg = cast(Any, getattr(os, "getloadavg")) load_1m = round(float(getloadavg()[0]), 2) except Exception as exc: - error_message = str(exc) + error_code = _sanitize_diagnostic_error(exc, "cpu_load_unavailable") if load_1m is not None and cpu_count > 0: load_ratio_percent = _relative_percent(load_1m, cpu_count) @@ -911,29 +945,28 @@ def _cpu_snapshot() -> Dict[str, Any]: "load_ratio_percent": load_ratio_percent, "usage_percent": usage_percent, } - if error_message: - payload["error"] = error_message + if error_code: + payload["error"] = error_code return payload def _gpu_snapshot() -> Dict[str, Any]: gpu_runtime = get_gpu_runtime_info() + gpu_runtime_data = gpu_runtime if isinstance(gpu_runtime, dict) else {} + gpu_error = _sanitize_diagnostic_error( + gpu_runtime_data.get("error"), + "gpu_runtime_unavailable", + ) devices = ( - gpu_runtime.get("devices", []) - if isinstance(gpu_runtime, dict) - else [] + gpu_runtime_data.get("devices", []) ) - if not gpu_runtime.get("available"): + if not gpu_runtime_data.get("available"): return { "available": False, "state": "warning", "note": "GPU 런타임이 감지되지 않았습니다. CPU fallback 또는 드라이버 상태를 확인하세요.", "devices": [], - "error": ( - gpu_runtime.get("error") - if isinstance(gpu_runtime, dict) - else None - ), + "error": gpu_error or "gpu_runtime_unavailable", } peak_usage = 0.0 diff --git a/tests/test_health_diagnostics_sanitization.py b/tests/test_health_diagnostics_sanitization.py new file mode 100644 index 0000000..2eaadc0 --- /dev/null +++ b/tests/test_health_diagnostics_sanitization.py @@ -0,0 +1,105 @@ +import ast +import os +from pathlib import Path +from typing import Any, Dict, List, Optional, cast + + +MAIN_PATH = Path(__file__).resolve().parent.parent / "backend" / "main.py" +SAFE_DIAGNOSTIC_ERROR_CODES = { + "cpu_load_unavailable", + "gpu_runtime_unavailable", + "memory_snapshot_unavailable", + "queue_runtime_unavailable", +} + + +def _load_functions(*names: str, extra_globals: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + tree = ast.parse(MAIN_PATH.read_text(encoding="utf-8-sig"), filename=str(MAIN_PATH)) + selected = [ + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name in names + ] + namespace: Dict[str, Any] = { + "Any": Any, + "Dict": Dict, + "List": List, + "Optional": Optional, + "Path": Path, + "cast": cast, + "os": os, + } + if extra_globals: + namespace.update(extra_globals) + exec(compile(ast.Module(body=selected, type_ignores=[]), str(MAIN_PATH), "exec"), namespace) + return namespace + + +def test_sanitize_diagnostic_error_redacts_exception_text(): + namespace = _load_functions( + "_sanitize_diagnostic_error", + extra_globals={ + "_SAFE_DIAGNOSTIC_ERROR_CODES": SAFE_DIAGNOSTIC_ERROR_CODES, + }, + ) + + sanitize = namespace["_sanitize_diagnostic_error"] + + assert sanitize(PermissionError("cannot open /proc/meminfo"), "memory_snapshot_unavailable") == "memory_snapshot_unavailable" + assert sanitize("gpu_runtime_unavailable", "memory_snapshot_unavailable") == "gpu_runtime_unavailable" + assert sanitize(" GPU_Runtime_Unavailable ", "memory_snapshot_unavailable") == "gpu_runtime_unavailable" + assert sanitize(None, "memory_snapshot_unavailable") is None + + +def test_memory_snapshot_error_becomes_warning_payload(): + namespace = _load_functions( + "_sanitize_diagnostic_error", + "_memory_snapshot", + extra_globals={ + "_SAFE_DIAGNOSTIC_ERROR_CODES": SAFE_DIAGNOSTIC_ERROR_CODES, + "_linux_memory_snapshot": lambda: {"error": "permission denied: /proc/meminfo"}, + "_windows_memory_snapshot": lambda: None, + "SAFE_COMPUTE_USAGE_LIMIT_PERCENT": 90, + "SAFE_MEMORY_OCCUPANCY_LIMIT_PERCENT": 75, + }, + ) + + payload = namespace["_memory_snapshot"]() + + assert payload["available"] is False + assert payload["state"] == "warning" + assert payload["error"] == "memory_snapshot_unavailable" + assert "/proc/meminfo" not in payload["error"] + + +def test_cpu_and_gpu_snapshots_expose_only_safe_error_codes(monkeypatch): + namespace = _load_functions( + "_sanitize_diagnostic_error", + "_cpu_snapshot", + "_gpu_snapshot", + extra_globals={ + "_SAFE_DIAGNOSTIC_ERROR_CODES": SAFE_DIAGNOSTIC_ERROR_CODES, + "SAFE_COMPUTE_USAGE_LIMIT_PERCENT": 90, + "SAFE_MEMORY_OCCUPANCY_LIMIT_PERCENT": 75, + "_relative_percent": lambda numerator, denominator: round((numerator / denominator) * 100, 1) if denominator > 0 else None, + "_linux_cpu_usage_percent": lambda: None, + "get_gpu_runtime_info": lambda: { + "available": False, + "error": "driver init failed for /dev/nvidia0", + "devices": [], + }, + }, + ) + + def _raise_loadavg_error(): + raise OSError("cannot read /proc/loadavg") + + monkeypatch.setattr(os, "getloadavg", _raise_loadavg_error) + + cpu_payload = namespace["_cpu_snapshot"]() + gpu_payload = namespace["_gpu_snapshot"]() + + assert cpu_payload["error"] == "cpu_load_unavailable" + assert "/proc/loadavg" not in cpu_payload["error"] + assert gpu_payload["error"] == "gpu_runtime_unavailable" + assert "/dev/nvidia0" not in gpu_payload["error"] From 0940c5d9c0c30e4c5d5ed03e600feb232b142a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Fri, 8 May 2026 21:23:29 +0900 Subject: [PATCH 06/17] Potential fix for code scanning alert no. 4: Information exposure through an exception (#10) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- backend/marketplace/router.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/marketplace/router.py b/backend/marketplace/router.py index 6d8e5b4..55cda54 100644 --- a/backend/marketplace/router.py +++ b/backend/marketplace/router.py @@ -1059,9 +1059,10 @@ def get_ad_queue_runtime_status() -> Dict[str, Dict[str, Any]]: if redis_client is not None: try: queue_depth = int(redis_client.llen(VIDEO_RENDER_QUEUE_NAME)) - except RedisError as exc: + except RedisError: + logger.exception("Failed to read Redis queue depth for health diagnostics") redis_available = False - redis_error = str(exc) + redis_error = "redis_queue_unavailable" with _ad_worker_lock: started_at = _ad_worker_runtime.get("started_at") From 75ed443b7dcb10967dc3f03a3f766f74fbcc1317 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 21:42:59 +0900 Subject: [PATCH 07/17] fix(ci): set explicit python-version in codeql workflow (#11) Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/4ea2a28e-7f09-4b9d-a3df-785939fa43ac Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 239520b..4357ddc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,6 +32,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Setup Python + if: matrix.language == 'python' + uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: From b0d3b717b5e6d7b3bdfa68aaa3dd577152fb577d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 21:57:37 +0900 Subject: [PATCH 08/17] fix: webauthn stub in tests, timezone-aware datetimes, Pydantic v2 ConfigDict, add .gitignore (#12) * fix(tests): stub webauthn in auth_router test fixture to fix import failures Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/63299979-62f4-489f-a1d2-307336759de9 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: stub webauthn in tests, replace datetime.utcnow, fix Pydantic Config, add .gitignore Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/63299979-62f4-489f-a1d2-307336759de9 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- .gitignore | 58 ++++++++++++++++++++++++++++++ backend/auth.py | 4 +-- backend/auth_router.py | 25 +++++++------ tests/test_auth_router_security.py | 45 +++++++++++++++++++---- 4 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88555e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg +*.egg-info/ +dist/ +build/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.whl +.installed.cfg + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Environment variables +.env +.env.* +!.env.example + +# Node +node_modules/ +npm-debug.log* + +# Distribution +*.tar.gz +*.zip diff --git a/backend/auth.py b/backend/auth.py index 511f234..deabddf 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -2,7 +2,7 @@ import os import bcrypt import secrets -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Optional @@ -67,7 +67,7 @@ def create_access_token( ) -> str: to_encode = data.copy() if not no_expiry: - expire = datetime.utcnow() + ( + expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) to_encode.update({"exp": expire}) diff --git a/backend/auth_router.py b/backend/auth_router.py index 0fb4767..1ae751d 100644 --- a/backend/auth_router.py +++ b/backend/auth_router.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from secrets import compare_digest, randbelow, token_urlsafe import base64 import os @@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm from typing import Optional, cast -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, ConfigDict, EmailStr from sqlalchemy.orm import Session from webauthn import ( generate_authentication_options, @@ -82,8 +82,7 @@ class UserResponse(BaseModel): business_registration_number: Optional[str] = None representative_name: Optional[str] = None - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class Token(BaseModel): @@ -254,7 +253,7 @@ class PasswordRecoveryResetResponse(BaseModel): def _issue_recovery_token(prefix: str) -> tuple[str, datetime]: - expires_at = datetime.utcnow() + timedelta(minutes=10) + expires_at = datetime.now(timezone.utc) + timedelta(minutes=10) return f"{prefix}_{token_urlsafe(24)}", expires_at @@ -273,7 +272,7 @@ def _normalize_password_recovery_scope(scope: str) -> str: def _purge_expired_password_recovery_sessions() -> None: - now = datetime.utcnow() + now = datetime.now(timezone.utc) expired_tokens = [ session_token for session_token, session_state in _password_recovery_store.items() @@ -380,7 +379,7 @@ def start_passkey_registration( "user_id": int(user.id), "challenge": _to_base64url(options.challenge), "device_label": str(payload.device_label or "이 기기 패스키").strip() or "이 기기 패스키", - "expires_at": datetime.utcnow() + timedelta(minutes=5), + "expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), "user_handle": user_handle, "rp_id": rp_id, "expected_origin": expected_origin, @@ -403,7 +402,7 @@ def finish_passkey_registration( raise HTTPException(status_code=404, detail="패스키 등록 세션을 찾을 수 없습니다") expires_at = state.get("expires_at") - if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow(): + if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc): _passkey_registration_store.pop(payload.registration_token, None) raise HTTPException(status_code=410, detail="패스키 등록 세션이 만료되었습니다") @@ -430,7 +429,7 @@ def finish_passkey_registration( user.passkey_public_key = _to_base64url(verification.credential_public_key) user.passkey_device_label = str(state.get("device_label") or "이 기기 패스키") user.passkey_sign_count = int(verification.sign_count) - user.passkey_registered_at = datetime.utcnow() + user.passkey_registered_at = datetime.now(timezone.utc) db.add(user) db.commit() _passkey_registration_store.pop(payload.registration_token, None) @@ -465,7 +464,7 @@ def start_passkey_login( ) _passkey_login_store[str(user.email)] = { "challenge": _to_base64url(options.challenge), - "expires_at": datetime.utcnow() + timedelta(minutes=5), + "expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), "credential_id": str(user.passkey_credential_id), "rp_id": rp_id, "expected_origin": expected_origin, @@ -492,7 +491,7 @@ def finish_passkey_login( raise HTTPException(status_code=404, detail="패스키 로그인 세션을 찾을 수 없습니다") expires_at = state.get("expires_at") - if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow(): + if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc): _passkey_login_store.pop(str(user.email), None) raise HTTPException(status_code=410, detail="패스키 로그인 세션이 만료되었습니다") @@ -581,7 +580,7 @@ def verify_password_recovery_identity(payload: PasswordRecoveryVerifyIdentityReq raise HTTPException(status_code=404, detail="복구 세션을 찾을 수 없습니다") expires_at = session_state.get("expires_at") - if not isinstance(expires_at, datetime) or expires_at <= datetime.utcnow(): + if not isinstance(expires_at, datetime) or expires_at <= datetime.now(timezone.utc): _password_recovery_store.pop(payload.recovery_session_token, None) raise HTTPException(status_code=410, detail="복구 세션이 만료되었습니다") @@ -638,7 +637,7 @@ def reset_password_via_recovery( raise HTTPException(status_code=403, detail="본인확인 검증이 완료되지 않았습니다") reset_expires_at = session_state.get("reset_expires_at") - if not isinstance(reset_expires_at, datetime) or reset_expires_at <= datetime.utcnow(): + if not isinstance(reset_expires_at, datetime) or reset_expires_at <= datetime.now(timezone.utc): _password_recovery_store.pop(session_token, None) raise HTTPException(status_code=410, detail="재설정 토큰이 만료되었습니다") diff --git a/tests/test_auth_router_security.py b/tests/test_auth_router_security.py index 5b861c5..245042d 100644 --- a/tests/test_auth_router_security.py +++ b/tests/test_auth_router_security.py @@ -1,7 +1,7 @@ import importlib import sys import types -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from types import SimpleNamespace import pytest @@ -50,6 +50,39 @@ class _StubUser: fake_models.User = _StubUser + # Stub webauthn if not installed so auth_router can be imported without it + if "webauthn" not in sys.modules: + _stub_webauthn = types.ModuleType("webauthn") + for _name in ( + "generate_authentication_options", + "generate_registration_options", + "options_to_json", + "verify_authentication_response", + "verify_registration_response", + ): + setattr(_stub_webauthn, _name, None) + + _stub_structs = types.ModuleType("webauthn.helpers.structs") + for _name in ( + "PublicKeyCredentialType", + "AuthenticatorAssertionResponse", + "AuthenticatorAttestationResponse", + "AuthenticatorSelectionCriteria", + "PublicKeyCredentialDescriptor", + "RegistrationCredential", + "AuthenticationCredential", + "UserVerificationRequirement", + "ResidentKeyRequirement", + ): + setattr(_stub_structs, _name, None) + + _stub_helpers = types.ModuleType("webauthn.helpers") + _stub_helpers.structs = _stub_structs + + monkeypatch.setitem(sys.modules, "webauthn", _stub_webauthn) + monkeypatch.setitem(sys.modules, "webauthn.helpers", _stub_helpers) + monkeypatch.setitem(sys.modules, "webauthn.helpers.structs", _stub_structs) + monkeypatch.setitem(sys.modules, "backend.database", fake_database) monkeypatch.setitem(sys.modules, "backend.models", fake_models) return importlib.import_module("backend.auth_router") @@ -96,7 +129,7 @@ def test_password_recovery_verify_identity_limits_failed_attempts(monkeypatch): "verified": False, "verification_code": "654321", "verification_attempts": 0, - "expires_at": datetime.utcnow() + timedelta(minutes=5), + "expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), } payload = auth_router.PasswordRecoveryVerifyIdentityRequest( recovery_session_token=recovery_session_token, @@ -123,8 +156,8 @@ def test_reset_password_requires_verified_identity(monkeypatch): "scope": "admin", "verified": False, "reset_token": "reset_token", - "reset_expires_at": datetime.utcnow() + timedelta(minutes=5), - "expires_at": datetime.utcnow() + timedelta(minutes=5), + "reset_expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), + "expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), } with pytest.raises(HTTPException) as exc_info: @@ -151,8 +184,8 @@ def test_reset_password_updates_hash_and_clears_session(monkeypatch): "verified": True, "identity_session_token": "identity-proof", "reset_token": "reset_token", - "reset_expires_at": datetime.utcnow() + timedelta(minutes=5), - "expires_at": datetime.utcnow() + timedelta(minutes=5), + "reset_expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), + "expires_at": datetime.now(timezone.utc) + timedelta(minutes=5), } response = auth_router.reset_password_via_recovery( From 5af66d726e7982258e9593c769cd1f42caa395c0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 22:39:05 +0900 Subject: [PATCH 09/17] Add consolidated design-change report and expanded PR body draft (#13) * docs: add overall design change and PR report Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * docs: make PR report paths portable Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * docs: clarify bilingual PR report structure Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- ...rall-design-change-pr-report-2026-05-08.md | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/overall-design-change-pr-report-2026-05-08.md diff --git a/docs/overall-design-change-pr-report-2026-05-08.md b/docs/overall-design-change-pr-report-2026-05-08.md new file mode 100644 index 0000000..232e4c3 --- /dev/null +++ b/docs/overall-design-change-pr-report-2026-05-08.md @@ -0,0 +1,182 @@ +# 전체 설계변경 요약 및 PR 본문 초안 + +> Note: This document is bilingual by design. The repository-wide design summary is written in Korean for the primary working context, and the PR body draft is written in English so it can be pasted directly into GitHub pull request fields. + +## 문서 목적 +- 현재 저장소 문서 기준으로 전체 설계변경 내용을 한 번에 검토할 수 있도록 정리한다. +- 바로 복사해 사용할 수 있는 실제 PR 본문 초안을 남긴다. +- 설계변경 요약과 PR 설명의 근거 문서를 함께 묶어 추적 가능하게 유지한다. + +## 근거 문서 +- `README.md` +- `docs/orchestrator-multigenerator-upgrade-status.md` +- `docs/final_readiness_checklist.md` +- `docs/admin-dashboard-ui-ux-browser-blueprint.md` +- `docs/admin-dashboard-section-linkage-checklist.md` +- `gpu-llm-server/reports/pr-body-2026-04-27.md` + +--- + +## 1. 전체 설계변경 요약 + +### 1-1. 플랫폼 운영 구조 재정렬 +- 공개 메인 앱과 관리자 앱의 운영 기준을 분리했다. +- 공개 메인 앱은 `frontend`, 관리자 앱은 `frontend/frontend` 기준으로 정렬했다. +- 운영 진입 경로를 `marketplace`, `admin`, `/api/llm/ws` 중심으로 고정했다. +- 운영 판정은 단순 구현 여부가 아니라 운영 경로 실검증 기준으로 묶었다. + +### 1-2. 오케스트레이터 및 멀티 생성기 계약 단일화 +- Python 산출물 서비스 구조를 `app/services/__init__.py`, `app/services/runtime_service.py` 패키지 기준으로 통일했다. +- 레거시 `app/services.py` 단일 파일 기준과 신규 패키지 기준이 동시에 유지되지 않도록 계약을 정렬했다. +- 템플릿, 검증기, 체크리스트, capability 진단 규칙이 같은 서비스 패키지 기준을 보도록 재정리했다. + +### 1-3. 생성 직후 hard gate 검증 체계 강화 +- 생성 직후 결과물 폴더에서 의존성 설치, 단독 기동, 핵심 API 호출, 테스트, ZIP 재현 검증까지 한 흐름으로 묶었다. +- readiness checklist, semantic gate, completion gate, packaging audit, output audit의 연결 구조를 강화했다. +- 산출물 문서와 운영 증거가 분리되지 않도록 `final_readiness_checklist.md` 중심의 판정 체계를 유지했다. + +### 1-4. capability evidence 및 self-run 추적 강화 +- 관리자 capability 진단에 summary/detail 분리와 evidence bundle 해석을 반영했다. +- `completion_gate_ok`, `self_run_status`, `failure_tags`, `target_file_ids`, `evidence_digest` 등 추적 요약을 노출하도록 정리했다. +- self-run terminal state, `applied_to_source`, runtime artifact, operational evidence를 같은 흐름에서 확인할 수 있게 했다. + +### 1-5. 운영 경로 실검증 기준 고정 +- 운영 경로 검증 대상을 `admin`, `marketplace`, websocket, system settings, workspace self-run record까지 포함해 정리했다. +- 로컬 성능 및 검증 기준은 `localhost` 대신 `127.0.0.1:8000` 또는 운영 도메인 기준으로 고정했다. +- 완료 판정은 운영 경로 실검증과 readiness evidence가 함께 닫혀야만 가능하도록 유지했다. + +### 1-6. 관리자 대시보드 UI/UX 구조 재설계 +- 관리자 대시보드를 중앙 오케스트레이터 허브 중심 구조로 재배치했다. +- 상단 바, 히어로 액션, 중앙 런처 허브, 양측 레일, 오버레이 창형 섹션 구조를 연결했다. +- 인라인 접기 카드 위주 화면에서 운영자가 실제 제어에 집중할 수 있는 실행형 패널 구조로 전환했다. + +### 1-7. 신규 생성 프로그램의 운영형 기본 규칙 확대 +- 새로 생성되는 프로그램에도 운영형 설정, 보안 파일, 상태 클라이언트, 최소 코드량, self-configurable 검증 규칙을 공통 적용하는 방향으로 확장했다. +- 단순 스캐폴드가 아니라 운영 준비도와 검증 문서까지 포함하는 생성기 구조를 목표 상태로 정리했다. + +--- + +## 2. 실제 PR 제목 제안 + +### 추천 제목 +`오케스트레이터·멀티 생성기·운영 검증 체계 전면 정렬 및 관리자 UI 구조 재설계` + +### 대안 제목 +- `생성기 계약 단일화와 hard gate 검증 체계 정렬, 관리자 허브 UI 재설계` +- `운영형 오케스트레이터 증거 체계 정렬 및 admin/marketplace 검증 구조 고도화` + +--- + +## 3. 실제 PR 본문 초안 + +## Summary + +This PR consolidates the repository-wide design changes into a single operational baseline. It aligns the public/admin runtime structure, unifies the generator contract around the `app/services/` package layout, strengthens post-generation hard-gate validation, and reorganizes the admin dashboard into an operator-centric orchestration hub. It also ties readiness evidence, self-run traces, and operational verification into a single reviewable flow. + +## Why + +- The generator contract, validation rules, and documentation needed to follow the same service-package standard. +- Completion status needed to be grounded in real operational evidence instead of partial implementation signals. +- Post-generation validation needed to verify dependency install, standalone boot, core API health, tests, and ZIP reproduction as one closed gate. +- The admin dashboard needed to shift from scattered inline sections to a workflow-centered control hub that exposes actionable evidence. + +## Scope Of Changes + +### 1. Runtime / Platform Structure +- Reaffirmed split operation between the public main app and the admin app. +- Kept operational routing focused on `marketplace`, `admin`, and `/api/llm/ws`. +- Synchronized runtime interpretation with the documented production entry points. + +### 2. Generator Contract Unification +- Standardized Python service outputs on: + - `app/services/__init__.py` + - `app/services/runtime_service.py` +- Removed contract ambiguity between legacy single-file service references and package-based service structure. +- Kept templates, validators, checklists, and capability diagnostics aligned to the same package contract. + +### 3. Hard-Gate Validation Baseline +- Strengthened the closed validation path executed immediately after generation: + - dependency installation + - standalone boot + - core API smoke + - test execution + - ZIP reproduction verification +- Preserved semantic gate, completion gate, packaging audit, and readiness checklist linkage. +- Kept `final_readiness_checklist.md` as the central review artifact for closure. + +### 4. Capability Evidence / Self-Run Traceability +- Expanded capability summary/detail separation and evidence bundle interpretation. +- Surfaced evidence-oriented fields such as: + - `completion_gate_ok` + - `self_run_status` + - `failure_tags` + - `target_file_ids` + - `evidence_digest` +- Connected self-run terminal status and `applied_to_source` evidence to the admin-facing review flow. + +### 5. Operational Verification Standards +- Kept operational verification centered on real production paths, including admin, marketplace, websocket, system-settings, and workspace self-run record flows. +- Kept local verification baselines on `127.0.0.1:8000` or the production domain rather than `localhost`. +- Maintained the rule that completion status requires operational evidence, not just code presence. + +### 6. Admin Dashboard UX Redesign +- Reorganized the admin screen around a central orchestration hub. +- Connected top actions, hero actions, launcher tiles, inline surfaces, and modal/overlay sections into a more operator-focused UI. +- Shifted away from dense inline foldable cards toward a clearer action-and-control workflow. + +### 7. Operational-Grade Output Defaults +- Extended generator expectations so newly produced applications follow operational-grade defaults rather than bare scaffolds. +- Preserved expectations for security/runtime/status components and stronger output quality gates. + +## Validation Basis + +- Operational readiness and completion status are documented in `docs/final_readiness_checklist.md`. +- Orchestrator and multi-generator alignment details are documented in `docs/orchestrator-multigenerator-upgrade-status.md`. +- Admin dashboard UX restructuring basis is documented in: + - `docs/admin-dashboard-ui-ux-browser-blueprint.md` + - `docs/admin-dashboard-section-linkage-checklist.md` +- Documentation-only baseline validation for this update: + - `npm --prefix frontend/frontend run test` + +## Risks + +- Tightening contract alignment can expose stale references in secondary documents or auxiliary diagnostic paths. +- Evidence-first completion criteria can downgrade previously tolerated partial states into explicit blockers. +- Admin dashboard workflow changes may alter operator navigation expectations until the new hub pattern is fully internalized. + +## Rollback Strategy + +- Roll back documentation and PR narrative independently if wording or scope grouping needs refinement. +- If a runtime/design interpretation needs to be reverted, restore the corresponding baseline in the status/readiness documents first so the repository does not present mismatched closure criteria. +- Preserve the service-package contract and evidence-based completion rule unless a repository-wide alternative standard is intentionally adopted. + +## Reviewer Focus + +- Verify that generator, validator, checklist, and capability documentation all describe the same `app/services/` package contract. +- Check that hard-gate validation is represented as a closed operational path rather than a partial quality signal. +- Review whether the admin dashboard redesign description accurately matches the current launcher-hub and overlay-window structure. +- Confirm that completion claims stay anchored to documented operational evidence. + +## Notes For Release / Reporting + +- This PR body is intended as a consolidated reporting layer for the current repository baseline. +- It is suitable for follow-up release notes, readiness reviews, or status reports that need one narrative covering generator structure, operational verification, and admin UI direction. + +--- + +## 4. 짧은 PR 본문 버전 + +## Summary +- 공개/관리자 운영 구조를 재정렬하고 생성기 계약을 `app/services/` 패키지 기준으로 단일화했다. +- 생성 직후 hard gate 검증, readiness checklist, self-run evidence, operational verification 흐름을 같은 판정 체계로 묶었다. +- 관리자 화면을 중앙 오케스트레이터 허브 기반 구조로 정리해 운영 제어와 증거 확인 흐름을 강화했다. + +## Validation +- `docs/final_readiness_checklist.md` +- `docs/orchestrator-multigenerator-upgrade-status.md` +- `npm --prefix frontend/frontend run test` + +## Reviewer Focus +- 서비스 패키지 계약 정합성 +- hard gate 및 readiness evidence 표현 정확성 +- 관리자 허브 UI 설명과 실제 구조의 일치 여부 From d9a43638ccd09c49f86387da43dc9b7631373895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:04:23 +0900 Subject: [PATCH 10/17] Copilot/fix unauthorized data in path expression (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: start codeql alert remediation plan Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: remediate CodeQL security and quality findings Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: finalize CodeQL remediation hardening updates Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * 검증확인했습니다, 병합해주세요 (#14) * Harden Pillow dependency floor to patched range for active image parsing CVEs (#7) * chore: raise Pillow minimum version to 12.2 Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/9ec743ae-a698-4cc0-aa87-8825771cb8d6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: remove accidental pycache artifacts Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/9ec743ae-a698-4cc0-aa87-8825771cb8d6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * Harden orchestrator/auth error surfaces and remove CodeQL-flagged unsafe patterns (#8) * chore: start codeql alert remediation plan Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: remediate CodeQL security and quality findings Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: finalize CodeQL remediation hardening updates Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/e096e163-c0eb-430e-95b8-006690b13d72 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * Sanitize health diagnostic errors to avoid exception detail exposure (#9) * fix: redact health diagnostic exception details Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * test: make health sanitization checks portable Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: remove compiled test artifacts Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * refactor: normalize diagnostic error codes Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * test: share diagnostic error code fixture Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * refactor: simplify safe diagnostic code map Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/5d18c2d0-8dda-4817-837b-37752598afa6 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * Potential fix for code scanning alert no. 4: Information exposure through an exception (#10) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(ci): set explicit python-version in codeql workflow (#11) Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/4ea2a28e-7f09-4b9d-a3df-785939fa43ac Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: webauthn stub in tests, timezone-aware datetimes, Pydantic v2 ConfigDict, add .gitignore (#12) * fix(tests): stub webauthn in auth_router test fixture to fix import failures Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/63299979-62f4-489f-a1d2-307336759de9 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: stub webauthn in tests, replace datetime.utcnow, fix Pydantic Config, add .gitignore Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/63299979-62f4-489f-a1d2-307336759de9 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * Add consolidated design-change report and expanded PR body draft (#13) * docs: add overall design change and PR report Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * docs: make PR report paths portable Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * docs: clarify bilingual PR report structure Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/82b0addf-4e64-42b9-ac75-63d99a14f84d Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update .github/workflows/codeql.yml 변경 감사합니다 Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Potential fix for pull request finding 커밋합니다. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: address PR review feedback for auth and progress persistence Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/54ddac5c-9ffc-4dd9-8328-a90404f7b582 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backend/auth.py | 29 ++- backend/llm/orchestrator.py | 50 +++-- ...rall-design-change-pr-report-2026-05-08.md | 182 ------------------ 3 files changed, 56 insertions(+), 205 deletions(-) delete mode 100644 docs/overall-design-change-pr-report-2026-05-08.md diff --git a/backend/auth.py b/backend/auth.py index deabddf..38d3535 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -22,16 +22,33 @@ def _resolve_secret_key() -> tuple[str, bool]: if configured_file: fallback_path = Path(configured_file).expanduser() try: - if fallback_path.exists() and fallback_path.is_file(): + if not fallback_path.exists(): + logger.error( + "SECRET_KEY_FILE is configured but does not exist: %s. Falling back to ephemeral runtime secret.", + str(fallback_path), + ) + elif not fallback_path.is_file(): + logger.error( + "SECRET_KEY_FILE is configured but is not a file: %s. Falling back to ephemeral runtime secret.", + str(fallback_path), + ) + else: cached_secret = fallback_path.read_text(encoding="utf-8").strip() if cached_secret: return cached_secret, True + logger.error( + "SECRET_KEY_FILE is configured but empty: %s. Falling back to ephemeral runtime secret.", + str(fallback_path), + ) except Exception: - pass - - logger.error( - "SECRET_KEY/SECRET_KEY_FILE is not configured; generating ephemeral runtime secret that invalidates tokens on restart." - ) + logger.exception( + "Failed to read SECRET_KEY_FILE at %s. Falling back to ephemeral runtime secret.", + str(fallback_path), + ) + else: + logger.error( + "SECRET_KEY/SECRET_KEY_FILE is not configured; generating ephemeral runtime secret that invalidates tokens on restart." + ) return secrets.token_urlsafe(48), True diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index 55d5a0f..d49739b 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2695,8 +2695,10 @@ def _runtime_progress_root() -> Path: return progress_root -def _orchestration_progress_store_path() -> Path: - return _runtime_progress_root() / "progress_store.json" +def _orchestration_progress_file_path(run_id: str) -> Path: + safe_run_id = str(run_id or "unknown") + file_name = f"{hashlib.sha256(safe_run_id.encode('utf-8')).hexdigest()}.json" + return _runtime_progress_root() / file_name def _build_progress_poll_url(run_id: str) -> str: @@ -2712,23 +2714,39 @@ def _save_orchestration_progress(run_id: str, payload: Dict[str, Any]) -> Dict[s normalized["run_id"] = str(run_id or normalized.get("run_id") or "") normalized.setdefault("updated_at", datetime.utcnow().isoformat() + "Z") _ORCHESTRATION_PROGRESS_STORE[normalized["run_id"]] = normalized - progress_path = _orchestration_progress_store_path() + progress_path = _orchestration_progress_file_path(normalized["run_id"]) with _ORCHESTRATION_PROGRESS_FILE_LOCK: - persisted_payload: Dict[str, Any] = {} + temp_path: Optional[Path] = None try: - if progress_path.exists() and progress_path.is_file(): - existing_payload = json.loads(progress_path.read_text(encoding="utf-8")) - if isinstance(existing_payload, dict): - persisted_payload = dict(existing_payload) + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=str(progress_path.parent), + prefix=f"{progress_path.name}.", + suffix=".tmp", + delete=False, + ) as temp_file: + temp_file.write(json.dumps(normalized, ensure_ascii=False, indent=2)) + temp_file.flush() + os.fsync(temp_file.fileno()) + temp_path = Path(temp_file.name) + os.replace(temp_path, progress_path) except Exception: + if temp_path is not None: + try: + temp_path.unlink(missing_ok=True) + except Exception: + logger.warning( + "Failed to remove temporary orchestration progress file %s", + str(temp_path), + exc_info=True, + ) logger.warning( - "Failed to read orchestration progress store from %s before write", + "Failed to write orchestration progress file at %s", str(progress_path), exc_info=True, ) - persisted_payload = {} - persisted_payload[normalized["run_id"]] = normalized - progress_path.write_text(json.dumps(persisted_payload, ensure_ascii=False, indent=2), encoding="utf-8") + return dict(_ORCHESTRATION_PROGRESS_STORE.get(normalized["run_id"], {})) return normalized @@ -2736,16 +2754,14 @@ def _load_orchestration_progress(run_id: str) -> Dict[str, Any]: cached = _ORCHESTRATION_PROGRESS_STORE.get(str(run_id or "")) if isinstance(cached, dict) and cached: return dict(cached) - progress_path = _orchestration_progress_store_path() + progress_path = _orchestration_progress_file_path(run_id) try: with _ORCHESTRATION_PROGRESS_FILE_LOCK: if progress_path.exists() and progress_path.is_file(): payload = json.loads(progress_path.read_text(encoding="utf-8")) if isinstance(payload, dict): - stored = payload.get(str(run_id or "")) - if isinstance(stored, dict): - _ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(stored) - return dict(stored) + _ORCHESTRATION_PROGRESS_STORE[str(run_id or "")] = dict(payload) + return dict(payload) except Exception: logger.error("Failed to load orchestration progress for run_id=%s", str(run_id or ""), exc_info=True) return {} diff --git a/docs/overall-design-change-pr-report-2026-05-08.md b/docs/overall-design-change-pr-report-2026-05-08.md deleted file mode 100644 index 232e4c3..0000000 --- a/docs/overall-design-change-pr-report-2026-05-08.md +++ /dev/null @@ -1,182 +0,0 @@ -# 전체 설계변경 요약 및 PR 본문 초안 - -> Note: This document is bilingual by design. The repository-wide design summary is written in Korean for the primary working context, and the PR body draft is written in English so it can be pasted directly into GitHub pull request fields. - -## 문서 목적 -- 현재 저장소 문서 기준으로 전체 설계변경 내용을 한 번에 검토할 수 있도록 정리한다. -- 바로 복사해 사용할 수 있는 실제 PR 본문 초안을 남긴다. -- 설계변경 요약과 PR 설명의 근거 문서를 함께 묶어 추적 가능하게 유지한다. - -## 근거 문서 -- `README.md` -- `docs/orchestrator-multigenerator-upgrade-status.md` -- `docs/final_readiness_checklist.md` -- `docs/admin-dashboard-ui-ux-browser-blueprint.md` -- `docs/admin-dashboard-section-linkage-checklist.md` -- `gpu-llm-server/reports/pr-body-2026-04-27.md` - ---- - -## 1. 전체 설계변경 요약 - -### 1-1. 플랫폼 운영 구조 재정렬 -- 공개 메인 앱과 관리자 앱의 운영 기준을 분리했다. -- 공개 메인 앱은 `frontend`, 관리자 앱은 `frontend/frontend` 기준으로 정렬했다. -- 운영 진입 경로를 `marketplace`, `admin`, `/api/llm/ws` 중심으로 고정했다. -- 운영 판정은 단순 구현 여부가 아니라 운영 경로 실검증 기준으로 묶었다. - -### 1-2. 오케스트레이터 및 멀티 생성기 계약 단일화 -- Python 산출물 서비스 구조를 `app/services/__init__.py`, `app/services/runtime_service.py` 패키지 기준으로 통일했다. -- 레거시 `app/services.py` 단일 파일 기준과 신규 패키지 기준이 동시에 유지되지 않도록 계약을 정렬했다. -- 템플릿, 검증기, 체크리스트, capability 진단 규칙이 같은 서비스 패키지 기준을 보도록 재정리했다. - -### 1-3. 생성 직후 hard gate 검증 체계 강화 -- 생성 직후 결과물 폴더에서 의존성 설치, 단독 기동, 핵심 API 호출, 테스트, ZIP 재현 검증까지 한 흐름으로 묶었다. -- readiness checklist, semantic gate, completion gate, packaging audit, output audit의 연결 구조를 강화했다. -- 산출물 문서와 운영 증거가 분리되지 않도록 `final_readiness_checklist.md` 중심의 판정 체계를 유지했다. - -### 1-4. capability evidence 및 self-run 추적 강화 -- 관리자 capability 진단에 summary/detail 분리와 evidence bundle 해석을 반영했다. -- `completion_gate_ok`, `self_run_status`, `failure_tags`, `target_file_ids`, `evidence_digest` 등 추적 요약을 노출하도록 정리했다. -- self-run terminal state, `applied_to_source`, runtime artifact, operational evidence를 같은 흐름에서 확인할 수 있게 했다. - -### 1-5. 운영 경로 실검증 기준 고정 -- 운영 경로 검증 대상을 `admin`, `marketplace`, websocket, system settings, workspace self-run record까지 포함해 정리했다. -- 로컬 성능 및 검증 기준은 `localhost` 대신 `127.0.0.1:8000` 또는 운영 도메인 기준으로 고정했다. -- 완료 판정은 운영 경로 실검증과 readiness evidence가 함께 닫혀야만 가능하도록 유지했다. - -### 1-6. 관리자 대시보드 UI/UX 구조 재설계 -- 관리자 대시보드를 중앙 오케스트레이터 허브 중심 구조로 재배치했다. -- 상단 바, 히어로 액션, 중앙 런처 허브, 양측 레일, 오버레이 창형 섹션 구조를 연결했다. -- 인라인 접기 카드 위주 화면에서 운영자가 실제 제어에 집중할 수 있는 실행형 패널 구조로 전환했다. - -### 1-7. 신규 생성 프로그램의 운영형 기본 규칙 확대 -- 새로 생성되는 프로그램에도 운영형 설정, 보안 파일, 상태 클라이언트, 최소 코드량, self-configurable 검증 규칙을 공통 적용하는 방향으로 확장했다. -- 단순 스캐폴드가 아니라 운영 준비도와 검증 문서까지 포함하는 생성기 구조를 목표 상태로 정리했다. - ---- - -## 2. 실제 PR 제목 제안 - -### 추천 제목 -`오케스트레이터·멀티 생성기·운영 검증 체계 전면 정렬 및 관리자 UI 구조 재설계` - -### 대안 제목 -- `생성기 계약 단일화와 hard gate 검증 체계 정렬, 관리자 허브 UI 재설계` -- `운영형 오케스트레이터 증거 체계 정렬 및 admin/marketplace 검증 구조 고도화` - ---- - -## 3. 실제 PR 본문 초안 - -## Summary - -This PR consolidates the repository-wide design changes into a single operational baseline. It aligns the public/admin runtime structure, unifies the generator contract around the `app/services/` package layout, strengthens post-generation hard-gate validation, and reorganizes the admin dashboard into an operator-centric orchestration hub. It also ties readiness evidence, self-run traces, and operational verification into a single reviewable flow. - -## Why - -- The generator contract, validation rules, and documentation needed to follow the same service-package standard. -- Completion status needed to be grounded in real operational evidence instead of partial implementation signals. -- Post-generation validation needed to verify dependency install, standalone boot, core API health, tests, and ZIP reproduction as one closed gate. -- The admin dashboard needed to shift from scattered inline sections to a workflow-centered control hub that exposes actionable evidence. - -## Scope Of Changes - -### 1. Runtime / Platform Structure -- Reaffirmed split operation between the public main app and the admin app. -- Kept operational routing focused on `marketplace`, `admin`, and `/api/llm/ws`. -- Synchronized runtime interpretation with the documented production entry points. - -### 2. Generator Contract Unification -- Standardized Python service outputs on: - - `app/services/__init__.py` - - `app/services/runtime_service.py` -- Removed contract ambiguity between legacy single-file service references and package-based service structure. -- Kept templates, validators, checklists, and capability diagnostics aligned to the same package contract. - -### 3. Hard-Gate Validation Baseline -- Strengthened the closed validation path executed immediately after generation: - - dependency installation - - standalone boot - - core API smoke - - test execution - - ZIP reproduction verification -- Preserved semantic gate, completion gate, packaging audit, and readiness checklist linkage. -- Kept `final_readiness_checklist.md` as the central review artifact for closure. - -### 4. Capability Evidence / Self-Run Traceability -- Expanded capability summary/detail separation and evidence bundle interpretation. -- Surfaced evidence-oriented fields such as: - - `completion_gate_ok` - - `self_run_status` - - `failure_tags` - - `target_file_ids` - - `evidence_digest` -- Connected self-run terminal status and `applied_to_source` evidence to the admin-facing review flow. - -### 5. Operational Verification Standards -- Kept operational verification centered on real production paths, including admin, marketplace, websocket, system-settings, and workspace self-run record flows. -- Kept local verification baselines on `127.0.0.1:8000` or the production domain rather than `localhost`. -- Maintained the rule that completion status requires operational evidence, not just code presence. - -### 6. Admin Dashboard UX Redesign -- Reorganized the admin screen around a central orchestration hub. -- Connected top actions, hero actions, launcher tiles, inline surfaces, and modal/overlay sections into a more operator-focused UI. -- Shifted away from dense inline foldable cards toward a clearer action-and-control workflow. - -### 7. Operational-Grade Output Defaults -- Extended generator expectations so newly produced applications follow operational-grade defaults rather than bare scaffolds. -- Preserved expectations for security/runtime/status components and stronger output quality gates. - -## Validation Basis - -- Operational readiness and completion status are documented in `docs/final_readiness_checklist.md`. -- Orchestrator and multi-generator alignment details are documented in `docs/orchestrator-multigenerator-upgrade-status.md`. -- Admin dashboard UX restructuring basis is documented in: - - `docs/admin-dashboard-ui-ux-browser-blueprint.md` - - `docs/admin-dashboard-section-linkage-checklist.md` -- Documentation-only baseline validation for this update: - - `npm --prefix frontend/frontend run test` - -## Risks - -- Tightening contract alignment can expose stale references in secondary documents or auxiliary diagnostic paths. -- Evidence-first completion criteria can downgrade previously tolerated partial states into explicit blockers. -- Admin dashboard workflow changes may alter operator navigation expectations until the new hub pattern is fully internalized. - -## Rollback Strategy - -- Roll back documentation and PR narrative independently if wording or scope grouping needs refinement. -- If a runtime/design interpretation needs to be reverted, restore the corresponding baseline in the status/readiness documents first so the repository does not present mismatched closure criteria. -- Preserve the service-package contract and evidence-based completion rule unless a repository-wide alternative standard is intentionally adopted. - -## Reviewer Focus - -- Verify that generator, validator, checklist, and capability documentation all describe the same `app/services/` package contract. -- Check that hard-gate validation is represented as a closed operational path rather than a partial quality signal. -- Review whether the admin dashboard redesign description accurately matches the current launcher-hub and overlay-window structure. -- Confirm that completion claims stay anchored to documented operational evidence. - -## Notes For Release / Reporting - -- This PR body is intended as a consolidated reporting layer for the current repository baseline. -- It is suitable for follow-up release notes, readiness reviews, or status reports that need one narrative covering generator structure, operational verification, and admin UI direction. - ---- - -## 4. 짧은 PR 본문 버전 - -## Summary -- 공개/관리자 운영 구조를 재정렬하고 생성기 계약을 `app/services/` 패키지 기준으로 단일화했다. -- 생성 직후 hard gate 검증, readiness checklist, self-run evidence, operational verification 흐름을 같은 판정 체계로 묶었다. -- 관리자 화면을 중앙 오케스트레이터 허브 기반 구조로 정리해 운영 제어와 증거 확인 흐름을 강화했다. - -## Validation -- `docs/final_readiness_checklist.md` -- `docs/orchestrator-multigenerator-upgrade-status.md` -- `npm --prefix frontend/frontend run test` - -## Reviewer Focus -- 서비스 패키지 계약 정합성 -- hard gate 및 readiness evidence 표현 정확성 -- 관리자 허브 UI 설명과 실제 구조의 일치 여부 From 85b895e531330cf966e51ffec5b13518e10e09de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:21:35 +0900 Subject: [PATCH 11/17] Potential fix for pull request finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정해주세요 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- run_profiler_backend.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/run_profiler_backend.py b/run_profiler_backend.py index fa3869f..19a6b96 100644 --- a/run_profiler_backend.py +++ b/run_profiler_backend.py @@ -23,7 +23,17 @@ logger = logging.getLogger(__name__) +def _is_container_runtime() -> bool: + return ( + Path("/.dockerenv").exists() + or Path("/run/.containerenv").exists() + or bool(os.getenv("KUBERNETES_SERVICE_HOST")) + ) + + def _default_profiler_host() -> str: + if _is_container_runtime(): + return "0.0.0.0" return "127.0.0.1" From da0fcce2850c9c7d4eba910dad1e5bfd843b8076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:23:46 +0900 Subject: [PATCH 12/17] Potential fix for pull request finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정해주세요 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backend/llm/orchestrator.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index d49739b..d7117ec 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2695,9 +2695,17 @@ def _runtime_progress_root() -> Path: return progress_root +def _legacy_orchestration_progress_file_path(run_id: str) -> Path: + safe_run_id = re.sub(r"[^A-Za-z0-9._-]", "_", str(run_id or "unknown")) + return _runtime_progress_root() / f"{safe_run_id}.json" + + def _orchestration_progress_file_path(run_id: str) -> Path: - safe_run_id = str(run_id or "unknown") - file_name = f"{hashlib.sha256(safe_run_id.encode('utf-8')).hexdigest()}.json" + normalized_run_id = str(run_id or "unknown") + legacy_path = _legacy_orchestration_progress_file_path(normalized_run_id) + if legacy_path.exists(): + return legacy_path + file_name = f"{hashlib.sha256(normalized_run_id.encode('utf-8')).hexdigest()}.json" return _runtime_progress_root() / file_name From 01a84e5b7578b7b1b74802df5a64aae8c6c4e630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:25:32 +0900 Subject: [PATCH 13/17] Potential fix for pull request finding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 변경해주세요, Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backend/auth.py | 64 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/backend/auth.py b/backend/auth.py index 38d3535..59e49d8 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -22,29 +22,59 @@ def _resolve_secret_key() -> tuple[str, bool]: if configured_file: fallback_path = Path(configured_file).expanduser() try: - if not fallback_path.exists(): - logger.error( - "SECRET_KEY_FILE is configured but does not exist: %s. Falling back to ephemeral runtime secret.", - str(fallback_path), - ) - elif not fallback_path.is_file(): - logger.error( - "SECRET_KEY_FILE is configured but is not a file: %s. Falling back to ephemeral runtime secret.", - str(fallback_path), - ) - else: + if fallback_path.exists(): + if not fallback_path.is_file(): + raise RuntimeError( + f"SECRET_KEY_FILE is configured but is not a file: {fallback_path}" + ) cached_secret = fallback_path.read_text(encoding="utf-8").strip() - if cached_secret: - return cached_secret, True - logger.error( - "SECRET_KEY_FILE is configured but empty: %s. Falling back to ephemeral runtime secret.", + if not cached_secret: + raise RuntimeError( + f"SECRET_KEY_FILE is configured but empty: {fallback_path}" + ) + return cached_secret, True + + fallback_path.parent.mkdir(parents=True, exist_ok=True) + generated_secret = secrets.token_urlsafe(48) + fd = os.open(str(fallback_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + with os.fdopen(fd, "w", encoding="utf-8") as secret_file: + secret_file.write(generated_secret) + secret_file.write("\n") + except Exception: + try: + fallback_path.unlink() + except OSError: + logger.exception( + "Failed to clean up partially written SECRET_KEY_FILE at %s.", + str(fallback_path), + ) + raise + + try: + os.chmod(fallback_path, 0o600) + except OSError: + logger.exception( + "Failed to set permissions on SECRET_KEY_FILE at %s.", str(fallback_path), ) - except Exception: + raise RuntimeError( + f"Failed to set secure permissions on SECRET_KEY_FILE: {fallback_path}" + ) + + logger.warning( + "SECRET_KEY_FILE did not exist; generated and stored a new persistent secret at %s with mode 0600.", + str(fallback_path), + ) + return generated_secret, True + except Exception as exc: logger.exception( - "Failed to read SECRET_KEY_FILE at %s. Falling back to ephemeral runtime secret.", + "Failed to initialize SECRET_KEY_FILE at %s. Refusing to start with an ephemeral runtime secret.", str(fallback_path), ) + raise RuntimeError( + f"Failed to initialize SECRET_KEY from SECRET_KEY_FILE: {fallback_path}" + ) from exc else: logger.error( "SECRET_KEY/SECRET_KEY_FILE is not configured; generating ephemeral runtime secret that invalidates tokens on restart." From 3378f3fd69cc6b905d21076c020defc6ba8961e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:30:59 +0900 Subject: [PATCH 14/17] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제안 사항에 맞게 수정해주세요. Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- backend/llm/orchestrator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index d7117ec..6f95c65 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2702,10 +2702,13 @@ def _legacy_orchestration_progress_file_path(run_id: str) -> Path: def _orchestration_progress_file_path(run_id: str) -> Path: normalized_run_id = str(run_id or "unknown") - legacy_path = _legacy_orchestration_progress_file_path(normalized_run_id) + safe_run_id = re.sub(r"[^A-Za-z0-9_.-]", "_", normalized_run_id).strip("._-") + if not safe_run_id: + safe_run_id = "unknown" + legacy_path = _legacy_orchestration_progress_file_path(safe_run_id) if legacy_path.exists(): return legacy_path - file_name = f"{hashlib.sha256(normalized_run_id.encode('utf-8')).hexdigest()}.json" + file_name = f"{hashlib.sha256(safe_run_id.encode('utf-8')).hexdigest()}.json" return _runtime_progress_root() / file_name From b12f631513cb1bdfcd6ac54571b9f17538c1afce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:33:26 +0900 Subject: [PATCH 15/17] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 제안 사항 감사합니다, 수정해주세요 Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- backend/llm/orchestrator.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index 6f95c65..30dba5a 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2705,11 +2705,20 @@ def _orchestration_progress_file_path(run_id: str) -> Path: safe_run_id = re.sub(r"[^A-Za-z0-9_.-]", "_", normalized_run_id).strip("._-") if not safe_run_id: safe_run_id = "unknown" + runtime_root = _runtime_progress_root().resolve() legacy_path = _legacy_orchestration_progress_file_path(safe_run_id) - if legacy_path.exists(): - return legacy_path + try: + resolved_legacy_path = legacy_path.resolve() + if resolved_legacy_path.exists() and resolved_legacy_path.is_relative_to(runtime_root): + return resolved_legacy_path + except Exception: + logger.warning( + "Ignoring unsafe legacy orchestration progress path for run_id=%s", + safe_run_id, + exc_info=True, + ) file_name = f"{hashlib.sha256(safe_run_id.encode('utf-8')).hexdigest()}.json" - return _runtime_progress_root() / file_name + return runtime_root / file_name def _build_progress_poll_url(run_id: str) -> str: From 609c3614b3d65caf1b13723dc2474187e8f7724c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=B2=A0=ED=99=8D?= <111139476+parkcheolhong@users.noreply.github.com> Date: Mon, 11 May 2026 21:46:59 +0900 Subject: [PATCH 16/17] Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수정해주세요 Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- backend/llm/orchestrator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index 30dba5a..526c92b 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2735,6 +2735,11 @@ def _save_orchestration_progress(run_id: str, payload: Dict[str, Any]) -> Dict[s normalized.setdefault("updated_at", datetime.utcnow().isoformat() + "Z") _ORCHESTRATION_PROGRESS_STORE[normalized["run_id"]] = normalized progress_path = _orchestration_progress_file_path(normalized["run_id"]) + trusted_temp_prefix = ( + f"progress-{hashlib.sha256(normalized['run_id'].encode('utf-8')).hexdigest()}." + if normalized["run_id"] + else "progress-unknown." + ) with _ORCHESTRATION_PROGRESS_FILE_LOCK: temp_path: Optional[Path] = None try: @@ -2742,7 +2747,7 @@ def _save_orchestration_progress(run_id: str, payload: Dict[str, Any]) -> Dict[s mode="w", encoding="utf-8", dir=str(progress_path.parent), - prefix=f"{progress_path.name}.", + prefix=trusted_temp_prefix, suffix=".tmp", delete=False, ) as temp_file: From b3c314a1ce3e2e074d96a66e2704f2329ee677b9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 22:02:49 +0900 Subject: [PATCH 17/17] Harden orchestrator progress file paths and profiler bind host policy (#17) * fix: harden path handling and profiler bind defaults for CodeQL alerts Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/320282cc-04c8-415c-bacc-8f409266cde2 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: address review nits for host and run_id normalization Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/320282cc-04c8-415c-bacc-8f409266cde2 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * fix: tighten localhost and hostname validation for profiler bind Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/320282cc-04c8-415c-bacc-8f409266cde2 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> * chore: improve profiler host validation diagnostics Agent-Logs-Url: https://github.com/parkcheolhong/codeAI/sessions/320282cc-04c8-415c-bacc-8f409266cde2 Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: parkcheolhong <111139476+parkcheolhong@users.noreply.github.com> --- backend/llm/orchestrator.py | 25 ++++--------------------- run_profiler_backend.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/backend/llm/orchestrator.py b/backend/llm/orchestrator.py index 526c92b..d60218e 100644 --- a/backend/llm/orchestrator.py +++ b/backend/llm/orchestrator.py @@ -2695,29 +2695,12 @@ def _runtime_progress_root() -> Path: return progress_root -def _legacy_orchestration_progress_file_path(run_id: str) -> Path: - safe_run_id = re.sub(r"[^A-Za-z0-9._-]", "_", str(run_id or "unknown")) - return _runtime_progress_root() / f"{safe_run_id}.json" - - def _orchestration_progress_file_path(run_id: str) -> Path: - normalized_run_id = str(run_id or "unknown") - safe_run_id = re.sub(r"[^A-Za-z0-9_.-]", "_", normalized_run_id).strip("._-") - if not safe_run_id: - safe_run_id = "unknown" + normalized_run_id = str(run_id if run_id is not None else "unknown") + if normalized_run_id == "": + normalized_run_id = "unknown" runtime_root = _runtime_progress_root().resolve() - legacy_path = _legacy_orchestration_progress_file_path(safe_run_id) - try: - resolved_legacy_path = legacy_path.resolve() - if resolved_legacy_path.exists() and resolved_legacy_path.is_relative_to(runtime_root): - return resolved_legacy_path - except Exception: - logger.warning( - "Ignoring unsafe legacy orchestration progress path for run_id=%s", - safe_run_id, - exc_info=True, - ) - file_name = f"{hashlib.sha256(safe_run_id.encode('utf-8')).hexdigest()}.json" + file_name = f"{hashlib.sha256(normalized_run_id.encode('utf-8')).hexdigest()}.json" return runtime_root / file_name diff --git a/run_profiler_backend.py b/run_profiler_backend.py index 19a6b96..55c66ab 100644 --- a/run_profiler_backend.py +++ b/run_profiler_backend.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ipaddress import logging import os import socket @@ -32,8 +33,35 @@ def _is_container_runtime() -> bool: def _default_profiler_host() -> str: - if _is_container_runtime(): - return "0.0.0.0" + return "127.0.0.1" + + +def _resolve_profiler_host() -> str: + requested_host = (os.getenv("BACKEND_PROFILER_HOST") or _default_profiler_host()).strip() + allow_remote = (os.getenv("BACKEND_PROFILER_ALLOW_REMOTE", "") or "").strip().lower() in {"1", "true", "yes", "on"} + if requested_host == "localhost": + try: + infos = socket.getaddrinfo("localhost", None) + if infos and all(ipaddress.ip_address(info[4][0]).is_loopback for info in infos): + return requested_host + except Exception: + logger.warning("[WARN] failed to resolve localhost loopback addresses", exc_info=True) + logger.warning("[WARN] localhost does not resolve to loopback only; fallback to 127.0.0.1") + return "127.0.0.1" + if requested_host in {"127.0.0.1", "::1"}: + return requested_host + try: + requested_ip = ipaddress.ip_address(requested_host) + except (TypeError, ValueError): + logger.warning("[WARN] hostname profiler host=%s is not allowed; fallback to 127.0.0.1", requested_host) + return "127.0.0.1" + if requested_ip.is_loopback: + return requested_host + if allow_remote: + if requested_ip.is_unspecified: + logger.warning("[WARN] profiler backend is binding to all interfaces (host=%s)", requested_host) + return requested_host + logger.warning("[WARN] remote profiler host=%s blocked; set BACKEND_PROFILER_ALLOW_REMOTE=true to allow", requested_host) return "127.0.0.1" @@ -68,7 +96,7 @@ def _resolve_bind_port(host: str, requested_port: int, max_attempts: int = 20) - def main() -> None: import uvicorn - host = os.getenv("BACKEND_PROFILER_HOST", _default_profiler_host()) + host = _resolve_profiler_host() port = _resolve_bind_port(host, int(os.getenv("BACKEND_PROFILER_PORT", "8000"))) logger.info("[OK] profiler backend bind target: http://%s:%s", host, port) uvicorn.run(app, host=host, port=port, reload=False)