Skip to content

feat(agents): support Foundry ${{...}} server-side expressions in env expansion#8602

Merged
glharper merged 3 commits into
mainfrom
huimiu/foundry-config-unification
Jun 11, 2026
Merged

feat(agents): support Foundry ${{...}} server-side expressions in env expansion#8602
glharper merged 3 commits into
mainfrom
huimiu/foundry-config-unification

Conversation

@huimiu

@huimiu huimiu commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

First incremental PR toward unifying Foundry config in azure.yaml (design spec #8590). It introduces the shared env expander the unified Foundry experience needs: a single helper that resolves azd ${VAR} references while preserving Foundry's server-side ${{...}} expressions verbatim. This is the building block called out in design spec §2.5 ("Templating: ${VAR} and ${{...}}").

Framed as a feature because it adds support for a new templating syntax (${{...}}) the extension did not handle before. It also corrects a latent bug where a value mixing both syntaxes was returned unexpanded.

What it adds

  • New shared helper project.ExpandEnv(value, mapping) in the azure.ai.agents extension.
    • Resolves ${VAR} (including ${VAR:-default} and multiple expressions) via drone/envsubst.
    • Preserves every ${{...}} span verbatim for Foundry's server-side resolver.
    • Implementation: masks each ${{...}} span with a sentinel placeholder (no $/{/}, which envsubst never rewrites since it only expands the braced ${...} form), runs a single envsubst.Eval, then restores the spans. Masking instead of splitting preserves full ${VAR:-default} semantics even when the default value is itself a ${{...}} expression (for example ${MISSING:-${{event.body}}}).

Behavior change

Input Before After
${VAR} resolved resolved (unchanged)
${{x}} preserved as a side effect of a swallowed parse error (+ stderr warning) preserved cleanly
prefix ${VAR} ${{x}} returned unexpanded (${VAR} silently dropped) ${VAR} resolved, ${{x}} preserved
${MISSING:-${{x}}} returned unexpanded resolves to default ${{x}}

Affected areas

Extension-only (cli/azd/extensions/azure.ai.agents). The three existing ${VAR} expansion call sites now route through ExpandEnv:

  • internal/project/service_target_agent.go -> resolveEnvironmentVariables: agent env-var resolution during azd ai agent deploy (service target).
  • internal/cmd/listen.go -> resolveEnvValue (+ the recursive map/slice walk): env reference resolution in the provision/deploy lifecycle hooks (connection credentials, toolbox env, and similar).
  • internal/cmd/run.go -> resolveAgentDefinitionEnvVars: env-var resolution for azd ai agent run (local dev).

Not affected:

  • azd core: no changes. Core pkg/osutil/expandable_string.go is intentionally untouched (design spec §2.5 places this in the extension, not core).
  • Schema: none here. The host: microsoft.foundry schema is the separate PR feat(agents): add microsoft.foundry azure.yaml schema #8603.
  • Public API / gRPC contract: none. This is an internal helper plus call-site routing.

Blast radius is low: pure ${VAR} values behave exactly as before; the only change is that previously-broken mixed and ${VAR:-default} cases now expand correctly.

Tests

go build, go vet, gofmt, golangci-lint (0 issues), cspell, and the full extension test suite pass. templating_test.go covers plain/defaulted/missing vars, preserved ${{...}}, mixed, adjacent spans, a var nested inside a span, the ${MISSING:-${{event.body}}} default case, a leading-$ case, multiline spans, and malformed input.

Fixes #8609

Add a shared ExpandEnv helper in the azure.ai.agents extension that expands
azd ${VAR} references while leaving Foundry server-side ${{...}} expressions
untouched, and route the existing env-expansion call sites through it.

drone/envsubst cannot parse ${{...}} and returns the whole string unexpanded,
so any ${VAR} appearing alongside a ${{...}} span was silently dropped.
ExpandEnv splits on ${{...}} spans, expands only the gaps, and reattaches the
spans verbatim. This is the single shared expander Foundry fields will use as
part of unifying Foundry config in azure.yaml (design spec section 2.5).

Refactors resolveEnvironmentVariables (service_target_agent.go), resolveEnvValue
(listen.go), and resolveAgentDefinitionEnvVars (run.go) to use it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a shared templating helper in the azure.ai.agents extension to expand client-side ${VAR} references while preserving Foundry server-side ${{...}} expressions verbatim, and wires existing call sites through it to fix mixed-template expansion failures.

Changes:

  • Add project.ExpandEnv helper to expand ${VAR} while keeping ${{...}} spans untouched.
  • Add unit tests covering mixed ${VAR} + ${{...}} scenarios and invariants.
  • Route existing envsubst expansion call sites through project.ExpandEnv for consistent behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
cli/azd/extensions/azure.ai.agents/internal/project/templating.go Adds the shared ${VAR} expander that preserves Foundry ${{...}} spans.
cli/azd/extensions/azure.ai.agents/internal/project/templating_test.go Adds tests validating preservation and mixed expansion behaviors.
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go Switches environment variable resolution to use ExpandEnv.
cli/azd/extensions/azure.ai.agents/internal/cmd/run.go Switches agent definition env var resolution to use project.ExpandEnv.
cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go Switches toolbox/env resolution to use project.ExpandEnv and updates the function doc comment.

Comment thread cli/azd/extensions/azure.ai.agents/internal/project/templating.go
@github-actions github-actions Bot added the ext-agents azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes} extensions label Jun 11, 2026
@huimiu huimiu changed the title fix(agents): preserve Foundry ${{...}} through ${VAR} expansion feat: preserve Foundry ${{...}} through ${VAR} expansion Jun 11, 2026
huimiu added 2 commits June 11, 2026 13:23
- Document that a ${VAR} inside a ${{...}} span is preserved verbatim, and
  add a test asserting it.
- Drop the now-redundant error fallback in resolveAgentDefinitionEnvVars;
  ExpandEnv already returns the original value on error.
Switch ExpandEnv from splitting on ${{...}} spans to masking each span with
a sentinel placeholder, running a single envsubst pass, then restoring the
spans. The split approach failed when a ${{...}} expression was the default
of a ${VAR:-default} construct (e.g. ${MISSING:-${{event.body}}}), leaving an
unterminated "${MISSING:-" fragment that envsubst could not parse, so the
whole value was returned unexpanded. The sentinel contains no $, { or }, so
envsubst leaves it untouched even after a literal $. Adds tests for the
default-value cases and a leading-$ case.

Addresses review comment on internal/project/templating.go.
@huimiu huimiu changed the title feat: preserve Foundry ${{...}} through ${VAR} expansion feat(agents): support Foundry ${{...}} server-side expressions in env expansion Jun 11, 2026
@glharper

Copy link
Copy Markdown
Member

Minor (non-blocking)

  1. run.go silently swallows the error (resolved, _ := project.ExpandEnv(...)), whereas listen.go logs a stderr warning on failure. This matches the prior run.go behavior (it never warned), so it's not a regression — but routing both through the same helper is a natural moment to consider whether run.go should also surface a --debug-level log.Printf for diagnosability. Optional.
  2. Theoretical sentinel-collision gap (very low priority). The uniqueness guard (for strings.Contains(value, sentinel)) only checks the input value, not the expanded ${VAR} results. If an azd env var's value were literally azdFoundryTemplateSpan_0_ and appeared before the real placeholder, restoration could mis-target. This is effectively impossible in practice (no real env value contains the internal sentinel), so I'd only flag it as a comment-worthy caveat, not a fix.

Neither point blocks merging. The behavior-change table in the PR description accurately reflects the code (the previously-broken mixed and ${VAR:-default} cases now expand correctly), and the change stays correctly extension-only with no core/schema/gRPC impact.

@github-actions

Copy link
Copy Markdown

📋 Prioritization Note

Thanks for the contribution! The linked issue isn't in the current milestone yet.
Review may take a bit longer — reach out to @rajeshkamal5050 or @kristenwomack if you'd like to discuss prioritization.

@glharper glharper enabled auto-merge (squash) June 11, 2026 19:12
@glharper

Copy link
Copy Markdown
Member

/check-enforcer override

@glharper glharper merged commit 1a58de3 into main Jun 11, 2026
25 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-agents azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes} extensions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ext-agents] support Foundry ${{...}} server-side expressions in env expansion

4 participants