feat: add service-scoped env to Foundry extensions#9079
Conversation
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
Co-authored-by: huimiu <107838226+huimiu@users.noreply.github.com>
This comment was marked as off-topic.
This comment was marked as off-topic.
| hasServiceEnvironment := false | ||
| if resp, envErr := azdClient.Project().GetServiceConfigValue( | ||
| ctx, | ||
| &azdext.GetServiceConfigValueRequest{ServiceName: svc.Name, Path: "env"}, | ||
| ); envErr == nil { | ||
| hasServiceEnvironment = resp.GetFound() | ||
| } |
| p.serviceEnvironments, err = p.projectServiceEnvironments(ctx) | ||
| if err != nil { | ||
| return err | ||
| } |
| ctx, | ||
| &azdext.GetServiceConfigValueRequest{ServiceName: svc.Name, Path: "env"}, | ||
| ); envErr == nil { | ||
| hasServiceEnvironment = resp.GetFound() |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
jongio
left a comment
There was a problem hiding this comment.
Re-reviewed the new commit. Still open from my earlier pass:
- cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go:
GetServiceConfigValueerror still falls back to the full azd environment instead of failing closed like the routines and toolboxes targets do in this same commit.
🤖 agent jongio
jongio
left a comment
There was a problem hiding this comment.
Re-reviewed the latest commit (honor explicit empty service env as an isolated scope). The routines, toolboxes, and synthesizer paths handle the isolated scope, but the agents run path still has the gap I flagged inline.
In resolveServiceRunContext (agents/internal/cmd/helpers.go), the GetServiceConfigValue error is swallowed, so hasServiceEnvironment stays false whenever the probe fails. mergeAgentRunEnvironment then takes the !hasServiceEnvironment branch and injects the full azd environment, which is the exact leak this commit closes for a service that declared an isolated (empty) env. The routines and toolboxes targets fail closed by returning the error from serviceEnvDeclared; agents should match that and only trust Found after a successful call.
Not blocking anything new, just making sure this carries over before the change lands.
jongio
left a comment
There was a problem hiding this comment.
The empty-env isolation in the latest commit works: env: {} now reads as a declared scope through GetServiceConfigValue's Found in the run, routine, and toolbox targets, and through the raw-node check in the synthesizers, so a service that opts into an empty scope no longer inherits the full azd environment. I traced it against core LoadConfig/config.Get, which keeps env: {} as a present key.
One item is still open from this push, already flagged inline on helpers.go: resolveServiceRunContext swallows a GetServiceConfigValue error and leaves hasServiceEnvironment false, so mergeAgentRunEnvironment takes the legacy branch and injects the full azd environment, the same leak this commit closes. The routine and toolbox targets fail closed by returning the error from serviceEnvDeclared; helpers.go should match that and only trust Found after a successful call.
Unrelated to this commit: the on-disk (ejected) provisioning path in foundry_provisioning_provider.go skips the synthesizer and resolves parameters with the project-wide resolveEnv rather than the service-scoped map, so isolation doesn't reach eject-to-Bicep. Is that intended, or should the on-disk path carry the same scoping?
Summary
ServiceConfig.environmentin the Foundry agent, connection, routine, and toolbox targets, with a fallback for older service definitions.envobject while preserving raw${VAR}templates.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-levelenv. Both local and hosted agent paths add all service values independently of legacyagent.yamlentries 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-levelenv.Connection creation is owned by the
microsoft.foundryprovisioning provider rather than the connection deploy target. The provider therefore obtains core-resolved environments for every connection service and applies the matching service scope totarget,credentials, andmetadatain both greenfield and brownfield provisioning. Provider-wide azd and process values remain available only when a connection service has noenv. This keeps the migration backward compatible, avoids cross-service variable leakage, and preserves Foundry server-side expressions.Environment terminology
envalso use them as the legacy fallback.envservices.<name>.envinazure.yaml. Commands use the project config API when they must preserve these templates on disk.ServiceConfig.Environment. Extensions consume these values without expanding them again.environmentVariablesor values from a legacyagent.yaml. Serviceenvtakes 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:
env, service-only and empty values reach disk-backed agents, unrelated azd values are excluded, and services withoutenvretain the legacy azd environment fallback.${...}are not expanded again.azd ai agent init: migrated legacyenvironmentVariablesto service-levelenv, removed the deprecated field, preserved${VAR:-default}, and escaped Foundry templates as$${{...}}.azd ai agent runwith service-levelenv: resolved service values and the Foundry project endpoint for the child process while preserving the escaped template inazure.yaml.azd ai agent runwith legacy inlineenvironmentVariables: 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 deployfor a toolbox with service-levelenv: resolved the service-scoped toolbox endpoint.azd deployfor a legacy toolbox without service-levelenv: resolved the toolbox endpoint through the active azd environment fallback.