Skip to content

fix: centralize environment variable expansion at config boundary#2710

Draft
yunus25jmi1 wants to merge 3 commits into
docker:mainfrom
yunus25jmi1:bug/fix-incompatible-env-expansion
Draft

fix: centralize environment variable expansion at config boundary#2710
yunus25jmi1 wants to merge 3 commits into
docker:mainfrom
yunus25jmi1:bug/fix-incompatible-env-expansion

Conversation

@yunus25jmi1
Copy link
Copy Markdown
Contributor

Root Cause

Fragmented expansion logic across the codebase caused incompatibility between syntaxes ($VAR vs ${env.VAR}) and silent failures. Path fields only supported POSIX style, while command fields used a JS-based expander that often ignored standard environment variables or failed silently.

Solution: Layered Expansion Architecture

Implemented a robust two-layer expansion pipeline at the configuration boundary:

  1. Layer 1: Universal Expansion (Config Loading Phase)

    • Normalizes all syntaxes ($VAR, ${VAR}, ${env.VAR}) into a standard form.
    • Uses a reflection-based walker (pkg/config/expand.go) to visit and expand every exported string field, slice, and map in the configuration immediately after unmarshaling.
    • Provides explicit error reporting via ErrMissingVars instead of silent partial expansion.
  2. Layer 2: Dynamic JS Expressions (Runtime Phase)

    • Simplified the JS expander (pkg/js/expand.go) by removing redundant environment handling.
    • JS engine now focuses strictly on dynamic logic (e.g., ${tool(...)}), as environment variables are guaranteed to be resolved by Layer 1.

Key Changes

  • pkg/environment/expand.go: Added normalization and centralized expansion logic using os.Expand.
  • pkg/config/expand.go: New reflection-based config walker to ensure 100% field coverage.
  • pkg/config/config.go: Integrated expansion into the main Load pipeline.
  • pkg/js/expand.go: Decoupled JS engine from environment resolution.
  • pkg/config/sources.go: Updated Source interface to carry EnvProvider context.

Impact

  • Full support for $VAR, ${VAR}, and ${env.VAR} in all string-based config fields (working_dir, commands, instruction, etc).
  • Elimination of silent expansion failures.
  • Cleaner separation of concerns between configuration loading and execution runtime.

Fixes #2615

@yunus25jmi1 yunus25jmi1 requested a review from a team as a code owner May 8, 2026 07:21
Implement Layered Expansion: Layer 1 (Universal POSIX/JS syntax normalization) and Layer 2 (JS expressions). Add reflection-based Config walker to expand all fields during loading. Normalize ${env.VAR} to ${VAR} across all configuration fields. Improve error reporting for missing environment variables. Fixes docker#2615
@yunus25jmi1 yunus25jmi1 force-pushed the bug/fix-incompatible-env-expansion branch from a09ecd7 to 3b35cf0 Compare May 8, 2026 07:25
@dgageot
Copy link
Copy Markdown
Member

dgageot commented May 8, 2026

/review

@docker docker deleted a comment from docker-agent May 8, 2026
@dgageot
Copy link
Copy Markdown
Member

dgageot commented May 8, 2026

/review

@yunus25jmi1
Copy link
Copy Markdown
Contributor Author

@dgageot I am currently working on this PR. I am facing some GO environment system-level configuration problems. I will make a comment when my PR is in good shape.

@docker-agent
Copy link
Copy Markdown

PR Review Failed — The review agent encountered an error and could not complete the review. View logs.

- Rename ErrMissingVars to ErrMissingVarsError to conform to XxxError format
- Update for loops to use Go 1.22+ integer range syntax
- Remove unused testEnvProvider type from js/expand_test.go
- Remove unused context import from js/expand_test.go
- Update test files to use new ErrMissingVarsError type
- Add EnvProvider method to mockSource in tests
@yunus25jmi1
Copy link
Copy Markdown
Contributor Author

@dgageot The PR is now in good shape. All lint issues have been resolved and the checks are passing. Ready for review!

@aheritier aheritier added area/config For configuration parsing, YAML, environment variables priority:medium labels May 11, 2026
@aheritier
Copy link
Copy Markdown
Contributor

Triage note: Community PR (contributor) with 5 comments and active discussion. The proposal — a reflection-based config walker that expands all string fields at load time — is a significant change to the config loading pipeline. Key concerns raised in comments:

  1. Overreach: A reflection walker that expands all string fields could inadvertently expand literal $ values in user-defined instructions, prompt text, or tool output patterns — any field not intended to support expansion.
  2. Interface change: Source carrying EnvProvider is a breaking change to the Source interface used by multiple call sites.
  3. JS engine coupling: Decoupling the JS expander from env vars may break existing YAML configs that rely on ${env.VAR} being handled at the JS layer.

Current status: not yet approved; needs maintainer sign-off on the design direction before proceeding. The original issue (#2615) may have a narrower fix available.

@yunus25jmi1
Copy link
Copy Markdown
Contributor Author

Hey @dgageot @aheritier — before continuing, I'd like to align on the direction given the triage concerns.

Three questions:

  1. Field overreach: The reflection walker expands all exported string fields. Is blanket expansion the right default, or should we scope it to specific fields?

  2. Interface change: Adding EnvProvider() to Source is breaking. Would passing env context as a parameter to Load() be preferable?

  3. JS engine simplification: Removing env handling from the JS layer may break existing configs relying on ${env.VAR} at the JS layer. Any concerns?

Alternative path: The original issue #2615 suggested replacing os.ExpandEnv in pkg/path/expand.go with the JS expander — a narrower fix targeting just working_dir/path fields. Would the team prefer this as an initial step?

Looking for direction before investing further effort.

@aheritier aheritier added status/needs-design Requires architectural discussion or design review and removed priority:medium labels May 12, 2026
@aheritier aheritier requested review from dgageot and rumpl May 17, 2026 19:01
dgageot pushed a commit that referenced this pull request May 21, 2026
Two incompatible expansion syntaxes coexist in agent YAML
(see #2615): JS template literals (`${env.X}`) for prompt-like
fields, and shell-style (`$VAR`/`${VAR}`/`~`) for path
fields. When the wrong syntax is used in a field, expansion
silently no-ops and the literal string is passed through.

Emit a slog warning at config-load time when:

  - a JS-expanded field (description, welcome_message, instruction,
    commands, headers, env values) contains a shell-style
    `${UPPER_CASE}` reference and no `${env.X}` is present;
  - a path field (toolset working_dir, path) contains `${env.X}`.

Warnings only — runtime behavior is unchanged. This makes the
silent no-op visible while a proper unification is designed
(#2710).
@aheritier aheritier added the kind/fix PR fixes a bug (maps to fix: commit prefix) label May 22, 2026
@aheritier aheritier marked this pull request as draft May 24, 2026 11:30
@aheritier aheritier added status/needs-rebase PR has merge conflicts or is out of date with main and removed status/needs-rebase PR has merge conflicts or is out of date with main labels May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config For configuration parsing, YAML, environment variables kind/fix PR fixes a bug (maps to fix: commit prefix) status/needs-design Requires architectural discussion or design review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two incompatible env-variable expansion syntaxes coexist silently across agent yaml fields

4 participants