Skip to content

Commit 02cc9f0

Browse files
committed
fix(cli): load the agent's .env into locally run ACP and worker processes
The docs say a .env next to manifest.yaml is loaded automatically for local development, but nothing did: agentex agents run built the child environment from os.environ plus the manifest, and EnvironmentVariables.refresh() looks for .env two directories above the installed module (site-packages), never the agent folder. Values in .env only reached a process when some import happened to call load_dotenv() first (litellm does, on import). That is why the OpenAI Agents Temporal template worked (its model client is built lazily after imports) while the Pydantic AI one failed at import time with openai.OpenAIError: Missing credentials. Merge <manifest dir>/.env into the environment handed to both the ACP and worker subprocesses, without overriding variables already set in the shell (python-dotenv semantics). Add tests for merge, shell precedence, and the no-file / no-manifest_dir cases. Claude-Session: https://claude.ai/code/session_01HCVKnA7LeJZ44nxZz1uzF3
1 parent 0db6037 commit 02cc9f0

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

‎src/agentex/lib/cli/handlers/run_handlers.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
from pathlib import Path
77

8+
from dotenv import dotenv_values
89
from rich.panel import Panel
910
from rich.console import Console
1011

@@ -332,7 +333,7 @@ async def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = Non
332333
raise RunError("Temporal agent requires a worker file path to be configured")
333334

334335
# Create environment for subprocesses
335-
agent_env = create_agent_environment(manifest)
336+
agent_env = create_agent_environment(manifest, manifest_dir=manifest_file.parent)
336337

337338
# Setup process manager
338339
process_manager = ProcessManager()
@@ -407,11 +408,22 @@ async def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = Non
407408

408409

409410

410-
def create_agent_environment(manifest: AgentManifest) -> dict[str, str]:
411+
def create_agent_environment(manifest: AgentManifest, manifest_dir: Path | None = None) -> dict[str, str]:
411412
"""Create environment variables for agent processes without modifying os.environ"""
412413
# Start with current environment
413414
env = dict(os.environ)
414415

416+
# Local development: load the .env next to manifest.yaml into BOTH the ACP and
417+
# worker processes (the docs promise this). Variables already set in the shell
418+
# win, matching python-dotenv's default. Without this, a value in .env only
419+
# reaches a process if some import happens to call load_dotenv() first.
420+
if manifest_dir is not None:
421+
env_file = Path(manifest_dir) / ".env"
422+
if env_file.is_file():
423+
for key, value in dotenv_values(env_file).items():
424+
if value is not None and key not in env:
425+
env[key] = value
426+
415427
agent_config = manifest.agent
416428

417429
# TODO: Combine this logic with the deploy_handlers so that we can reuse the env vars
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Tests for the environment `agentex agents run` hands to the ACP and worker processes."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
from agentex.lib.cli.commands.init import (
10+
TemplateType,
11+
get_project_context,
12+
create_project_structure,
13+
)
14+
from agentex.lib.cli.handlers.run_handlers import create_agent_environment
15+
from agentex.lib.sdk.config.agent_manifest import load_agent_manifest
16+
17+
18+
@pytest.fixture
19+
def project_dir(tmp_path: Path) -> Path:
20+
"""A scaffolded Sync ACP project, so the manifest is a real one."""
21+
answers = {
22+
"template_type": TemplateType.SYNC,
23+
"project_path": str(tmp_path),
24+
"agent_name": "env-agent",
25+
"agent_directory_name": "env-agent",
26+
"description": "An Agentex agent",
27+
"use_uv": True,
28+
}
29+
context = get_project_context(answers, tmp_path, Path("../../"))
30+
context["template_type"] = TemplateType.SYNC.value
31+
context["use_uv"] = True
32+
create_project_structure(tmp_path, context, TemplateType.SYNC, use_uv=True)
33+
return tmp_path / context["project_name"]
34+
35+
36+
def test_dotenv_next_to_manifest_is_loaded(project_dir: Path, monkeypatch: pytest.MonkeyPatch):
37+
"""Values in <manifest dir>/.env reach the subprocess environment."""
38+
(project_dir / ".env").write_text("FROM_DOTENV=1\nLITELLM_API_KEY=sk-test\n")
39+
monkeypatch.delenv("FROM_DOTENV", raising=False)
40+
monkeypatch.delenv("LITELLM_API_KEY", raising=False)
41+
manifest = load_agent_manifest(file_path=str(project_dir / "manifest.yaml"))
42+
43+
env = create_agent_environment(manifest, manifest_dir=project_dir)
44+
45+
assert env["FROM_DOTENV"] == "1"
46+
assert env["LITELLM_API_KEY"] == "sk-test"
47+
assert env["ENVIRONMENT"] == "development"
48+
49+
50+
def test_shell_variables_win_over_dotenv(project_dir: Path, monkeypatch: pytest.MonkeyPatch):
51+
"""A variable already exported in the shell is not overridden by .env."""
52+
(project_dir / ".env").write_text("PRESET=from-dotenv\n")
53+
monkeypatch.setenv("PRESET", "from-shell")
54+
manifest = load_agent_manifest(file_path=str(project_dir / "manifest.yaml"))
55+
56+
env = create_agent_environment(manifest, manifest_dir=project_dir)
57+
58+
assert env["PRESET"] == "from-shell"
59+
60+
61+
def test_missing_dotenv_and_no_manifest_dir_are_fine(project_dir: Path, monkeypatch: pytest.MonkeyPatch):
62+
"""No .env file, or no manifest_dir given, still builds a normal environment."""
63+
monkeypatch.delenv("FROM_DOTENV", raising=False)
64+
manifest = load_agent_manifest(file_path=str(project_dir / "manifest.yaml"))
65+
66+
assert "FROM_DOTENV" not in create_agent_environment(manifest, manifest_dir=project_dir)
67+
assert "FROM_DOTENV" not in create_agent_environment(manifest)

0 commit comments

Comments
 (0)