Skip to content

feat: add service-scoped env to Foundry extensions#9079

Open
huimiu wants to merge 17 commits into
mainfrom
hui/update-foundry-extension-env
Open

feat: add service-scoped env to Foundry extensions#9079
huimiu wants to merge 17 commits into
mainfrom
hui/update-foundry-extension-env

Conversation

@huimiu

@huimiu huimiu commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

  • Read service-scoped environment values from ServiceConfig.environment in the Foundry agent, connection, routine, and toolbox targets, with a fallback for older service definitions.
  • Write generated agent and resource variables through the standard service-level env object while preserving raw ${VAR} templates.
  • Update schemas, examples, minimum azd versions, and SDK dependencies for azd 1.27.1.

Follow-up to #8936.

Fixes: #9231

Why this change

azd core now expands a service's environment before it reaches the extension. The Foundry targets must consume those forwarded values so each service resolves its own variables correctly, while existing projects without a service-level environment continue to work.

Approach

Each target prefers the forwarded service environment and falls back to the active azd environment only for legacy service definitions. For azd ai agent run, explicit process and command-owned values have the highest priority, followed by service-level env. Both local and hosted agent paths add all service values independently of legacy agent.yaml entries and do not expand core-resolved literals again. Modern services receive only their declared values and required Foundry platform values; the complete active azd environment is injected only when a service has no service-level env.

Connection creation is owned by the microsoft.foundry provisioning provider rather than the connection deploy target. The provider therefore obtains core-resolved environments for every connection service and applies the matching service scope to target, credentials, and metadata in both greenfield and brownfield provisioning. Provider-wide azd and process values remain available only when a connection service has no env. This keeps the migration backward compatible, avoids cross-service variable leakage, and preserves Foundry server-side expressions.

Environment terminology

Term Meaning
Process environment Values inherited by a locally launched agent process. Process and command-owned values have the highest precedence.
Active azd environment Project-wide values stored for the selected azd environment. Modern services use these only for required platform values; services without env also use them as the legacy fallback.
Service env Raw templates authored under services.<name>.env in azure.yaml. Commands use the project config API when they must preserve these templates on disk.
Forwarded service environment The service-scoped, core-expanded literals received through ServiceConfig.Environment. Extensions consume these values without expanding them again.
Agent definition environment Deprecated inline environmentVariables or values from a legacy agent.yaml. Service env takes precedence when both are present.

Template syntax also has distinct owners: ${VAR} is resolved by azd, ${{...}} is resolved by Foundry, and $${{...}} preserves a Foundry expression while it passes through azd expansion.

Testing

Validated unit behavior and locally built extension bundles from the PR:

  • Added regression coverage for local-run precedence and isolation: process and command-owned values override service-level env, service-only and empty values reach disk-backed agents, unrelated azd values are excluded, and services without env retain the legacy azd environment fallback.
  • Added hosted-deploy coverage confirming service-only values reach the Foundry request, service values override legacy definitions, and core-resolved literals containing ${...} are not expanded again.
  • Added greenfield and brownfield connection coverage for service aliases, service precedence, cross-service isolation, and the legacy project/process environment fallback.
  • Confirmed the duplicated agent/projects synthesis implementations remain byte-identical.
  • Confirmed the affected agent, connection, routine, and toolbox extensions were installed from the PR bundles and their commands were discoverable.
  • azd ai agent init: migrated legacy environmentVariables to service-level env, removed the deprecated field, preserved ${VAR:-default}, and escaped Foundry templates as $${{...}}.
  • azd ai agent run with service-level env: resolved service values and the Foundry project endpoint for the child process while preserving the escaped template in azure.yaml.
  • azd ai agent run with legacy inline environmentVariables: resolved values through the active azd environment fallback.
  • azd ai agent optimize apply: preserved raw templates and scalar values while migrating with field-level updates.
  • azd deploy for a toolbox with service-level env: resolved the service-scoped toolbox endpoint.
  • azd deploy for a legacy toolbox without service-level env: resolved the toolbox endpoint through the active azd environment fallback.

@github-actions github-actions Bot added ext-agents azure.ai.agents extension ext-connections azure.ai.connections extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-routines azure.ai.routines extension ext-toolboxes azure.ai.toolboxes extension labels Jul 10, 2026
@huimiu
huimiu marked this pull request as ready for review July 10, 2026 12:16
Copilot AI review requested due to automatic review settings July 10, 2026 12:16

This comment was marked as outdated.

jongio

This comment was marked as outdated.

@huimiu huimiu changed the title fix: use core env in Foundry service targets refactor: use core env in Foundry service targets Jul 14, 2026
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 09:54
@huimiu
huimiu removed the request for review from Copilot July 14, 2026 09:54
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 14, 2026 09:59
@huimiu
huimiu removed the request for review from Copilot July 14, 2026 09:59
jongio

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 14, 2026 10:33

@trangevi trangevi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you please document where possible the terminology being used for the different environment locations? It's a bit difficult to keep track, and at least to me non-intuitive if I'm understanding correctly

return fmt.Errorf("adding %s service %q: %w", host, name, err)
}

if err := setServiceEnvironment(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we just put the environment on the serviceConfig object on line 281, instead of needing this separate method? I'm not sure I understand what that method is doing on top of the environment object.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ServiceConfig.Environment only carries values after azd expands them, so using it here would lose the original ${VAR} templates. The separate config call preserves the raw templates in azure.yaml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does "after azd expands them" mean? Does that mean if I have ${VAR}, azd core will replace that with the value of VAR in the azd environment?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ServiceConfig.Environment contains resolved values, not the original text from azure.yaml. For example, if azure.yaml contains ${API_KEY}, core sends the extension the actual API key.

AddService therefore treats these values as plain text and cannot use this field to create a new ${API_KEY} reference. SetServiceConfigSection writes the original text directly, which is why the separate call is needed

Comment thread cli/azd/extensions/azure.ai.agents/internal/cmd/run.go
env:
LOG_LEVEL: info
MODEL_ENDPOINT: ${{project.endpoint}}
MODEL_ENDPOINT: $${{project.endpoint}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are there two "$"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

$${{project.endpoint}} is the escaped form stored in azure.yaml. azd removes the extra $ and passes ${{project.endpoint}} through for the Foundry extension to resolve

"credentials": {
"type": "object",
"description": "Credentials. Values may contain ${VAR} (azd env, resolved client-side) or ${{...}} (Foundry server-side resolution, passed through untouched).",
"description": "Credentials. Values may contain ${VAR} declared in the service-level env object or ${{...}} for Foundry server-side resolution.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This line has ${VAR} and ${{VAR}}, I left a comment above regarding something that has $${VAR}}. Are all of these different things?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, they are different. ${VAR} is resolved from azd environment values, ${{...}} is resolved by Foundry, and $${{...}} escapes the Foundry expression while it passes through azd

@trangevi trangevi Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need all three? If ${VAR} is handled locally, and ${{VAR}} is handled remotely, why do we need the third? Where does the parsing happen, and can it be updated to properly filter on ${ and ${{ so that we only need two? I don't think that the string literal "{VAR}" would be considered a valid env var key anyway, so I don't thing we need to be worried about properly being able to resolve that to something concrete

@huimiu huimiu Jul 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

There are only two kinds of expressions:

  • ${VAR} is resolved by azd.
  • ${{...}} is resolved by Foundry.

$${{...}} isn't a third kind — it's just the escaped form of a Foundry expression inside a service env value. Core expands env before handing it to the extension: it understands ${VAR}, but a raw ${{...}} makes core's envsubst error out, so the extra $ tells core to pass the expression through unchanged.

You're right that this is the awkward part — migration rewrites ${{...}} to $${{...}} when it moves a value into env, so the input and output azure.yaml don't match. The clean fix is to teach core's env expansion to preserve ${{...}} (the same masking we already do for Foundry config fields like target/credentials), which gets us back to two syntaxes everywhere. That changes env handling for every azd service, so I want to check the compatibility impact and add coverage first — I'd rather do that than keep the escape as a user-facing workaround. So I'll keep it out of this PR and track the core fix separately.

Comment thread eng/scripts/Test-BicepLint.ps1 Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 04:33

This comment was marked as outdated.

jongio

This comment was marked as outdated.

@Azure Azure deleted a comment from github-actions Bot Jul 23, 2026
jongio

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 23, 2026 10:11

This comment was marked as outdated.

jongio

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 23, 2026 10:41
@github-actions

This comment was marked as off-topic.

jongio

This comment was marked as outdated.

This comment was marked as outdated.

jongio

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings July 23, 2026 13:15

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

Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.

Comment on lines +998 to +1004
hasServiceEnvironment := false
if resp, envErr := azdClient.Project().GetServiceConfigValue(
ctx,
&azdext.GetServiceConfigValueRequest{ServiceName: svc.Name, Path: "env"},
); envErr == nil {
hasServiceEnvironment = resp.GetFound()
}
Comment on lines +170 to +173
p.serviceEnvironments, err = p.projectServiceEnvironments(ctx)
if err != nil {
return err
}

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One issue on the latest commit (ce4d70a):

MEDIUM

  • agents helpers.go: the env-detection RPC failure falls open to the full-environment fallback, unlike the routines and toolboxes targets in this same commit which fail closed.

ctx,
&azdext.GetServiceConfigValueRequest{ServiceName: svc.Name, Path: "env"},
); envErr == nil {
hasServiceEnvironment = resp.GetFound()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

On a GetServiceConfigValue error this leaves hasServiceEnvironment false, so mergeAgentRunEnvironment takes the legacy branch and injects the full azd environment, the exact leak this commit is closing. The routines and toolboxes targets in this same change fail closed by returning the error from serviceEnvDeclared. Do the same here and only trust Found after a successful call.

jongio

This comment was marked as outdated.

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One issue from my earlier reviews is still open on this HEAD, already flagged inline at helpers.go:1003.

In azure.ai.agents/internal/cmd/helpers.go, resolveServiceRunContext swallows the GetServiceConfigValue error and leaves hasServiceEnvironment false. On a transient error that pushes mergeAgentRunEnvironment down the !hasServiceEnvironment branch and injects the full azd environment, the exact leak this change closes everywhere else. The routines and toolboxes targets fail closed by returning the error from serviceEnvDeclared. The agents path should do the same and only trust Found after a successful call.

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed at ce4d70a. The empty-env isolation is handled the right way in routines, toolboxes, and both synthesizers, but two things on the agents path are still open.

The fail-open in azure.ai.agents/internal/cmd/helpers.go (line 1003) is unchanged: resolveServiceRunContext drops the GetServiceConfigValue error and leaves HasServiceEnvironment false, so mergeAgentRunEnvironment falls back to injecting the full azd environment on a transient RPC failure, which re-opens the exact leak this commit closes. routines and toolboxes fail closed by returning the error from serviceEnvDeclared; please do the same here and only trust Found after a successful call.

The new TestMergeAgentRunEnvironment case passes hasServiceEnvironment in directly, so it never exercises resolveServiceRunContext where the error is swallowed. Stubbing the client to error from GetServiceConfigValue would cover the leak path and lock the fix in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/extensions Extensions (general) ext-agents azure.ai.agents extension ext-connections azure.ai.connections extension ext-foundry azure.ai.{agents,connections,inspector,projects,routines,skills,toolboxes}, microsoft.foundry ext-projects azure.ai.projects extension ext-routines azure.ai.routines extension ext-toolboxes azure.ai.toolboxes extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Foundry extensions to service-level env

5 participants