Skip to content

Commit 1979f76

Browse files
committed
sec: clear wizard-set API keys from os.environ after session ends
Track env vars the CLI wizard adds for LiteLLM models and remove them in a finally block when the session loop ends, including on Ctrl+C. Pre-existing keys are never deleted. Adds unit tests for the tracking and cleanup helpers. Fixes #5
1 parent 6c1fba2 commit 1979f76

2 files changed

Lines changed: 94 additions & 16 deletions

File tree

cli.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import os
22
import sys
3+
from typing import List, Optional
4+
35
from rich.console import Console
46
from rich.panel import Panel
57
from rich.prompt import Prompt, Confirm
@@ -50,7 +52,31 @@
5052
)
5153
]
5254

53-
def create_custom_agent_wizard() -> AgentConfig:
55+
56+
def _set_session_env_key(tracked_keys: List[str], key_name: str, value: str) -> None:
57+
"""Set a wizard-provided secret and track it for cleanup. Skips if the key already exists."""
58+
if not key_name or key_name in os.environ:
59+
return
60+
os.environ[key_name] = value
61+
tracked_keys.append(key_name)
62+
63+
64+
def _prompt_api_key_if_needed(tracked_keys: List[str], key_prompt: str = "Enter the environment variable name (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY)") -> None:
65+
"""Prompt for an API key env var when the model needs one; track keys set during this session."""
66+
if not Confirm.ask("Does this model require an API Key?"):
67+
return
68+
key_name = Prompt.ask(key_prompt)
69+
if key_name and key_name not in os.environ:
70+
_set_session_env_key(tracked_keys, key_name, Prompt.ask(f"Enter your {key_name}", password=True))
71+
72+
73+
def _cleanup_session_env(tracked_keys: List[str]) -> None:
74+
"""Remove environment variables that were added by the wizard for this session."""
75+
for key in tracked_keys:
76+
os.environ.pop(key, None)
77+
78+
79+
def create_custom_agent_wizard(tracked_env_keys: Optional[List[str]] = None) -> AgentConfig:
5480
"""Guided wizard to create a brand new agent."""
5581
console.print(Panel("[bold yellow]Create Custom Agent[/bold yellow]"))
5682
name = Prompt.ask("Agent Name")
@@ -78,11 +104,8 @@ def create_custom_agent_wizard() -> AgentConfig:
78104
config.model = model_str
79105

80106
if not model_str.startswith("ollama/"):
81-
if Confirm.ask("Does this model require an API Key?"):
82-
key_name = Prompt.ask("Enter the environment variable name (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY)")
83-
if key_name and key_name not in os.environ:
84-
os.environ[key_name] = Prompt.ask(f"Enter your {key_name}", password=True)
85-
107+
_prompt_api_key_if_needed(tracked_env_keys or [])
108+
86109
config.color = Prompt.ask("CLI output color (e.g. red, green, blue, cyan, magenta, yellow)", default="blue")
87110
config.temperature = float(Prompt.ask("Generation Temperature", default="0.7"))
88111
return config
@@ -96,6 +119,7 @@ def main_menu():
96119
user_name = Prompt.ask("Your name (or alias)", default="User")
97120
user_background = Prompt.ask("Brief background or role (e.g. 'CTO with 15 years in cloud infrastructure')", default="")
98121
user_profile = {"name": user_name, "background": user_background}
122+
tracked_env_keys: List[str] = []
99123

100124
# 1. Session Basics
101125
console.print("\n[bold cyan]--- 1. Session Setup ---[/bold cyan]")
@@ -130,7 +154,7 @@ def main_menu():
130154

131155
while True:
132156
if Confirm.ask("Would you like to build and invite a Custom Agent?", default=False):
133-
custom_agent = create_custom_agent_wizard()
157+
custom_agent = create_custom_agent_wizard(tracked_env_keys)
134158
active_agent_configs.append(custom_agent)
135159
else:
136160
break
@@ -150,10 +174,10 @@ def main_menu():
150174
model = Prompt.ask("Orchestrator Model", default="ollama/llama3")
151175

152176
if not model.startswith("ollama/"):
153-
if Confirm.ask("Does this orchestrator model require an API Key?"):
154-
key_name = Prompt.ask("Enter the environment variable name (e.g. OPENAI_API_KEY)")
155-
if key_name and key_name not in os.environ:
156-
os.environ[key_name] = Prompt.ask(f"Enter your {key_name}", password=True)
177+
_prompt_api_key_if_needed(
178+
tracked_env_keys,
179+
key_prompt="Enter the environment variable name (e.g. OPENAI_API_KEY)",
180+
)
157181

158182
orchestrator_config = AgentConfig(
159183
name="System Moderator",
@@ -176,13 +200,19 @@ def main_menu():
176200
)
177201

178202
console.print("\n[bold yellow]Starting Room Session...[/bold yellow]")
179-
run_session(session_config, agents, user_profile)
203+
run_session(session_config, agents, user_profile, tracked_env_keys)
180204

181-
def run_session(config: SessionConfig, agents: list[Agent], user_profile: dict = None):
205+
def run_session(
206+
config: SessionConfig,
207+
agents: list[Agent],
208+
user_profile: dict = None,
209+
tracked_env_keys: Optional[List[str]] = None,
210+
):
182211
session = Session(config, agents, user_profile=user_profile)
183-
212+
env_keys = tracked_env_keys if tracked_env_keys is not None else []
213+
184214
console.print(Panel(session.global_intro, title="System Introduction", border_style="bold grey53"))
185-
215+
186216
try:
187217
while session.turn_count < config.max_turns:
188218
# Human in the loop logic (interval OR early trigger if user is addressed)
@@ -217,7 +247,9 @@ def run_session(config: SessionConfig, agents: list[Agent], user_profile: dict =
217247

218248
except KeyboardInterrupt:
219249
console.print("\n[yellow]Session interrupted via keyboard.[/yellow]")
220-
250+
finally:
251+
_cleanup_session_env(env_keys)
252+
221253
console.print("\n[bold green]Session ended.[/bold green]")
222254
prompt_save(session)
223255

tests/test_cli.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import os
2+
3+
import cli
4+
5+
6+
def test_set_session_env_key_tracks_new_keys():
7+
tracked = []
8+
key = "ROOMS_TEST_WIZARD_KEY"
9+
os.environ.pop(key, None)
10+
11+
cli._set_session_env_key(tracked, key, "secret-value")
12+
13+
assert tracked == [key]
14+
assert os.environ[key] == "secret-value"
15+
16+
cli._cleanup_session_env(tracked)
17+
assert key not in os.environ
18+
19+
20+
def test_set_session_env_key_skips_existing_keys():
21+
tracked = []
22+
key = "ROOMS_TEST_EXISTING_KEY"
23+
os.environ[key] = "pre-existing"
24+
25+
cli._set_session_env_key(tracked, key, "new-value")
26+
27+
assert tracked == []
28+
assert os.environ[key] == "pre-existing"
29+
30+
os.environ.pop(key, None)
31+
32+
33+
def test_cleanup_session_env_removes_only_tracked_keys():
34+
wizard_key = "ROOMS_TEST_CLEANUP_WIZARD"
35+
existing_key = "ROOMS_TEST_CLEANUP_EXISTING"
36+
os.environ.pop(wizard_key, None)
37+
os.environ[existing_key] = "keep-me"
38+
39+
tracked = []
40+
cli._set_session_env_key(tracked, wizard_key, "wizard-secret")
41+
cli._cleanup_session_env(tracked)
42+
43+
assert wizard_key not in os.environ
44+
assert os.environ[existing_key] == "keep-me"
45+
46+
os.environ.pop(existing_key, None)

0 commit comments

Comments
 (0)