Skip to content

Prompt request: Environment Variable Access in Service Mode #4

@pors

Description

@pors

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:

  1. Service processes don't inherit shell environment variables (e.g., from .bashrc/.zshrc)
  2. Current service files (systemd/launchd) only pass ASH_HOME, not API keys
  3. 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 (.env files)
  • systemd services (EnvironmentFile=)
  • Node.js (dotenv package)
  • 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/.env if it exists
  • Parse KEY=VALUE lines (ignore comments, blank lines)
  • Support quoted values: KEY="value with spaces"
  • Load into os.environ before config loading
  • Handle missing file gracefully (optional)

2. Integrate into Config Loading

File: src/ash/config/loader.py:47

Modify load_config() to:

  1. Determine ASH_HOME (from env var or default)
  2. Load .env file from ASH_HOME/.env if exists
  3. 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}/.env

The - 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:

  1. Check if ASH_HOME/.env exists
  2. If exists, parse it and populate EnvironmentVariables dict
  3. 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 .env loading 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.example

Example .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

  1. src/ash/config/env.py (new) - .env file parser
  2. src/ash/config/loader.py:47 - Load .env before config
  3. src/ash/service/backends/systemd.py:166-179 - Add EnvironmentFile directive
  4. src/ash/service/backends/launchd.py:148-171 - Parse .env into plist

Optional Enhancement

  1. src/ash/cli/commands/config.py (new or extend existing) - ash config init-env command

Documentation

  1. 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 .gitignore if 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 serve

Test 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 -f

Test 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 -f

Test Missing .env (Should Work)

# Remove .env
rm ~/.ash/.env

# Should still work (falls back to config.toml or env vars)
ash service start

Benefits

  1. Security: Secrets separate from config.toml
  2. Standard: .env is industry-standard format
  3. Cross-platform: Works on Linux, macOS, and Windows
  4. Optional: Service works without .env file
  5. Developer-friendly: Same format as shell exports
  6. Service-compatible: Native OS integration (systemd/launchd)

Alternative Considered: Keychain Integration

Could integrate with:

  • macOS Keychain (security command)
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions