-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Fix: Environment Variable Access in Service Mode
Problem
When running Ash as a service (via ash service start), environment variables like ANTHROPIC_API_KEY are not accessible because:
- Service processes don't inherit shell environment variables (e.g., from
.bashrc/.zshrc) - Current service files (systemd/launchd) only pass
ASH_HOME, not API keys - This affects both Linux (systemd) and macOS (launchd)
Current Behavior
Direct run (uv run ash serve):
- ✅ Inherits all shell environment variables
- ✅ API keys work from
export ANTHROPIC_API_KEY=...
Service mode (ash service start):
- ❌ No access to shell environment variables
- ❌ API keys must be in config.toml (bad practice)
- Only has:
ASH_HOME=/Users/mark/.ash
Solution: .env File Support
Implement standard .env file loading at ~/.ash/.env (or ASH_HOME/.env) with OS-specific service integration.
Common Pattern Used By Services
| OS | Standard Approach |
|---|---|
| Linux | Systemd's EnvironmentFile= directive to load .env files |
| macOS | Read .env file and populate launchd's EnvironmentVariables dict |
| Generic | Load .env file in Python before service startup |
This is the industry standard approach used by:
- Docker Compose (
.envfiles) - systemd services (
EnvironmentFile=) - Node.js (
dotenvpackage) - Python frameworks (django-environ, python-dotenv)
Implementation Plan
1. Add .env File Loader
File: src/ash/config/env.py (new file)
Create a simple .env parser:
- Read
ASH_HOME/.envif it exists - Parse
KEY=VALUElines (ignore comments, blank lines) - Support quoted values:
KEY="value with spaces" - Load into
os.environbefore config loading - Handle missing file gracefully (optional)
2. Integrate into Config Loading
File: src/ash/config/loader.py:47
Modify load_config() to:
- Determine
ASH_HOME(from env var or default) - Load
.envfile fromASH_HOME/.envif exists - Then proceed with existing config loading (which reads from
os.environ)
This ensures .env values are available when _resolve_env_secrets() runs.
3. Update Systemd Service (Linux)
File: src/ash/service/backends/systemd.py:166-179
Modify _write_unit_file() to add EnvironmentFile= directive:
[Service]
Type=simple
ExecStart={ash_exec} serve
Restart=on-failure
RestartSec=5
Environment=ASH_HOME={ash_home}
EnvironmentFile=-{ash_home}/.envThe - prefix makes the file optional (service won't fail if missing).
4. Update Launchd Service (macOS)
File: src/ash/service/backends/launchd.py:148-171
Modify _write_plist() to:
- Check if
ASH_HOME/.envexists - If exists, parse it and populate
EnvironmentVariablesdict - Always include
ASH_HOME(existing behavior)
env_vars = {"ASH_HOME": str(ash_home)}
# Load .env file if exists
env_file = ash_home / ".env"
if env_file.exists():
env_vars.update(_load_env_file(env_file))
plist = {
"Label": SERVICE_LABEL,
"ProgramArguments": [ash_path] + ash_args,
"EnvironmentVariables": env_vars,
...
}5. Update Generic Backend
File: src/ash/service/backends/generic.py
The generic backend inherits the parent process's environment, but for consistency:
- Document that
.envloading happens in Python (via config loader) - No changes needed (Python-level loading sufficient)
6. Add .env Template Generation
Optional enhancement: Add a helper command to generate template:
ash config init-env # Creates ~/.ash/.env.exampleExample .env.example:
# Ash Environment Variables
# Copy to .env and fill in your values
# Claude API
ANTHROPIC_API_KEY=sk-ant-...
# OpenAI API
OPENAI_API_KEY=sk-...
# Telegram Bot
TELEGRAM_BOT_TOKEN=...
# Brave Search
BRAVE_SEARCH_API_KEY=...
# Sentry (optional)
# SENTRY_DSN=...Files to Modify
Core Changes
- src/ash/config/env.py (new) - .env file parser
- src/ash/config/loader.py:47 - Load .env before config
- src/ash/service/backends/systemd.py:166-179 - Add EnvironmentFile directive
- src/ash/service/backends/launchd.py:148-171 - Parse .env into plist
Optional Enhancement
- src/ash/cli/commands/config.py (new or extend existing) -
ash config init-envcommand
Documentation
- README.md or docs/configuration.md - Document .env file usage
.env File Location
Path: ~/.ash/.env (or $ASH_HOME/.env)
Security:
- Mode 600 (read/write owner only)
- Add to
.gitignoreif workspace is version controlled - Document in security best practices
Format:
ANTHROPIC_API_KEY=sk-ant-api03-xxx
OPENAI_API_KEY=sk-xxx
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...Verification
Test Direct Run
# Create .env file
echo "ANTHROPIC_API_KEY=sk-test-123" > ~/.ash/.env
chmod 600 ~/.ash/.env
# Remove from shell
unset ANTHROPIC_API_KEY
# Run directly - should work
uv run ash serveTest Service Mode (macOS)
# Stop existing service
ash service stop
# Reinstall to regenerate plist with new EnvironmentVariables
ash service uninstall
ash service install
# Start service
ash service start
# Verify it can access API key
ash service logs -fTest Service Mode (Linux)
# Stop existing service
ash service stop
# Regenerate unit file
ash service uninstall
ash service install
# Start service
ash service start
# Verify via journalctl
journalctl --user -u ash -fTest Missing .env (Should Work)
# Remove .env
rm ~/.ash/.env
# Should still work (falls back to config.toml or env vars)
ash service startBenefits
- Security: Secrets separate from config.toml
- Standard: .env is industry-standard format
- Cross-platform: Works on Linux, macOS, and Windows
- Optional: Service works without .env file
- Developer-friendly: Same format as shell exports
- Service-compatible: Native OS integration (systemd/launchd)
Alternative Considered: Keychain Integration
Could integrate with:
- macOS Keychain (
securitycommand) - Linux gnome-keyring/kwallet
Pros: Better security, encrypted storage
Cons: More complex, platform-specific, harder to automate
Decision: Start with .env approach (simpler, more universal). Keychain can be added later as an enhancement.