From 6ab6da37195909996000246f6877c89a61a03234 Mon Sep 17 00:00:00 2001 From: changukyu Date: Sat, 11 Jul 2026 16:07:49 +0900 Subject: [PATCH] chore: startup path anchoring, env parsing and gitignore - Launcher guards Python 3.13 before importing the package and chdirs to the repo root so cwd-relative default paths resolve consistently. - main() logs the resolved .env path and warns on a missing static dir or a short auth token; env_file reads utf-8-sig so a BOM doesn't drop the first variable. - .gitignore covers .claude/settings.local.json and .impeccable/. --- .gitignore | 4 ++++ run_copyrighter_server.py | 12 ++++++++++++ src/rights_filter/server/__main__.py | 21 +++++++++++++++++++-- src/rights_filter/server/env_file.py | 4 +++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b5d3068..0f1cec5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,10 @@ artifacts/ tmp_* tmp_dbg_img/ +# Local tool state (do not rely on the machine-global gitignore) +.claude/settings.local.json +.impeccable/ + # OS .DS_Store Thumbs.db diff --git a/run_copyrighter_server.py b/run_copyrighter_server.py index d36d5d7..5f76041 100644 --- a/run_copyrighter_server.py +++ b/run_copyrighter_server.py @@ -1,6 +1,14 @@ from pathlib import Path +import os import sys +# Guard BEFORE importing rights_filter so a 3.13-only syntax in the package +# surfaces this friendly message instead of a raw SyntaxError/ImportError. +if sys.version_info < (3, 13): + sys.exit( + "Copyrighter requires Python 3.13+ — activate the offline venv " + "(see docs/operations/offline-install.md)." + ) ROOT = Path(__file__).resolve().parent SRC = ROOT / "src" @@ -12,4 +20,8 @@ from rights_filter.server.__main__ import main if __name__ == "__main__": + # Anchor the working directory to the repo root so the cwd-relative default + # paths (data/, web/operator-gui) resolve the same regardless of where the + # launcher was invoked from. + os.chdir(ROOT) main() diff --git a/src/rights_filter/server/__main__.py b/src/rights_filter/server/__main__.py index 6192655..b81719a 100644 --- a/src/rights_filter/server/__main__.py +++ b/src/rights_filter/server/__main__.py @@ -33,7 +33,12 @@ def main() -> None: parser.add_argument("--env", default=".env") args = parser.parse_args() - load_env_file(Path(args.env), os.environ) + env_path = Path(args.env) + load_env_file(env_path, os.environ) + # Log the resolved env path (and whether it was found) so an operator running + # from an unexpected directory can see which .env — if any — was actually + # loaded, rather than silently getting no token / disabled providers. + print(f".env: {env_path.resolve()} ({'loaded' if env_path.exists() else 'not found'})") provider_runtime = build_provider_runtime(os.environ) print("Provider readiness: " + _provider_readiness(provider_runtime)) image_store = LocalSubmissionImageStore(Path(args.images)) @@ -44,13 +49,25 @@ def main() -> None: auth_token = os.environ.get("COPYRIGHTER_AUTH_TOKEN", "").strip() or None if auth_token is None: print("WARNING: COPYRIGHTER_AUTH_TOKEN not set — data routes are unauthenticated.") + elif len(auth_token) < 32: + print( + "WARNING: COPYRIGHTER_AUTH_TOKEN is shorter than 32 characters — " + "use a longer, high-entropy token (e.g. `python -c \"import secrets; print(secrets.token_urlsafe(32))\"`)." + ) + + static_path = Path(args.static) + if not static_path.exists(): + print( + f"WARNING: static GUI directory not found: {static_path.resolve()} — " + "the operator UI will return 404. Pass --static or run from the repo root." + ) server = build_server( host=args.host, port=args.port, store=store, image_store=image_store, - static_dir=Path(args.static), + static_dir=static_path, auth_token=auth_token, ) print(f"Copyrighter API server listening on http://{args.host}:{args.port}") diff --git a/src/rights_filter/server/env_file.py b/src/rights_filter/server/env_file.py index 5161286..10dd3e2 100644 --- a/src/rights_filter/server/env_file.py +++ b/src/rights_filter/server/env_file.py @@ -9,7 +9,9 @@ def load_env_file(path: Path | str, environ: MutableMapping[str, str]) -> None: if not env_path.exists(): return - for raw_line in env_path.read_text(encoding="utf-8").splitlines(): + # utf-8-sig so a BOM on the file doesn't get glued to the first key (which + # would silently drop the first variable — it would never match its name). + for raw_line in env_path.read_text(encoding="utf-8-sig").splitlines(): line = raw_line.strip() if not line or line.startswith("#"): continue