Package Environment Aggregation
Context
autograph packages can require operator-provided configuration (secrets, preferences, endpoints). For example:
autograph.pkg.agentneeds LLM credentials (provider, token, models)- A future
autograph.pkg.pythonmight need a Python version or registry URL - A future
autograph.pkg.databasemight need connection strings
The system must support multiple environments (dev, staging, prod) with clear separation of concerns and a predictable workflow.
Requirements
- Discoverability - After installing deps, the operator must know exactly what variables need to be filled
- Aggregation - Package env templates must be collected generically (no hardcoding)
- Idempotency - Re-running install must NOT overwrite values already filled by the operator
- Fail-fast - Build must fail clearly if required env vars are missing
- Multi-environment - Support multiple env profiles (dev, staging, prod) cleanly
- Separation - Clear distinction between: project meta-config, package templates, operator-filled values, and generated output
- Genericity - Works for any package without modifying the engine
- Freshness - The system detects when source configs changed and rebuild is needed
- Safety - Destructive operations use backup/restore to prevent accidental data loss
Related decisions
- ADR package-context-resolution: structural paths from root
env:, secrets from dotenv - ADR package-interface-contract: packages declare interface via
augr.pkg.yml - ADR lifecycle-tasks: defines overall lifecycle stages
Options considered
Option A - Single env/.env with appended sections
All package templates appended to one file with section headers.
- Pro: Single file, simple dotenv
- Con: Grows large. Hard to attribute ownership. Idempotency complex (detect existing sections). No multi-env support.
Option B - Per-environment directory with env.d/ drop-ins + build step (chosen)
Layered structure: meta-config at root, per-env directories with drop-in files, aggregated into a single generated file at build time.
- Pro: Multi-env native. Clear ownership (one file per package). Idempotent (existing files untouched). Build step validates completeness. Drop-in pattern well-known (
conf.d/). - Con: More complex directory structure. Requires build step. Operator must know workflow.
Option C - Per-package env files loaded directly by compose
Separate files, compose uses env_file: to load them.
- Pro: No build step.
- Con: Task
dotenv:doesn't support globs. Compose must list each file. No multi-env without duplication.
Key constraint: Task dotenv limitation
Task's dotenv: directive does not support template resolution in paths (./env/{{.VAR}}/... fails) and does not support globs. This forces us to use fixed-path generated files rather than per-env paths in the dotenv declaration. The ideal architecture would use ./env/<ENV>/.env.packages directly, but Task's limitation requires an intermediate fixed-name file at env/ root.
Decision
Option B - Per-environment directory with env.d/ drop-ins + build step.
Consequences
Complete lifecycle (symmetric)
┌─────────────────────────────────────────────────────────────────────┐
│ CONSTRUCT │ DECONSTRUCT │
├─────────────────────────────────────┼───────────────────────────────┤
│ │ │
│ setup │ teardown │
│ ├── git submodule init │ ├── deinit submodules │
│ └── create env/ from env.template/ │ └── remove env/ entirely │
│ │ │
│ augr:install │ augr:uninstall │
│ ├── west resolve packages │ ├── remove resolved deps │
│ ├── scan package env templates │ └── backup + remove env.d/ │
│ └── copy templates → env.d/ │ │
│ │ │
│ ⟶ MANUAL: fill env/<ENV>/env.d/ │ │
│ │ │
│ augr:build │ augr:clean │
│ ├── aggregate env.d/ → .env.project│ └── remove .env.project │
│ ├── validate completeness │ │
│ └── (operator runs augr:up next) │ │
│ │ │
│ augr:up │ augr:down │
│ ├── build Docker images │ └── stop + remove containers │
│ └── start containers │ │
│ │ │
│ (app interaction - future ADR) │ │
│ │ │
├─────────────────────────────────────┼───────────────────────────────┤
│ SAFETY │
├─────────────────────────────────────────────────────────────────────┤
│ augr:env:backup - move env/<ENV>/ to env/<ENV>.backup/ │
│ augr:env:restore - restore env/<ENV>.backup/ to env/<ENV>/ │
│ augr:env:purge - permanently delete backups │
│ teardown - full reset (removes env/ entirely) │
└─────────────────────────────────────────────────────────────────────┘
Note on augr:build → augr:up separation: Task's dotenv: loads files at parse time (before any task runs). After augr:build generates .env.project, the operator must invoke augr:up as a separate command so that Task re-parses and loads the newly generated file. This two-step is a consequence of Task's architecture, not a design choice.
Directory structure
env/
├── .env.augr # Engine meta-config (created by setup)
│ # Contains: AUGR_ENV, AUGR_CONTAINER_CLI
│ # Fixed name, loaded first by dotenv
│
├── .env.project # Build output (created by augr:build)
│ # Aggregation of all env.d/ package configs
│ # Fixed name, loaded second by dotenv
│
├── dev/ # Active environment (AUGR_ENV=dev)
│ ├── env.d/ # Package configs (operator fills these)
│ │ ├── agent.env # From pkg.agent/env.template/.env
│ │ ├── database.env # From pkg.database/env.template/.env
│ │ └── ...
│ ├── state/ # Runtime state (volumes, caches, logs)
│ │ └── ...
│ └── shared/ # Shared resources (build caches, etc.)
│ └── ...
│
├── dev.backup/ # Backup of dev/ (created by augr:env:backup)
│
├── staging/ # Another environment (same structure)
│ └── ...
│
└── .gitignore
Naming rationale:
.env.augr- the autograph engine configuration (meta, always present).env.project- the project's full runtime configuration (generated from packages)
dotenv loading (root Taskfile)
dotenv:
- './env/.env.augr' # Engine meta-config (always exists after setup)
- './env/.env.project' # Aggregated package config (exists after augr:build)
Both are fixed paths - no template resolution needed. dotenv: silently ignores missing files, so the Taskfile works at every lifecycle stage.
env/.env.augr - Engine meta-configuration
Created by setup from workspace/env.template/.env.augr. The template is versioned in the repo. The actual file in env/ is NOT committed (env/ is gitignored entirely). The operator regenerates it by re-running setup.
# Active environment profile
AUGR_ENV=dev
# Container runtime
AUGR_CONTAINER_CLI=docker
env/.env.project - Build output
Created by augr:build. Contains all variables from env/<ENV>/env.d/*.env concatenated.
Ephemeral - regenerated on every build. Never edited manually.
env/ gitignore strategy
# env/.gitignore
# Everything in env/ is gitignored.
# The directory structure is recreated by setup from env.template/.
*
!.gitignore
The env/ directory is never committed. Its structure is reproducible:
setupcreates it fromworkspace/env.template/(which IS versioned)augr:installpopulatesenv.d/from package templatesaugr:buildgenerates.env.project
This ensures no accidental secret commits and full reproducibility.
Package env template convention
Each package that needs operator configuration:
autograph.pkg.<name>/
├── augr.pkg.yml # Declares: env_template: env.template/.env
└── env.template/
└── .env # Documented template with empty values
Variable naming:
AUGR_ADE_*- agent/LLM config (pkg.agent)AUGR_PKG_<NAME>_*- package-specific
augr:install behavior
- Resolve packages (west init + update)
- Read
AUGR_ENVfromenv/.env.augr - Create
env/<ENV>/env.d/if not exists - For each resolved package with
env_templateinaugr.pkg.yml:- Target:
env/<ENV>/env.d/<package-name>.env - If target does NOT exist: copy template, display "New: please fill in"
- If target exists: compare with template, warn if new vars were added upstream
- Target:
- Display summary of files needing operator attention
augr:build behavior
Freshness checks (implemented as internal: true tasks for reuse):
env/.env.projectexistsenv/.env.projectis newer than allenv/<ENV>/env.d/*.envenv/.env.projectis newer thanenv/.env.augr
If stale or missing:
- Read
AUGR_ENVfromenv/.env.augr - Concatenate all
env/<ENV>/env.d/*.env - Write to
env/.env.project - Validate: scan for empty required values → fail with message listing what's missing
Note: Infrastructure paths (AUGR_PATH_WORKSPACE, etc.) are NOT in .env.project. They are resolved by the root Taskfile's vars: section (using ROOT_DIR from git rev-parse) and exported via env:. This is documented in ADR package-context-resolution.
Implementation note: Freshness checks and regeneration should be internal: true tasks for reuse. The entire env lifecycle could live in a dedicated Taskfile.env.yaml, testable independently.
augr:up behavior
After augr:build has generated .env.project:
- Task re-parses (new invocation) → dotenv loads
.env.project - All package vars + infra paths are in process env
- Build Docker images (compose reads
${VAR:?}from process env) - Start containers
Backup / Restore / Purge
Safe-delete protocol for environment data:
augr:env:backup:
- Moves
env/<ENV>/toenv/<ENV>.backup/ - If
env/<ENV>.backup/already exists: fail (must purge old backup first) - Confirms action to operator
augr:env:restore:
- If
env/<ENV>/already exists: fail (must uninstall current env first) - Moves
env/<ENV>.backup/back toenv/<ENV>/ - Removes
.env.project(stale after restore - forces rebuild)
augr:env:purge:
- Permanently deletes
env/<ENV>.backup/(or all*.backup/dirs) - Requires confirmation (destructive)
teardown:
- Removes entire
env/directory (including backups) - Deinits submodules
- Nuclear option - returns to post-clone state
Compose env loading
Compose reads from process env (inherited from Task's dotenv). No env_file: in compose. Compose uses ${VAR:?} for fail-fast on required vars.
Scope exclusion
App-level interaction with containers (serve, shell, dev servers) is out of scope. Conventions for running services will be defined in a future ADR.
Implementation note
The env lifecycle (install/build/backup/restore/purge) has enough complexity to warrant its own dedicated Taskfile (e.g., Taskfile.env.yaml) that can be tested independently from the rest of the system.