Skip to content

Commit 2c08695

Browse files
authored
Merge pull request #167 from hud-evals/l/folder-env-var
Add auto env passing
2 parents 897be3f + 4d124b8 commit 2c08695

File tree

17 files changed

+253
-70
lines changed

17 files changed

+253
-70
lines changed

docs/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"navigation": {
3030
"versions": [
3131
{
32-
"version": "0.4.52",
32+
"version": "0.4.53",
3333
"groups": [
3434
{
3535
"group": "Get Started",

docs/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ icon: "book"
55
---
66

77
<Note>
8-
**Version 0.4.52** - Latest stable release
8+
**Version 0.4.53** - Latest stable release
99
</Note>
1010

1111
<CardGroup cols={3}>

docs/train-agents/quickstart.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ title: "RL Quickstart"
33
icon: "graduation-cap"
44
---
55

6+
## Prerequisites
7+
8+
- HUD API key: Remote training requires authentication. Set `HUD_API_KEY` before running:
9+
10+
```bash
11+
export HUD_API_KEY="sk-hud-..." # get one at https://hud.so
12+
# Or persist it locally:
13+
hud set HUD_API_KEY=sk-hud-...
14+
```
15+
16+
- Docker daemon: For local runs (using `--local`) or when training against a local Docker image, ensure Docker Desktop is installed and the Docker daemon is running.
17+
618
## Quickstart
719

820
Install and download a taskset:

environments/blank/server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
description = "MCP server for blank environment"
55
requires-python = ">=3.11"
66
dependencies = [
7-
"hud-python>=0.4.52",
7+
"hud-python>=0.4.53",
88
"httpx>=0.28.1",
99
]
1010

environments/browser/server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
description = "HUD Browser MCP Server"
55
requires-python = ">=3.11,<3.14"
66
dependencies = [
7-
"hud-python>=0.4.52",
7+
"hud-python>=0.4.53",
88
"httpx",
99
"playwright",
1010
"pyautogui",

environments/deepresearch/server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
description = "MCP server for DeepResearch environment"
55
requires-python = ">=3.11"
66
dependencies = [
7-
"hud-python>=0.4.52",
7+
"hud-python>=0.4.53",
88
"httpx>=0.24.0",
99
]
1010

hud/cli/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,18 @@ def debug(
242242
if build and not build_environment(directory, image_name):
243243
raise typer.Exit(1)
244244

245-
# Build Docker command
246-
from .utils.docker import build_run_command
245+
# Build Docker command with folder-mode envs
246+
from .utils.docker import create_docker_run_command
247247

248-
command = build_run_command(image_name, docker_args)
248+
command = create_docker_run_command(
249+
image_name, docker_args=docker_args, env_dir=directory
250+
)
249251
else:
250252
# Assume it's an image name
251253
image = first_param
252254
from .utils.docker import build_run_command
253255

256+
# Image-only mode: do not auto-inject local .env
254257
command = build_run_command(image, docker_args)
255258
else:
256259
console.print(

hud/cli/build.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -161,49 +161,42 @@ async def analyze_mcp_environment(
161161
hud_console = HUDConsole()
162162
env_vars = env_vars or {}
163163

164-
# Build Docker command to run the image
165-
docker_cmd = ["docker", "run", "--rm", "-i"]
164+
# Build Docker command to run the image, injecting any provided env vars
165+
from hud.cli.utils.docker import build_env_flags
166166

167-
# Add environment variables
168-
for key, value in env_vars.items():
169-
docker_cmd.extend(["-e", f"{key}={value}"])
167+
docker_cmd = ["docker", "run", "--rm", "-i", *build_env_flags(env_vars), image]
170168

171-
docker_cmd.append(image)
169+
# Show full docker command being used for analysis
170+
hud_console.dim_info("Command:", " ".join(docker_cmd))
172171

173-
# Create MCP config
174-
config = {
175-
"server": {"command": docker_cmd[0], "args": docker_cmd[1:] if len(docker_cmd) > 1 else []}
176-
}
172+
# Create MCP config consistently with analyze helpers
173+
from hud.cli.analyze import parse_docker_command
174+
175+
mcp_config = parse_docker_command(docker_cmd)
177176

178177
# Initialize client and measure timing
179178
start_time = time.time()
180-
client = MCPClient(mcp_config=config, verbose=verbose, auto_trace=False)
179+
client = MCPClient(mcp_config=mcp_config, verbose=verbose, auto_trace=False)
181180
initialized = False
182181

183182
try:
184183
if verbose:
185-
hud_console.info(f"Initializing MCP client with command: {' '.join(docker_cmd)}")
184+
hud_console.info("Initializing MCP client...")
186185

187-
# Add timeout to fail fast instead of hanging (30 seconds)
186+
# Add timeout to fail fast instead of hanging (60 seconds)
188187
await asyncio.wait_for(client.initialize(), timeout=60.0)
189188
initialized = True
190189
initialize_ms = int((time.time() - start_time) * 1000)
191190

192-
# Get tools
193-
tools = await client.list_tools()
194-
195-
# Extract tool information
196-
tool_info = []
197-
for tool in tools:
198-
tool_dict = {"name": tool.name, "description": tool.description}
199-
if hasattr(tool, "inputSchema") and tool.inputSchema:
200-
tool_dict["inputSchema"] = tool.inputSchema
201-
tool_info.append(tool_dict)
191+
# Delegate to standard analysis helper for consistency
192+
full_analysis = await client.analyze_environment()
202193

194+
# Normalize to build's expected fields
195+
tools_list = full_analysis.get("tools", [])
203196
return {
204197
"initializeMs": initialize_ms,
205-
"toolCount": len(tools),
206-
"tools": tool_info,
198+
"toolCount": len(tools_list),
199+
"tools": tools_list,
207200
"success": True,
208201
}
209202
except TimeoutError:
@@ -295,6 +288,10 @@ def build_environment(
295288
hud_console.error(f"Directory not found: {directory}")
296289
raise typer.Exit(1)
297290

291+
from hud.cli.utils.docker import require_docker_running
292+
293+
require_docker_running()
294+
298295
# Step 1: Check for hud.lock.yaml (previous build)
299296
lock_path = env_dir / "hud.lock.yaml"
300297
base_name = None
@@ -355,13 +352,24 @@ def build_environment(
355352

356353
hud_console.success(f"Built temporary image: {temp_tag}")
357354

358-
# Analyze the environment
355+
# Analyze the environment (merge folder .env if present)
359356
hud_console.progress_message("Analyzing MCP environment...")
360357

361358
loop = asyncio.new_event_loop()
362359
asyncio.set_event_loop(loop)
363360
try:
364-
analysis = loop.run_until_complete(analyze_mcp_environment(temp_tag, verbose, env_vars))
361+
# Merge .env from env_dir for analysis only
362+
try:
363+
from hud.cli.utils.docker import load_env_vars_for_dir
364+
365+
env_from_file = load_env_vars_for_dir(env_dir)
366+
except Exception:
367+
env_from_file = {}
368+
merged_env_for_analysis = {**env_from_file, **(env_vars or {})}
369+
370+
analysis = loop.run_until_complete(
371+
analyze_mcp_environment(temp_tag, verbose, merged_env_for_analysis)
372+
)
365373
except Exception as e:
366374
hud_console.error(f"Failed to analyze MCP environment: {e}")
367375
hud_console.info("")

hud/cli/dev.py

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -504,15 +504,12 @@ def run_docker_dev_server(
504504
base_name = image_name.replace(":", "-").replace("/", "-")
505505
container_name = f"{base_name}-dev-{pid}"
506506

507-
# Build docker run command with volume mounts
508-
docker_cmd = [
509-
"docker",
510-
"run",
511-
"--rm",
512-
"-i",
507+
# Build docker run command with volume mounts and folder-mode envs
508+
from .utils.docker import create_docker_run_command
509+
510+
base_args = [
513511
"--name",
514512
container_name,
515-
# Mount both server and environment for hot-reload
516513
"-v",
517514
f"{env_dir.absolute()}/server:/app/server:rw",
518515
"-v",
@@ -524,29 +521,14 @@ def run_docker_dev_server(
524521
"-e",
525522
"HUD_DEV=1",
526523
]
524+
combined_args = [*base_args, *docker_args] if docker_args else base_args
525+
docker_cmd = create_docker_run_command(
526+
image_name,
527+
docker_args=combined_args,
528+
env_dir=env_dir,
529+
)
527530

528-
# Load .env file if present
529-
env_file = env_dir / ".env"
530-
loaded_env_vars: dict[str, str] = {}
531-
if env_file.exists():
532-
try:
533-
from hud.cli.utils.config import parse_env_file
534-
535-
env_contents = env_file.read_text(encoding="utf-8")
536-
loaded_env_vars = parse_env_file(env_contents)
537-
for key, value in loaded_env_vars.items():
538-
docker_cmd.extend(["-e", f"{key}={value}"])
539-
if verbose and loaded_env_vars:
540-
hud_console.info(f"Loaded {len(loaded_env_vars)} env var(s) from .env")
541-
except Exception as e:
542-
hud_console.warning(f"Failed to load .env file: {e}")
543-
544-
# Add user-provided Docker arguments
545-
if docker_args:
546-
docker_cmd.extend(docker_args)
547-
548-
# Append the image name
549-
docker_cmd.append(image_name)
531+
# Env flags already injected by create_docker_run_command
550532

551533
# Print startup info
552534
hud_console.header("HUD Development Mode (Docker)")

hud/cli/tests/test_build.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,17 @@ async def test_analyze_success(self, mock_client_class):
219219
mock_tool.description = "Test tool"
220220
mock_tool.inputSchema = {"type": "object"}
221221

222+
# Prefer analyze_environment path (aligns with analyze CLI tests)
223+
mock_client.analyze_environment = mock.AsyncMock(
224+
return_value={
225+
"metadata": {"servers": ["local"], "initialized": True},
226+
"tools": [{"name": "test_tool", "description": "Test tool"}],
227+
"hub_tools": {},
228+
"resources": [],
229+
"telemetry": {},
230+
}
231+
)
232+
# Fallback still defined for completeness
222233
mock_client.list_tools.return_value = [mock_tool]
223234

224235
result = await analyze_mcp_environment("test:latest")
@@ -247,6 +258,15 @@ async def test_analyze_verbose_mode(self, mock_client_class):
247258
"""Test analysis in verbose mode."""
248259
mock_client = mock.AsyncMock()
249260
mock_client_class.return_value = mock_client
261+
mock_client.analyze_environment = mock.AsyncMock(
262+
return_value={
263+
"metadata": {"servers": ["local"], "initialized": True},
264+
"tools": [],
265+
"hub_tools": {},
266+
"resources": [],
267+
"telemetry": {},
268+
}
269+
)
250270
mock_client.list_tools.return_value = []
251271

252272
# Just test that it runs without error in verbose mode

0 commit comments

Comments
 (0)