Skip to content

Commit fd4e801

Browse files
committed
fix: avoid runtime makedirs in read-only install locations (Windows Program Files)
- Replace runtime hook with build-time inclusion of fakeredis/model/__init__.py - Ensures model/ directory exists in bundle without needing write access at runtime - Fixes PermissionError on Windows when installed to Program Files via MSI
1 parent b05dbf0 commit fd4e801

File tree

7 files changed

+4004
-3928
lines changed

7 files changed

+4004
-3928
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 82ccabdd2928761d0ad226a0cd42ef767564f830

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Include package data
2-
recursive-include pantheon/factory/templates *.md *.json
2+
recursive-include pantheon/factory/templates *.md *.json *.example
33
recursive-include pantheon/toolsets/database_api/schemas *.json
44

55
# Include documentation

build_backend.spec

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
block_cipher = None
44

55
from PyInstaller.utils.hooks import copy_metadata, collect_data_files
6-
import os, sys, tempfile
6+
import os, sys
77

88
datas = [
99
('pantheon/factory/templates', 'pantheon/factory/templates'),
@@ -15,17 +15,14 @@ datas += copy_metadata('fastmcp')
1515
datas += collect_data_files('litellm', includes=['**/*.json'])
1616
# fakeredis: model/_command_info.py loads os.path.join(dirname(__file__), '..', 'commands.json')
1717
# PyInstaller must include the JSON so the relative path resolves at runtime.
18+
# Also include model/__init__.py so the model/ directory exists in the bundle
19+
# (avoids runtime os.makedirs in read-only install locations like Program Files).
1820
datas += collect_data_files('fakeredis', includes=['commands.json'])
19-
20-
# Runtime hook: ensure fakeredis/model/ dir exists so ../commands.json resolves.
21-
_rthook = tempfile.NamedTemporaryFile('w', suffix='.py', delete=False)
22-
_rthook.write(
23-
"import sys, os\n"
24-
"d = getattr(sys, '_MEIPASS', None)\n"
25-
"if d:\n"
26-
" os.makedirs(os.path.join(d, 'fakeredis', 'model'), exist_ok=True)\n"
27-
)
28-
_rthook.close()
21+
import fakeredis
22+
_fakeredis_dir = os.path.dirname(fakeredis.__file__)
23+
_model_init = os.path.join(_fakeredis_dir, 'model', '__init__.py')
24+
if os.path.exists(_model_init):
25+
datas.append((_model_init, 'fakeredis/model'))
2926

3027
a = Analysis(
3128
['pantheon/chatroom/__main__.py'],
@@ -65,7 +62,7 @@ a = Analysis(
6562
],
6663
hookspath=[],
6764
hooksconfig={},
68-
runtime_hooks=[_rthook.name],
65+
runtime_hooks=[],
6966
excludes=[
7067
# ── Knowledge / vector DB (lancedb only used in RAG toolset, lazy import) ──
7168
'lancedb', 'lance', 'pyarrow', 'llama_index', 'qdrant_client',

pantheon/chatroom/room.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,3 +2033,35 @@ async def reload_settings(self) -> dict:
20332033
"success": False,
20342034
"message": f"Failed to reload settings: {str(e)}"
20352035
}
2036+
2037+
@tool(exclude=True)
2038+
async def check_api_keys(self) -> dict:
2039+
"""Check the configuration status of LLM API keys.
2040+
2041+
Returns a dict with each key's status (configured, source, masked value)
2042+
and whether any key is configured at all.
2043+
"""
2044+
import os
2045+
from pantheon.settings import get_settings
2046+
2047+
settings = get_settings()
2048+
key_names = [
2049+
"OPENAI_API_KEY",
2050+
"ANTHROPIC_API_KEY",
2051+
"GEMINI_API_KEY",
2052+
"DEEPSEEK_API_KEY",
2053+
]
2054+
2055+
keys = {}
2056+
for key in key_names:
2057+
value = settings.get_api_key(key)
2058+
if value:
2059+
# Determine source
2060+
source = "env" if os.environ.get(key) else "settings"
2061+
masked = value[:6] + "***" if len(value) > 6 else "***"
2062+
keys[key] = {"configured": True, "source": source, "masked": masked}
2063+
else:
2064+
keys[key] = {"configured": False, "source": None, "masked": None}
2065+
2066+
has_any_key = any(v["configured"] for v in keys.values())
2067+
return {"keys": keys, "has_any_key": has_any_key}

pantheon/chatroom/start.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import os
3+
import shutil
34
import sys
45
import uuid
56
from pathlib import Path
@@ -547,6 +548,16 @@ async def cleanup_zombie_nats(work_dir: Path):
547548
memory_dir = str(Path(memory_dir).resolve())
548549
workspace_path = str(Path(workspace_path).resolve())
549550

551+
# ===== Ensure .env exists (create from template if missing) =====
552+
env_file = Path(workspace_path) / ".env"
553+
if not env_file.exists():
554+
env_template = Path(__file__).resolve().parent.parent / "factory" / "templates" / ".env.example"
555+
if env_template.exists():
556+
shutil.copy2(str(env_template), str(env_file))
557+
logger.info(f"[STARTUP] Created .env template at {env_file}")
558+
else:
559+
logger.warning(f"[STARTUP] .env.example template not found at {env_template}")
560+
550561
# ===== Step 1: Start or connect Endpoint =====
551562
endpoint = None
552563
final_endpoint_service_id = endpoint_service_id
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ========================================
2+
# Pantheon API Keys Configuration
3+
# ========================================
4+
#
5+
# After editing this file, use "Reload Settings" in the UI
6+
# or restart the service to apply changes.
7+
#
8+
# Priority: Environment Variables > .env > settings.json
9+
#
10+
# ========================================
11+
12+
# OpenAI API Key (GPT-4, GPT-3.5, etc.)
13+
#OPENAI_API_KEY=sk-your-openai-key-here
14+
15+
# Anthropic API Key (Claude models)
16+
#ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
17+
18+
# Google Gemini API Key
19+
#GEMINI_API_KEY=your-gemini-key-here
20+
21+
# DeepSeek API Key
22+
#DEEPSEEK_API_KEY=your-deepseek-key-here
23+
24+
# ========================================
25+
# Advanced Configuration (Optional)
26+
# ========================================
27+
28+
# Custom LiteLLM endpoint
29+
#LITELLM_BASE_URL=https://your-litellm-proxy.com
30+
31+
# Debug mode
32+
#DEBUG=false

0 commit comments

Comments
 (0)