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
jongio

This comment was marked as outdated.

@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?

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

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 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 note on the automated review comments about the len == 0 fallback in the Foundry targets (run.go, the routine and toolbox service targets, and the projects synthesizer). They're framed as cross-service secret leakage, but azdEnvironment and the service-scoped environment are both values from the same azd environment, so this is scoping within a single project, not a cross-tenant boundary. Nothing leaks between users or subscriptions.

The one real nuance: an explicit env: {} serializes the same as an omitted env, so it can't opt out of the compatibility fallback. If honoring an explicit empty scope ever matters, gate the fallback on whether env was declared in the raw service config rather than on map length. Low priority and fine to defer, since the fallback is intentional back-compat.

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

The new commit closes the explicit env: {} isolation gap in the synthesizers and the routines/toolboxes targets, and the added tests cover the empty-declared case.

One issue is still open on this HEAD (already flagged inline on 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 sends mergeAgentRunEnvironment down the legacy branch and injects the full azd environment, the exact leak this commit 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.

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.

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