From 27b4862728b80085409e9ccd74be99acd5b8c7b8 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 2 Nov 2026 16:38:03 -0300 Subject: [PATCH 1/5] feat: provision managed Knowledge Base Toolbox and optional user delegation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- config/aifoundry/toolbox.py | 92 +++++++++++++ config/deployment/hosted_access.py | 70 ++++++++-- config/deployment/topology.py | 5 +- config/requirements.txt | 1 + config/retrieval/settings.py | 23 ++-- config/search/setup.py | 3 +- .../tests/test_ingestion_permissions.py | 37 +++++ docs/adr/ADR-0038-optional-user-delegation.md | 122 +++++++++++++++++ infra/bicep/main.bicep | 35 ++++- infra/bicep/main.parameters.json | 1 + .../connection-knowledge-base.bicep | 35 +++++ infra/shared/roles.json | 4 + infra/terraform/main.compute.tf | 3 +- infra/terraform/main.data.tf | 1 + infra/terraform/main.foundry.tf | 36 ++--- infra/terraform/main.identity.tf | 20 +-- main.parameters.json | 1 + scripts/postProvision.ps1 | 2 + scripts/postProvision.sh | 3 +- tests/test_knowledge_toolbox.py | 126 ++++++++++++++++++ tests/test_native_knowledge_connection.py | 64 +++++++++ tests/test_retrieval_provisioning.py | 104 +++++++++++++-- tests/test_topology_cli.py | 16 +++ 23 files changed, 744 insertions(+), 60 deletions(-) create mode 100644 config/aifoundry/toolbox.py create mode 100644 docs/adr/ADR-0038-optional-user-delegation.md create mode 100644 infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep create mode 100644 tests/test_knowledge_toolbox.py create mode 100644 tests/test_native_knowledge_connection.py diff --git a/config/aifoundry/toolbox.py b/config/aifoundry/toolbox.py new file mode 100644 index 00000000..4446c4b3 --- /dev/null +++ b/config/aifoundry/toolbox.py @@ -0,0 +1,92 @@ +"""Provision the managed Foundry IQ Toolbox after Search configures its KB.""" + +import re +from collections.abc import Mapping +from dataclasses import dataclass + +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import MCPToolboxTool +from azure.identity import ( + AzureCliCredential, + ChainedTokenCredential, + ManagedIdentityCredential, +) + +from .appconfig import AppConfigClient + +SETTINGS = ( + "RETRIEVAL_BACKEND", + "AI_FOUNDRY_PROJECT_ENDPOINT", "AI_FOUNDRY_PROJECT_RESOURCE_ID", "TOOLBOX_ENDPOINT", + "KNOWLEDGE_BASE_NAME", "KNOWLEDGE_BASE_ENDPOINT", "KNOWLEDGE_BASE_CONNECTION_ID", "FOUNDRY_IQ_API_VERSION", +) + + +@dataclass(frozen=True) +class KnowledgeToolbox: + project_endpoint: str + name: str + server_url: str + connection_name: str + + @classmethod + def read(cls, values: Mapping[str, str]) -> "KnowledgeToolbox | None": + if values.get("RETRIEVAL_BACKEND") != "foundry_iq": + return None + name = values.get("KNOWLEDGE_BASE_NAME", "") + project = values.get("AI_FOUNDRY_PROJECT_ENDPOINT", "").rstrip("/") + project_id = values.get("AI_FOUNDRY_PROJECT_RESOURCE_ID", "") + search = values.get("KNOWLEDGE_BASE_ENDPOINT", "").rstrip("/") + connection = values.get("KNOWLEDGE_BASE_CONNECTION_ID", "") + api_version = values.get("FOUNDRY_IQ_API_VERSION", "") + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}(?:-preview)?", api_version): + raise ValueError("Managed Toolbox requires FOUNDRY_IQ_API_VERSION from provisioning.") + if not re.fullmatch(r"[A-Za-z0-9_-]+", name): + raise ValueError("Managed Toolbox requires a valid KNOWLEDGE_BASE_NAME.") + if not re.fullmatch(r"https://[a-z0-9-]+\.services\.ai\.azure\.com/api/projects/[A-Za-z0-9_.-]+", project): + raise ValueError("Managed Toolbox requires AI_FOUNDRY_PROJECT_ENDPOINT.") + if not re.fullmatch(r"https://[a-z0-9-]+\.search\.windows\.net", search): + raise ValueError("Managed Toolbox requires the provisioned KNOWLEDGE_BASE_ENDPOINT.") + project_match = re.fullmatch( + r"/subscriptions/[0-9a-fA-F-]{36}/resourceGroups/[^/]+/providers/" + r"Microsoft\.CognitiveServices/accounts/([^/]+)/projects/([^/]+)", project_id, + ) + if ( + project_match is None + or project != f"https://{project_match[1]}.services.ai.azure.com/api/projects/{project_match[2]}" + or not connection.startswith(project_id + "/connections/") + or not re.fullmatch(r"[A-Za-z0-9_.-]+", connection.rsplit("/", 1)[-1]) + ): + raise ValueError("Managed Toolbox connection must belong to its provisioned Foundry project.") + if values.get("TOOLBOX_ENDPOINT") != f"{project}/toolboxes/{name}/mcp?api-version=v1": + raise ValueError("TOOLBOX_ENDPOINT must match the provisioned project and knowledge base.") + return cls( + project, name, f"{search}/knowledgebases/{name}/mcp?api-version={api_version}", + connection.rsplit("/", 1)[-1], + ) + + def provision(self, client: AIProjectClient) -> None: + version = client.toolboxes.create_version( + self.name, description="Foundry IQ knowledge base behind one managed endpoint", + tools=[MCPToolboxTool( + name="knowledge_base", server_label="knowledge_base", + server_url=self.server_url, project_connection_id=self.connection_name, + allowed_tools=["knowledge_base_retrieve"], require_approval="never", + )], + ) + client.toolboxes.update(self.name, default_version=version.version) + + +def main() -> None: + config = AppConfigClient() + binding = KnowledgeToolbox.read({key: config.get(key) for key in SETTINGS if config.get(key) is not None}) + if binding is None: + return + with ( + ChainedTokenCredential(AzureCliCredential(), ManagedIdentityCredential()) as credential, + AIProjectClient(endpoint=binding.project_endpoint, credential=credential) as client, + ): + binding.provision(client) + + +if __name__ == "__main__": + main() diff --git a/config/deployment/hosted_access.py b/config/deployment/hosted_access.py index be0f96c8..50e9227a 100644 --- a/config/deployment/hosted_access.py +++ b/config/deployment/hosted_access.py @@ -1,10 +1,10 @@ """Plan (default) or apply the hosted instance's minimum runtime RBAC. Only configuration metadata, Foundry agent metadata and ARM are read. No secret -values, inference requests or continuity activation are used. Configured Search -retrieval adds an exact-service Search Index Data Reader data-plane query grant -for transport. It is not a Blob grant or permission to omit delegated source -authorization; no end-user Search RBAC or impersonation grant is added here. +values, inference requests or continuity activation are used. Managed KB access +adds Foundry User at the exact project; standalone Search uses an exact-service +Search Index Data Reader grant. Neither grants Blob access or permission to +omit delegated source authorization. """ from __future__ import annotations @@ -27,7 +27,9 @@ from config.deployment.hosted import FOUNDRY_TOKEN_AUDIENCE from config.deployment.composition import HOSTED_AGENT_NAME_DEFAULT, resolve_mode from config.deployment.resource_groups import resource_group_for -from config.retrieval.settings import SEARCH_BACKENDS, validate_auth_mode +from config.retrieval.settings import ( + SEARCH_BACKENDS, hosted_auth_mode, validate_auth_mode, +) from util.azure_cli import resolve_az_command @@ -74,6 +76,7 @@ class Role(str, Enum): SECRET = "4633458b-17de-408a-b874-0445c86b69e6" MODEL = "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" SEARCH = "1407120a-92aa-4202-b7e9-c0e197c71c8f" + TOOLBOX = "53ca6127-db72-4b80-b1b0-d745d6d5456d" GUID = r"[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}" @@ -84,6 +87,7 @@ class Role(str, Enum): Role.MODEL: rf"Microsoft\.CognitiveServices/accounts/{NAME}", Role.SECRET: r"Microsoft\.KeyVault/vaults/[A-Za-z0-9-]+/secrets/[A-Za-z0-9-]{1,127}", Role.SEARCH: rf"Microsoft\.Search/searchServices/{NAME}", + Role.TOOLBOX: rf"Microsoft\.CognitiveServices/accounts/{NAME}/projects/{NAME}", } PROJECT_PATH = rf"Microsoft\.CognitiveServices/accounts/({NAME})/projects/({NAME})" KV_REFERENCE = "application/vnd.microsoft.appconfig.keyvaultref+json" @@ -194,10 +198,11 @@ class AccessPlan: skipped: bool = False foundry_resource_group: str = "" search_transport: bool = False + toolbox: bool = False def validate(self) -> None: if self.skipped: - if self.grants or self.principal_id or self.search_transport: + if self.grants or self.principal_id or self.search_transport or self.toolbox: raise AccessError("Inapplicable Hosted bootstrap cannot contain grants.") return _guid(self.subscription) @@ -208,6 +213,10 @@ def validate(self) -> None: raise AccessError("Invalid Search transport configuration status.") if self.search_transport: expected.add(Role.SEARCH) + if type(self.toolbox) is not bool: + raise AccessError("Invalid managed Toolbox configuration status.") + if self.toolbox: + expected.add(Role.TOOLBOX) if self.audit == "configured": expected.add(Role.SECRET) elif self.audit != "not-configured": @@ -217,7 +226,7 @@ def validate(self) -> None: for grant in self.grants: group = ( self.foundry_resource_group or self.resource_group - if grant.role in {Role.MODEL, Role.SEARCH} else self.resource_group + if grant.role in {Role.MODEL, Role.SEARCH, Role.TOOLBOX} else self.resource_group ) _bound_scope(grant.scope, RESOURCE_PATHS[grant.role], self.subscription, group) @@ -452,7 +461,8 @@ def discover(environment: Mapping[str, str], *, run: RunAzure = run_az) -> Acces sub, group, principal, name, version, tuple(grants), "configured" if audit else "not-configured", foundry_resource_group=foundry_group, - search_transport=search is not None, + search_transport=search is not None and search.role is Role.SEARCH, + toolbox=search is not None and search.role is Role.TOOLBOX, ) plan.validate() return plan @@ -476,6 +486,17 @@ def _search_transport_grant( backend = _consistent(environment, ("RETRIEVAL_BACKEND",), backend_row.get("value")) if backend not in SEARCH_BACKENDS: raise AccessError("Configured retrieval backend has no supported Search transport contract.") + auth_row = _setting(run, config_endpoint, "HOSTED_AGENT_AUTH_MODE") + if auth_row is None and environment.get("HOSTED_AGENT_AUTH_MODE") == "service_identity": + raise AccessError("Publish HOSTED_AGENT_AUTH_MODE (agent-lz) before planning service_identity access.") + configured_auth = _consistent( + environment, ("HOSTED_AGENT_AUTH_MODE",), + auth_row.get("value") if auth_row else "user_delegated", + ) + try: + hosted_auth_mode({"HOSTED_AGENT_AUTH_MODE": configured_auth}) + except ValueError as error: + raise AccessError(str(error)) from None mode_row = _setting(run, config_endpoint, "RETRIEVAL_AUTH_MODE") if mode_row is not None: try: @@ -502,9 +523,42 @@ def _search_transport_grant( or not _same(resource.get("type"), "Microsoft.Search/searchServices") ): raise AccessError("Search ARM resource identity/type mismatch.") + if backend == "foundry_iq": + return _toolbox_grant( + environment, run=run, config_endpoint=config_endpoint, + subscription=subscription, foundry_group=foundry_group, + ) return Grant(Role.SEARCH, scope) +def _toolbox_grant( + environment: Mapping[str, str], *, run: RunAzure, config_endpoint: str, + subscription: str, foundry_group: str, +) -> Grant: + scope = _consistent( + environment, ("AZURE_AI_PROJECT_RESOURCE_ID",), + _config_value(run, config_endpoint, "AI_FOUNDRY_PROJECT_RESOURCE_ID"), + ) + _bound_scope(scope, RESOURCE_PATHS[Role.TOOLBOX], subscription, foundry_group) + match = _scope_match(scope, PROJECT_PATH) + endpoint = _consistent( + environment, ("AZURE_AI_PROJECT_ENDPOINT",), + _config_value(run, config_endpoint, "AI_FOUNDRY_PROJECT_ENDPOINT"), + ).rstrip("/") + if endpoint != f"https://{match[3].lower()}.services.ai.azure.com/api/projects/{match[4]}": + raise AccessError("Managed Toolbox project endpoint/resource scope mismatch.") + name = _config_value(run, config_endpoint, "KNOWLEDGE_BASE_NAME") + if not re.fullmatch(r"[A-Za-z0-9_-]+", name): + raise AccessError("Managed Toolbox requires a valid knowledge base name.") + toolbox = _consistent( + environment, ("TOOLBOX_ENDPOINT",), _config_value(run, config_endpoint, "TOOLBOX_ENDPOINT"), + ) + if toolbox != f"{endpoint}/toolboxes/{name}/mcp?api-version=v1": + raise AccessError("Managed Toolbox endpoint must match the configured project and knowledge base.") + # The hosted instance calls Toolbox; its RemoteTool project MI calls Search. + return Grant(Role.TOOLBOX, scope) + + def _has_exact(run: RunAzure, plan: AccessPlan, grant: Grant) -> bool: rows = run([ "role", "assignment", "list", "--assignee-object-id", plan.principal_id, diff --git a/config/deployment/topology.py b/config/deployment/topology.py index 3388c283..d602c42a 100644 --- a/config/deployment/topology.py +++ b/config/deployment/topology.py @@ -12,11 +12,14 @@ describe_application, materialized_settings, validate_hosted_prerequisites, ) from config.deployment.native import NativeApplication, OrchestrationHost, resolve_application +from config.retrieval.settings import public_settings def resolve_environment_plan(environment: Mapping[str, str]) -> NativeApplication: """Explicit current-product intent only; no persisted-state classification.""" - return resolve_application(environment) + app = resolve_application(environment) + public_settings(environment, hosted=app.host is OrchestrationHost.HOSTED) + return app def main() -> int: diff --git a/config/requirements.txt b/config/requirements.txt index 05ed4be4..cc4ef6d3 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -10,6 +10,7 @@ azure-mgmt-appcontainers==5.1.0 # aifoundry azure-mgmt-cognitiveservices==13.6.0 +azure-ai-projects==2.7.0 # search diff --git a/config/retrieval/settings.py b/config/retrieval/settings.py index d37c071b..986f6748 100644 --- a/config/retrieval/settings.py +++ b/config/retrieval/settings.py @@ -1,4 +1,4 @@ -"""Public settings for the implemented native Search delegation route. +"""Public settings for the native Search route and its structural auth profiles. An empty mode denotes no Hosted route (the Container Apps host). It is not a service-identity fallback. Source credentials remain request-local in @@ -7,7 +7,7 @@ from __future__ import annotations -from typing import Mapping +from typing import Any, Mapping from uuid import UUID from config.deployment.composition import is_truthy @@ -25,6 +25,13 @@ ) +def hosted_auth_mode(settings: Mapping[str, Any]) -> str: + value = settings.get("HOSTED_AGENT_AUTH_MODE") or "user_delegated" + if not isinstance(value, str) or value.strip() not in {"user_delegated", "service_identity"}: + raise ValueError("HOSTED_AGENT_AUTH_MODE must be user_delegated or service_identity.") + return value.strip() + + def source_public_settings(environment: Mapping[str, str]) -> dict[str, str]: """Validate/publish only explicitly supplied nonsecret source identity IDs. @@ -64,19 +71,17 @@ def public_settings( ) -> dict[str, str]: """Resolve the route before discovery/publication, preserving request auth.""" validate_auth_mode(environment) + mode = hosted_auth_mode(environment) if not hosted: + if mode == "service_identity": + raise ValueError("HOSTED_AGENT_AUTH_MODE=service_identity requires ORCHESTRATION_HOST=hosted.") return {RETRIEVAL_AUTH_MODE: ""} - if (environment.get("HOSTED_AGENT_AUTH_MODE") or "user_delegated").strip() != "user_delegated": - raise ValueError( - "Native protected retrieval requires HOSTED_AGENT_AUTH_MODE=user_delegated; " - "service credentials authenticate Search transport only." - ) if is_truthy(environment.get("ALLOW_ANONYMOUS")): raise ValueError("Native protected retrieval requires ALLOW_ANONYMOUS=false.") backend = (environment.get("RETRIEVAL_BACKEND") or "foundry_iq").strip() if backend not in SEARCH_BACKENDS: raise ValueError("RETRIEVAL_BACKEND must be foundry_iq or ai_search for native Search retrieval.") - if backend == "foundry_iq": + if backend == "foundry_iq" and mode == "user_delegated": forward = environment.get("FOUNDRY_IQ_FORWARD_SOURCE_AUTH") if forward is not None and not is_truthy(forward): raise ValueError( @@ -87,5 +92,5 @@ def public_settings( # never change source flags, invent app audiences or remove a KB member here. return { RETRIEVAL_AUTH_MODE: NATIVE_SEARCH, - "HOSTED_AGENT_AUTH_MODE": "user_delegated", + "HOSTED_AGENT_AUTH_MODE": mode, } diff --git a/config/search/setup.py b/config/search/setup.py index 493f2725..5e0e7b84 100644 --- a/config/search/setup.py +++ b/config/search/setup.py @@ -1061,7 +1061,8 @@ def execute_setup(defs: Optional[dict], context: dict): ) # Step 1: Clean up knowledge base resources in correct order (KB -> KS) - cleanup_knowledge_resources(defs, context, cred, search_endpoint) + if context.get("HOSTED_AGENT_AUTH_MODE", "user_delegated") != "service_identity": + cleanup_knowledge_resources(defs, context, cred, search_endpoint) # Step 2: Reconcile the remaining standard Search resources. provision_datasources(defs, context, cred, ds_to_indexers, search_endpoint, api_version) diff --git a/config/search/tests/test_ingestion_permissions.py b/config/search/tests/test_ingestion_permissions.py index 3049690e..e2ccad17 100644 --- a/config/search/tests/test_ingestion_permissions.py +++ b/config/search/tests/test_ingestion_permissions.py @@ -34,6 +34,43 @@ def normalized_settings(overrides=None): class IngestionPermissionTests(unittest.TestCase): + def service_definitions(self): + context = { + **BASE_CONTEXT, **normalized_settings({PERMISSION_KEY: []}), + "HOSTED_AGENT_AUTH_MODE": "service_identity", + } + definitions = render_json_template("search.j2", { + **context, "EMBEDDING_MODEL_INFO": {}, "GPT_MODEL_INFO": {}, + }) + return definitions, context + + def test_service_profile_preserves_explicit_shared_source_and_ingestion_identity(self): + definitions, context = self.service_definitions() + original = copy.deepcopy(definitions) + setup.validate_foundry_iq_settings(context) + self.assertEqual(definitions, original) + self.assertEqual(definitions["knowledgeSources"][0]["azureBlobParameters"][ + "ingestionParameters"]["ingestionPermissionOptions"], []) + + def test_auth_mode_does_not_change_source_definitions_or_permission_metadata(self): + for source in ({}, {PERMISSION_KEY: []}, {"FOUNDRY_IQ_IS_ADLS_GEN2": True}, + {"FOUNDRY_IQ_PATTERN": "searchIndex", "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND": "searchIndex"}): + context = { + **BASE_CONTEXT, **normalized_settings(source), + "EMBEDDING_MODEL_INFO": {}, "GPT_MODEL_INFO": {}, + } + delegated = render_json_template("search.j2", {**context, "HOSTED_AGENT_AUTH_MODE": "user_delegated"}) + service = render_json_template("search.j2", {**context, "HOSTED_AGENT_AUTH_MODE": "service_identity"}) + self.assertEqual(delegated, service) + + def test_missing_and_null_service_permission_policy_remain_protected(self): + for permissions in (None, ["rbacScope"]): + _, context = self.service_definitions() + context[PERMISSION_KEY] = permissions + setup.normalize_foundry_iq_settings(context) + self.assertEqual(context[PERMISSION_KEY], ["rbacScope"]) + setup.validate_foundry_iq_settings(context) + def test_omitted_and_null_retain_protected_default(self): for overrides in ({}, {PERMISSION_KEY: None}): with self.subTest(overrides=overrides): diff --git a/docs/adr/ADR-0038-optional-user-delegation.md b/docs/adr/ADR-0038-optional-user-delegation.md new file mode 100644 index 00000000..88e05781 --- /dev/null +++ b/docs/adr/ADR-0038-optional-user-delegation.md @@ -0,0 +1,122 @@ +# ADR-0038: Explicit service identity without delegated fallback + +**Status:** Accepted for bounded implementation; not deployed
+**Date:** 2026-11-02
+**Owners:** Agent Landing Zone maintainers + +## Context + +The approved product plan preserves authenticated user-delegated access and +adds authenticated service access governed by each source's authentication +and scoped permissions, without a Blob-only source allowlist. Priorities are fail-closed source +authorization, one operator choice, native SDK ownership of state, provider +parity, and reversible coordinated deployment. No tenant-specific configuration +or consent workaround belongs in the product. + +## Alternatives considered + +- **Keep delegation mandatory:** preserves protected-source behavior, but cannot + serve the approved common-corpus use case. +- **Remove guards or retry OBO failures with service credentials:** simpler, but + cannot establish source audience and can expose protected content. Rejected. +- **Explicit structural profile with source-owned authorization:** selected. + Requires coordinated platform, UI, runtime and connection updates, but has no + implicit fallback or additional authentication flags. + +## Decision + +Reuse `HOSTED_AGENT_AUTH_MODE=user_delegated` (default) or `service_identity`. +Both retain mandatory login and Entra admission. Both use the existing +`RETRIEVAL_AUTH_MODE=native_search` publication under `agent-lz`. + +Service mode does not restrict source families or capability flags and does not +require `FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS=[]`. Missing/null remains protected +`["rbacScope"]`. Sources authorize their configured service caller, and operations +requiring unavailable user delegation fail explicitly at invocation. +Never rewrite permission metadata or remove knowledge-base members to satisfy +mode selection. Source configuration and schema validation remain independent. + +Provision the KB connection as a real project child, category `RemoteTool`, +`authType=ProjectManagedIdentity`, audience `https://search.azure.com/`, targeting +`{Search endpoint}/knowledgebases/{KB}/mcp?api-version={foundryIqApiVersion}`. +The separate direct Search connection remains `CognitiveSearch`. The project +MI's existing exact-service Search Index Data Reader grant authenticates this +connection. Ingestion retains its own Search identity and Blob grant. +After Search succeeds, both post-provision hooks run +`config.aifoundry.toolbox`. The official `azure-ai-projects==2.7.0` data-plane +SDK creates a Toolbox version with `MCPToolboxTool` named/server-labeled +`knowledge_base`, the project connection name, the configured KB MCP endpoint, +`allowed_tools=["knowledge_base_retrieve"]`, and approval `never`, then promotes +that version. No unsupported ARM Toolbox resource is invented. + +Both providers publish internal `TOOLBOX_ENDPOINT` under `agent-lz`: +`{AI_FOUNDRY_PROJECT_ENDPOINT}/toolboxes/{KNOWLEDGE_BASE_NAME}/mcp?api-version=v1`. +It is a structural runtime binding, not another operator selector or root ARM +output. Native MAF uses the pinned official `FoundryToolbox`, authenticating as +the workload to `https://ai.azure.com/.default` and forwarding the genuine +**current** platform call ID. Foundry resolves the user and, for the KB target, +sets `x-ms-query-source-authorization` from its managed user delegation. +No local Search token acquisition, raw model MCP assertion, or parallel +fallback remains in this KB path. Preserve the existing AzureRbac Foundry +audience; no blueprint migration or arbitrary `UserEntraToken` is introduced. + +The hosted instance receives the shared `AzureAIUser` (Foundry User) role at +the exact project. It receives no redundant Search grant for managed KB calls. +Standalone Search retains its exact-service grant. +The UI receives only Foundry Agent Consumer at project scope through the shared +role catalog, covering its conversational/workflow endpoint calls. + +## Consequences + +Service source access is shared by admitted application users, not public. +Mode and native connection-binding changes require redeployment, not hot reload. +The official Responses host still requires genuine gateway-supplied caller +partition and call-ID context for service callers. Never synthesize user IDs, +inject trusted gateway headers, or replace SDK checkpoints/storage. +Missing context fails before Toolbox HTTP. Managed tool authorization failures +abort the response rather than becoming model-visible error text. Delegated +consent produces official `oauth_consent_request` output with `incomplete` +status; service consent is a failed dependency, never user fallback. The UI +suppresses consent links and offers actionable denial/manual retry, not an +interactive consent/resumption implementation. + +## Adoption and migration + +Review the actual corpus and existing resource bindings first; choosing the +mode does not prove an existing source is shared. Configure/validate Search +sources and the project MCP connection, then deploy the compatible native UI +and runtime together with the selected publication. A failed configuration +stage must block component deployment. Existing components fail closed on a +live profile change; profile publication alone is not activation readiness. + +The old account-level CognitiveSearch KB connection is not the native MCP +connection. Review its migration separately: incremental Bicep may leave the +old resource; Terraform removes the old managed account connection while +creating the project child. Do not delete sources or rewrite ACLs. +Rollback restores the coordinated delegated publication/components and reviewed +connection binding; it does not convert an approved common corpus back into a +protected source or restore permission metadata automatically. + +## Compliance verification + +Focused offline publication, pre-provision CLI, source-type independence, +RBAC and native connection contract tests cover defaults and negative cases. +Real project-SDK HTTP mocks verify Toolbox registration/promotion. Real pinned +MAF, Toolbox MCP transport and Responses-host tests verify current two-user +call IDs, both profiles, successful KB function invocation, consent and hard +failures, and absence of bearer/source tokens in model inputs or checkpoints. +Compile the affected Bicep connection and root, check Terraform interfaces and +formatting, and run native SDK tests in the orchestrator. Local evidence is not +deployed provider, gateway, downstream user-token exchange, retrieval or +tenant-consent-policy proof. Those boundaries require external validation. + +## Documentation impact + +The coordinating change owns central authentication, Foundry IQ, settings, +deployment, migration and rollback guidance in `Azure/AI-Landing-Zones`. +This record is an internal decision, not a duplicated product guide. + +## Review trigger + +Review before any live adoption, when SDK gateway/storage requirements change, +or before introducing hybrid authorization. diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 8483a7f8..3c871f37 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -2689,14 +2689,16 @@ module aiFoundryConnectionSearch 'modules/ai-foundry/connection-ai-search.bicep' // Dedicated AI Search connection for Foundry IQ knowledge bases. This is kept // separate from SEARCH_CONNECTION_ID so application search and knowledge-base // lifecycle can evolve independently. -module aiFoundryKnowledgeBaseSearchConnection 'modules/ai-foundry/connection-ai-search.bicep' = if (deployAiFoundry && deploySearchService && retrievalBackend == 'foundry_iq') { +module aiFoundryKnowledgeBaseSearchConnection 'modules/ai-foundry/connection-knowledge-base.bicep' = if (deployAiFoundry && deployAfProject && deploySearchService && retrievalBackend == 'foundry_iq') { scope: resourceGroup(_foundryResourceGroupName) name: 'connection-foundry-iq-search-${resourceToken}' params: { aiFoundryName: aiFoundry!.outputs.aiServicesName aiProjectName: aiFoundry!.outputs.aiProjectName - connectedResourceName: searchService!.outputs.name - aiSearchConnectionName: knowledgeBaseConnectionName + knowledgeBaseConnectionName: knowledgeBaseConnectionName + searchEndpoint: searchService!.outputs.endpoint + knowledgeBaseName: knowledgeBaseName + knowledgeApiVersion: foundryIqApiVersion } dependsOn: [ aiFoundry! @@ -2938,7 +2940,7 @@ module durableTaskRoles 'modules/security/resource-role-assignment.bicep' = [ { roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', const.roles.DurableTaskDataContributor.guid) resourceId: durableTask!.outputs.backend.taskHubResourceId - principalId: _useUAI ? containerAppsUAI[i].outputs.principalId : containerApps[i].outputs.systemAssignedMIPrincipalId! + principalId: _useUAI ? containerAppsUAI[i]!.outputs.principalId : containerApps[i]!.outputs.systemAssignedMIPrincipalId! principalType: 'ServicePrincipal' } ] @@ -2983,8 +2985,9 @@ var _containerRuntimeEnv = [ { name: 'ENABLE_AGENTIC_RETRIEVAL', value: toLower(string(enableAgenticRetrieval)) } { name: 'RETRIEVAL_BACKEND', value: retrievalBackend } { name: 'KNOWLEDGE_BASE_NAME', value: retrievalBackend == 'foundry_iq' ? knowledgeBaseName : '' } + { name: 'TOOLBOX_ENDPOINT', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? '${aiFoundryProjectEndpoint}/toolboxes/${knowledgeBaseName}/mcp?api-version=v1' : '' } { name: 'KNOWLEDGE_BASE_ENDPOINT', value: retrievalBackend == 'foundry_iq' ? 'https://${resourceNames.searchServiceName}.search.windows.net' : '' } - { name: 'KNOWLEDGE_BASE_CONNECTION_ID', value: retrievalBackend == 'foundry_iq' ? '/subscriptions/${subscription().subscriptionId}/resourceGroups/${_foundryResourceGroupName}/providers/Microsoft.CognitiveServices/accounts/${resourceNames.aiFoundryAccountName}/projects/${resourceNames.aiFoundryProjectName}/connections/${knowledgeBaseConnectionName}' : '' } + { name: 'KNOWLEDGE_BASE_CONNECTION_ID', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? '/subscriptions/${subscription().subscriptionId}/resourceGroups/${_foundryResourceGroupName}/providers/Microsoft.CognitiveServices/accounts/${resourceNames.aiFoundryAccountName}/projects/${resourceNames.aiFoundryProjectName}/connections/${knowledgeBaseConnectionName}' : '' } { name: 'FOUNDRY_IQ_API_VERSION', value: retrievalBackend == 'foundry_iq' ? foundryIqApiVersion : '' } { name: 'FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN', value: retrievalBackend == 'foundry_iq' ? foundryIqKnowledgeRetrievalBillingPlan : '' } { name: 'FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME', value: retrievalBackend == 'foundry_iq' ? foundryIqKnowledgeSourceName : '' } @@ -3691,6 +3694,23 @@ module assignCosmosDBCosmosDbBuiltInDataContributorExecutor 'modules/security/co // (guid(principalId, roleDefinitionId, resourceId)), so consolidating the // deployment invocations does not change the deployed role assignment set. // Cosmos DB data-plane assignments stay in their dedicated module below. +module assignAgentConsumerRoles 'modules/security/resource-role-assignment.bicep' = [ + for (app, i) in containerAppsList: if (_deployContainerApps && deployAiFoundry && deployAfProject && contains(app.roles, const.roles.FoundryAgentConsumer.key)) { + scope: resourceGroup(_foundryResourceGroupName) + name: 'agent-consumer-${app.service_name}-${resourceToken}' + params: { + roleAssignments: [ + { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', const.roles.FoundryAgentConsumer.guid) + principalId: _useUAI ? containerAppsUAI[i].outputs.principalId : containerApps[i].outputs.systemAssignedMIPrincipalId! + resourceId: aiFoundryProjectResourceId + principalType: 'ServicePrincipal' + } + ] + } + } +] + module assignContainerAppRoles 'modules/security/resource-role-assignment.bicep' = [ for (app, i) in containerAppsList: if (_deployContainerApps && ((deployKeyVault && contains(app.roles, const.roles.KeyVaultSecretsUser.key)) || (deployAppConfig && _runtimeConfigIsAppConfig && contains(app.roles, const.roles.AppConfigurationDataReader.key)) || (deployAiFoundry && contains(app.roles, const.roles.CognitiveServicesUser.key)) || (deployAiFoundry && contains(app.roles, const.roles.CognitiveServicesOpenAIUser.key)) || (deploySpeechService && contains(app.roles, const.roles.CognitiveServicesUser.key)) || (deployContainerRegistry && contains(app.roles, const.roles.AcrPull.key)) || (deploySearchService && contains(app.roles, const.roles.SearchIndexDataReader.key)) || (deploySearchService && contains(app.roles, const.roles.SearchIndexDataContributor.key)) || (deployStorageAccount && contains(app.roles, const.roles.StorageBlobDataContributor.key)) || (deployStorageAccount && contains(app.roles, const.roles.StorageBlobDataReader.key)) || (deployStorageAccount && contains(app.roles, const.roles.StorageBlobDelegator.key)))) { name: 'assignContainerAppRoles-${app.service_name}' @@ -4139,7 +4159,7 @@ module appConfigPopulate 'modules/app-configuration/app-configuration.bicep' = i { name: 'SEARCH_SERVICE_RESOURCE_ID', value: deploySearchService ? searchService.outputs.resourceId : '', label: appConfigLabel, contentType: 'text/plain' } { name: 'SEARCH_SHARED_PRIVATE_LINKS', value: string(searchSharedPrivateLinkReadinessManifest), label: appConfigLabel, contentType: 'application/json' } #disable-next-line BCP318 - { name: 'KNOWLEDGE_BASE_CONNECTION_ID', value: retrievalBackend == 'foundry_iq' && deploySearchService && deployAiFoundry ? aiFoundryKnowledgeBaseSearchConnection.outputs.searchConnectionId : '', label: appConfigLabel, contentType: 'text/plain' } + { name: 'KNOWLEDGE_BASE_CONNECTION_ID', value: retrievalBackend == 'foundry_iq' && deploySearchService && deployAiFoundry && deployAfProject ? aiFoundryKnowledgeBaseSearchConnection.outputs.connectionId : '', label: appConfigLabel, contentType: 'text/plain' } #disable-next-line BCP318 { name: 'AZURE_SPEECH_RESOURCE_ID', value: deploySpeechService ? speechService.outputs.resourceId : '', label: appConfigLabel, contentType: 'text/plain' } @@ -4190,6 +4210,7 @@ module appConfigPopulate 'modules/app-configuration/app-configuration.bicep' = i #disable-next-line BCP318 { name: 'SEARCH_CONNECTION_ID', value: deploySearchService && deployAiFoundry ? aiFoundryConnectionSearch.outputs.searchConnectionId : '', label: appConfigLabel, contentType: 'text/plain' } { name: 'KNOWLEDGE_BASE_NAME', value: retrievalBackend == 'foundry_iq' ? knowledgeBaseName : '', label: appConfigLabel, contentType: 'text/plain' } + { name: 'TOOLBOX_ENDPOINT', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? '${aiFoundryProjectEndpoint}/toolboxes/${knowledgeBaseName}/mcp?api-version=v1' : '', label: appConfigLabel, contentType: 'text/plain' } #disable-next-line BCP318 { name: 'BING_CONNECTION_ID', value: deployGroundingWithBing && deployAiFoundry ? bingSearchConnection!.outputs.bingConnectionId : '', label: appConfigLabel, contentType: 'text/plain' } @@ -4273,7 +4294,7 @@ output KNOWLEDGE_BASE_NAME string = retrievalBackend == 'foundry_iq' ? knowledge #disable-next-line BCP318 output KNOWLEDGE_BASE_ENDPOINT string = retrievalBackend == 'foundry_iq' && deploySearchService ? searchService.outputs.endpoint : '' #disable-next-line BCP318 -output KNOWLEDGE_BASE_CONNECTION_ID string = retrievalBackend == 'foundry_iq' && deploySearchService && deployAiFoundry ? aiFoundryKnowledgeBaseSearchConnection.outputs.searchConnectionId : '' +output KNOWLEDGE_BASE_CONNECTION_ID string = retrievalBackend == 'foundry_iq' && deploySearchService && deployAiFoundry && deployAfProject ? aiFoundryKnowledgeBaseSearchConnection.outputs.connectionId : '' output FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME string = retrievalBackend == 'foundry_iq' ? foundryIqKnowledgeSourceName : '' output FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND string = effectiveFoundryIqKnowledgeSourceKind diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index a7a014c1..021b4bd8 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -332,6 +332,7 @@ "enabled": true }, "roles": [ + "FoundryAgentConsumer", "AppConfigurationDataReader", "AcrPull", "StorageBlobDataReader", diff --git a/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep b/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep new file mode 100644 index 00000000..da442049 --- /dev/null +++ b/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep @@ -0,0 +1,35 @@ +metadata name = 'ai-foundry-connection-knowledge-base' +metadata description = 'Project MCP connection to a Search knowledge base using project managed identity.' + +param aiFoundryName string +param aiProjectName string +param knowledgeBaseConnectionName string +param searchEndpoint string +param knowledgeBaseName string +param knowledgeApiVersion string + +resource account 'Microsoft.CognitiveServices/accounts@2025-06-01' existing = { + name: aiFoundryName +} + +resource project 'Microsoft.CognitiveServices/accounts/projects@2025-06-01' existing = { + parent: account + name: aiProjectName +} + +resource connection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview' = { + parent: project + name: knowledgeBaseConnectionName + properties: { + category: 'RemoteTool' + authType: 'ProjectManagedIdentity' + target: '${searchEndpoint}/knowledgebases/${knowledgeBaseName}/mcp?api-version=${knowledgeApiVersion}' + audience: 'https://search.azure.com/' + isSharedToAll: true + metadata: { + ApiType: 'Azure' + } + } +} + +output connectionId string = connection.id diff --git a/infra/shared/roles.json b/infra/shared/roles.json index 2c95e740..783d604e 100644 --- a/infra/shared/roles.json +++ b/infra/shared/roles.json @@ -163,6 +163,10 @@ "guid": "53ca6127-db72-4b80-b1b0-d745d6d5456d", "key": "AzureAIUser" }, + "FoundryAgentConsumer": { + "guid": "eed3b665-ab3a-47b6-8f48-c9382fb1dad6", + "key": "FoundryAgentConsumer" + }, "AzureApiCenterComplianceManager": { "guid": "ede9aaa3-4627-494e-be13-4aa7c256148d", "key": "AzureApiCenterComplianceManager" diff --git a/infra/terraform/main.compute.tf b/infra/terraform/main.compute.tf index 53c8063f..c2b8c1a5 100644 --- a/infra/terraform/main.compute.tf +++ b/infra/terraform/main.compute.tf @@ -117,8 +117,9 @@ locals { ENABLE_AGENTIC_RETRIEVAL = tostring(var.enableAgenticRetrieval) RETRIEVAL_BACKEND = var.retrievalBackend KNOWLEDGE_BASE_NAME = var.retrievalBackend == "foundry_iq" ? var.knowledgeBaseName : "" + TOOLBOX_ENDPOINT = var.retrievalBackend == "foundry_iq" && var.deployAiFoundry && var.deployAfProject && var.deploySearchService ? "${local.foundry_context.project_endpoint}/toolboxes/${var.knowledgeBaseName}/mcp?api-version=v1" : "" KNOWLEDGE_BASE_ENDPOINT = var.retrievalBackend == "foundry_iq" ? "https://${local.resource_names.searchServiceName}.search.windows.net" : "" - KNOWLEDGE_BASE_CONNECTION_ID = var.retrievalBackend == "foundry_iq" ? "/subscriptions/${local.deployment_context.subscription_id}/resourceGroups/${local.deployment_context.foundry_rg}/providers/Microsoft.CognitiveServices/accounts/${local.resource_names.aiFoundryAccountName}/projects/${local.resource_names.aiFoundryProjectName}/connections/${var.knowledgeBaseConnectionName}" : "" + KNOWLEDGE_BASE_CONNECTION_ID = var.retrievalBackend == "foundry_iq" && var.deployAiFoundry && var.deployAfProject && var.deploySearchService ? "/subscriptions/${local.deployment_context.subscription_id}/resourceGroups/${local.deployment_context.foundry_rg}/providers/Microsoft.CognitiveServices/accounts/${local.resource_names.aiFoundryAccountName}/projects/${local.resource_names.aiFoundryProjectName}/connections/${var.knowledgeBaseConnectionName}" : "" FOUNDRY_IQ_API_VERSION = var.retrievalBackend == "foundry_iq" ? var.foundryIqApiVersion : "" FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN = var.retrievalBackend == "foundry_iq" ? var.foundryIqKnowledgeRetrievalBillingPlan : "" FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME = var.retrievalBackend == "foundry_iq" ? var.foundryIqKnowledgeSourceName : "" diff --git a/infra/terraform/main.data.tf b/infra/terraform/main.data.tf index cff6aca4..8cd78dfc 100644 --- a/infra/terraform/main.data.tf +++ b/infra/terraform/main.data.tf @@ -521,6 +521,7 @@ locals { AZURE_SPEECH_ENDPOINT = local.data_context.speech.endpoint SEARCH_CONNECTION_ID = local.foundry_context.search_connection_id KNOWLEDGE_BASE_NAME = var.retrievalBackend == "foundry_iq" ? var.knowledgeBaseName : "" + TOOLBOX_ENDPOINT = var.retrievalBackend == "foundry_iq" && var.deployAiFoundry && var.deployAfProject && var.deploySearchService ? "${local.foundry_context.project_endpoint}/toolboxes/${var.knowledgeBaseName}/mcp?api-version=v1" : "" BING_CONNECTION_ID = local.foundry_context.bing_connection_id CONTAINER_ENV_PRINCIPAL_ID = local.compute_context.environment_principal_id SEARCH_SERVICE_PRINCIPAL_ID = local.data_context.search.principal_id diff --git a/infra/terraform/main.foundry.tf b/infra/terraform/main.foundry.tf index b4dad52b..6f0007db 100644 --- a/infra/terraform/main.foundry.tf +++ b/infra/terraform/main.foundry.tf @@ -312,11 +312,11 @@ locals { substr(local.services_project_internal_id, 20, 12) ) : "" search_connection_id = var.deployAiFoundry && var.deploySearchService ? "${local.services_project_arm_path}/connections/${local.services_connection_names.search}" : "" - knowledge_base_connection_id = var.deployAiFoundry && var.deploySearchService && var.retrievalBackend == "foundry_iq" ? "${local.services_project_arm_path}/connections/${var.knowledgeBaseConnectionName}" : "" + knowledge_base_connection_id = local.services_project && var.deploySearchService && var.retrievalBackend == "foundry_iq" ? "${local.services_project_arm_path}/connections/${var.knowledgeBaseConnectionName}" : "" bing_connection_id = var.deployAiFoundry && var.deployGroundingWithBing ? "${local.services_project_arm_path}/connections/${local.services_connection_names.bing}" : "" } - # This is intentionally a project-scoped runtime path, even though connections - # are ARM account children; source consumers rely on project visibility. + # Shared account connections use project-scoped runtime paths; the knowledge + # base RemoteTool below is a real project child, not an inherited Search binding. services_project_arm_path = "${var.deployAiFoundry ? azapi_resource.foundry_account[0].id : ""}/projects/${local.resource_names.aiFoundryProjectName}" } @@ -451,18 +451,6 @@ locals { metadata = { ApiType = "Azure", ResourceId = local.data_context.search.id, location = local.effective_locations.search } } } - } : {}, - var.deployAiFoundry && var.deploySearchService && var.retrievalBackend == "foundry_iq" ? { - knowledge = { - name = var.knowledgeBaseConnectionName - properties = { - category = "CognitiveSearch" - target = local.data_context.search.endpoint - authType = "AAD" - isSharedToAll = true - metadata = { ApiType = "Azure", ResourceId = local.data_context.search.id, location = local.effective_locations.search } - } - } } : {} ) } @@ -476,6 +464,24 @@ resource "azapi_resource" "account_connection" { depends_on = [terraform_data.foundry_connection_contract] } +resource "azapi_resource" "knowledge_base_connection" { + count = local.services_project && var.deploySearchService && var.retrievalBackend == "foundry_iq" ? 1 : 0 + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview" + parent_id = local.foundry_context.project_id + name = var.knowledgeBaseConnectionName + body = { + properties = { + category = "RemoteTool" + authType = "ProjectManagedIdentity" + target = "${local.data_context.search.endpoint}/knowledgebases/${var.knowledgeBaseName}/mcp?api-version=${var.foundryIqApiVersion}" + audience = "https://search.azure.com/" + isSharedToAll = true + metadata = { ApiType = "Azure" } + } + } + depends_on = [terraform_data.foundry_connection_contract] +} + resource "azapi_resource" "insights_connection" { count = var.deployAiFoundry && (local.services_existing_ai != "" || local.services_create_ai) ? 1 : 0 type = "Microsoft.CognitiveServices/accounts/connections@2025-06-01" diff --git a/infra/terraform/main.identity.tf b/infra/terraform/main.identity.tf index 4b386257..9322e9d7 100644 --- a/infra/terraform/main.identity.tf +++ b/infra/terraform/main.identity.tf @@ -35,15 +35,16 @@ locals { { ContainerRegistryRepositoryReader = { guid = "b93aa761-3e63-49ed-ac28-beffa264f7ac", key = "ContainerRegistryRepositoryReader" } } ) identity_scope_definitions = { - registry = { enabled = var.deployContainerRegistry, id = local.registry_context.id } - key_vault = { enabled = var.deployKeyVault, id = local.data_context.key_vault.id } - search = { enabled = var.deploySearchService, id = local.data_context.search.id } - storage = { enabled = var.deployStorageAccount, id = local.data_context.storage.id } - foundry = { enabled = var.deployAiFoundry, id = local.foundry_context.account_id } - speech = { enabled = var.deploySpeechService, id = local.data_context.speech.id } - app_config = { enabled = var.deployAppConfig, id = local.data_context.app_config.id } - project = { enabled = var.deployAiFoundry && var.deployAfProject && local.effective_flags.hosted_prerequisites && var.grantHostedAgentProjectManager, id = local.foundry_context.project_id } - root = { enabled = true, id = "/subscriptions/${local.deployment_context.subscription_id}/resourceGroups/${local.deployment_context.foundry_rg}" } + registry = { enabled = var.deployContainerRegistry, id = local.registry_context.id } + key_vault = { enabled = var.deployKeyVault, id = local.data_context.key_vault.id } + search = { enabled = var.deploySearchService, id = local.data_context.search.id } + storage = { enabled = var.deployStorageAccount, id = local.data_context.storage.id } + foundry = { enabled = var.deployAiFoundry, id = local.foundry_context.account_id } + speech = { enabled = var.deploySpeechService, id = local.data_context.speech.id } + app_config = { enabled = var.deployAppConfig, id = local.data_context.app_config.id } + project = { enabled = var.deployAiFoundry && var.deployAfProject && local.effective_flags.hosted_prerequisites && var.grantHostedAgentProjectManager, id = local.foundry_context.project_id } + agent_project = { enabled = var.deployAiFoundry && var.deployAfProject, id = local.foundry_context.project_id } + root = { enabled = true, id = "/subscriptions/${local.deployment_context.subscription_id}/resourceGroups/${local.deployment_context.foundry_rg}" } } executor_role_specs = { registry = ["AcrPush", "AcrPull"] @@ -82,6 +83,7 @@ locals { StorageBlobDataContributor = ["storage"] StorageBlobDataReader = ["storage"] StorageBlobDelegator = ["storage"] + FoundryAgentConsumer = ["agent_project"] } executor_grants = flatten([ for scope_key, roles in local.executor_role_specs : [ diff --git a/main.parameters.json b/main.parameters.json index 98a09b3f..cf258864 100644 --- a/main.parameters.json +++ b/main.parameters.json @@ -377,6 +377,7 @@ "enabled": true }, "roles": [ + "FoundryAgentConsumer", "AppConfigurationDataReader", "AcrPull", "SearchIndexDataReader", diff --git a/scripts/postProvision.ps1 b/scripts/postProvision.ps1 index 06e643a1..d618d282 100755 --- a/scripts/postProvision.ps1 +++ b/scripts/postProvision.ps1 @@ -925,6 +925,8 @@ if (-not $missing.Contains('APP_CONFIG_ENDPOINT')) { Invoke-PythonModule -ModuleName 'config.search.setup' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Write-Host "✅ Search setup script finished." + Invoke-PythonModule -ModuleName 'config.aifoundry.toolbox' + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } else { Write-Host "⏭️ Skipping Search setup (missing APP_CONFIG_ENDPOINT)." } diff --git a/scripts/postProvision.sh b/scripts/postProvision.sh index 045a2e1f..631b9580 100755 --- a/scripts/postProvision.sh +++ b/scripts/postProvision.sh @@ -171,7 +171,8 @@ echo echo "🔍 AI Search setup…" { echo "🚀 Running config.search.setup…" - python -m config.search.setup + python -m config.search.setup || exit $? + python -m config.aifoundry.toolbox || exit $? echo "✅ Search setup script finished." } || { echo "❗️ Error during Search setup." >&2 diff --git a/tests/test_knowledge_toolbox.py b/tests/test_knowledge_toolbox.py new file mode 100644 index 00000000..7c8fb86f --- /dev/null +++ b/tests/test_knowledge_toolbox.py @@ -0,0 +1,126 @@ +"""Managed KB registration through the real pinned project SDK, offline only.""" + +import json +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock, patch + +import pytest +import requests +from azure.ai.projects import AIProjectClient +from azure.core.credentials import AccessToken + +from config.aifoundry.toolbox import KnowledgeToolbox + +PROJECT = "https://test-foundry.services.ai.azure.com/api/projects/test-project" +PROJECT_ID = ( + "/subscriptions/11111111-1111-4111-8111-111111111111/resourceGroups/test-group/providers/" + "Microsoft.CognitiveServices/accounts/test-foundry/projects/test-project" +) +BASE = { + "RETRIEVAL_BACKEND": "foundry_iq", "AI_FOUNDRY_PROJECT_ENDPOINT": PROJECT, + "AI_FOUNDRY_PROJECT_RESOURCE_ID": PROJECT_ID, "KNOWLEDGE_BASE_NAME": "test-kb", + "KNOWLEDGE_BASE_ENDPOINT": "https://test-search.search.windows.net", + "KNOWLEDGE_BASE_CONNECTION_ID": PROJECT_ID + "/connections/kb-connection", + "TOOLBOX_ENDPOINT": PROJECT + "/toolboxes/test-kb/mcp?api-version=v1", + "FOUNDRY_IQ_API_VERSION": "2026-05-01-preview", +} +SERVICE = { + "HOSTED_AGENT_AUTH_MODE": "service_identity", "FOUNDRY_IQ_PATTERN": "azureBlob", + "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND": "azureBlob", "FOUNDRY_IQ_IS_ADLS_GEN2": "false", + "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "[]", +} + + +@pytest.mark.parametrize("profile", [{}, SERVICE]) +def test_real_sdk_registers_kb_tool_and_promotes_version_without_user_credentials(profile): + binding = KnowledgeToolbox.read({**BASE, **profile}) + assert binding is not None + packets = [] + + def boundary(method, url, **kwargs): + body = json.loads(kwargs["data"]) + packets.append((method, url, body)) + response = requests.Response() + response.status_code = 200 + response.headers["Content-Type"] = "application/json" + response._content = json.dumps({ + "id": "test-toolbox", "name": "test-kb", "version": "1", + "default_version": "1", "tools": body.get("tools", []), + }).encode() + response.raw = SimpleNamespace() + response._content_consumed = True + return response + + credential = SimpleNamespace(get_token=Mock(return_value=AccessToken("synthetic-agent-token", 2**31))) + with patch("requests.Session.request", side_effect=boundary), AIProjectClient( + endpoint=PROJECT, credential=credential, + ) as client: + binding.provision(client) + assert [(method, url) for method, url, _ in packets] == [ + ("POST", PROJECT + "/toolboxes/test-kb/versions?api-version=v1"), + ("PATCH", PROJECT + "/toolboxes/test-kb?api-version=v1"), + ] + assert packets[0][2]["tools"] == [{ + "type": "mcp", "name": "knowledge_base", "server_label": "knowledge_base", + "server_url": "https://test-search.search.windows.net/knowledgebases/test-kb/mcp?api-version=2026-05-01-preview", + "project_connection_id": "kb-connection", "require_approval": "never", + "allowed_tools": ["knowledge_base_retrieve"], + }] + assert packets[1][2] == {"default_version": "1"} + assert credential.get_token.call_args.args == ("https://ai.azure.com/.default",) + assert "synthetic-agent-token" not in json.dumps(packets) + assert "x-ms-query-source-authorization" not in json.dumps(packets) + + +@pytest.mark.parametrize("override", [ + {"KNOWLEDGE_BASE_NAME": "../other"}, {"TOOLBOX_ENDPOINT": PROJECT + "/other"}, + {"KNOWLEDGE_BASE_CONNECTION_ID": PROJECT_ID + "-other/connections/kb"}, + {"AI_FOUNDRY_PROJECT_ENDPOINT": "https://other.services.ai.azure.com/api/projects/test-project"}, +]) +def test_invalid_toolbox_binding_rejected_before_sdk_use(override): + with pytest.raises(ValueError): + KnowledgeToolbox.read({**BASE, **SERVICE, **override}) + + +@pytest.mark.parametrize("source", [ + {}, {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": None}, + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": '["rbacScope"]'}, + {"FOUNDRY_IQ_IS_ADLS_GEN2": "true"}, + {"FOUNDRY_IQ_PATTERN": "searchIndex", "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND": "searchIndex"}, + {"WORK_IQ_ENABLED": "true"}, {"FOUNDRY_IQ_MCP_ENABLED": "true"}, +]) +def test_service_toolbox_registration_does_not_restrict_source_types_or_change_permissions(source): + settings = {**BASE, "HOSTED_AGENT_AUTH_MODE": "service_identity", **source} + original = dict(settings) + assert KnowledgeToolbox.read(settings) == KnowledgeToolbox.read(BASE) + assert settings == original + + +def test_default_protected_policy_remains_unchanged_and_non_kb_is_not_provisioned(): + settings = dict(BASE) + before = dict(settings) + assert KnowledgeToolbox.read(settings) + assert settings == before + assert "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS" not in settings + assert KnowledgeToolbox.read({"RETRIEVAL_BACKEND": "ai_search"}) is None + + +def test_failed_registration_never_promotes_or_retries_without_its_connection(): + client = Mock() + client.toolboxes.create_version.side_effect = RuntimeError("registration denied") + binding = KnowledgeToolbox.read(BASE) + with pytest.raises(RuntimeError, match="registration denied"): + binding.provision(client) + client.toolboxes.create_version.assert_called_once() + client.toolboxes.update.assert_not_called() + + +def test_hooks_order_and_provider_binding_publication_are_coherent(): + root = Path(__file__).resolve().parents[1] + for path in ("scripts/postProvision.ps1", "scripts/postProvision.sh"): + source = (root / path).read_text(encoding="utf-8-sig") + assert source.index("config.search.setup") < source.index("config.aifoundry.toolbox") + for path in ("infra/bicep/main.bicep", "infra/terraform/main.compute.tf", "infra/terraform/main.data.tf"): + source = (root / path).read_text() + assert "TOOLBOX_ENDPOINT" in source and "/toolboxes/" in source and "/mcp?api-version=v1" in source diff --git a/tests/test_native_knowledge_connection.py b/tests/test_native_knowledge_connection.py new file mode 100644 index 00000000..ef008ea3 --- /dev/null +++ b/tests/test_native_knowledge_connection.py @@ -0,0 +1,64 @@ +"""Offline contract of the connection consumed by the managed Foundry KB Toolbox.""" + +import json +import shutil +import subprocess +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class NativeKnowledgeConnectionTests(unittest.TestCase): + def test_compiled_connection_targets_kb_mcp_with_project_managed_identity(self): + executable = shutil.which("bicep") + if not executable: + self.skipTest("Bicep compiler not installed") + result = subprocess.run([ + executable, "build", + str(ROOT / "infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep"), + "--no-restore", "--stdout", + ], capture_output=True, text=True, timeout=30) + self.assertEqual(0, result.returncode, result.stderr) + template = json.loads(result.stdout) + resources = template["resources"] + connection = resources["connection"] if isinstance(resources, dict) else next( + item for item in resources if item["type"].endswith("/connections") + ) + self.assertEqual("Microsoft.CognitiveServices/accounts/projects/connections", connection["type"]) + self.assertEqual("RemoteTool", connection["properties"]["category"]) + self.assertEqual("ProjectManagedIdentity", connection["properties"]["authType"]) + self.assertEqual("https://search.azure.com/", connection["properties"]["audience"]) + self.assertIn("knowledgeBaseName", connection["properties"]["target"]) + self.assertIn("/mcp?api-version=", connection["properties"]["target"]) + + def test_bicep_and_terraform_provision_project_remote_tool_with_managed_auth(self): + bicep = (ROOT / "infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep").read_text() + terraform = (ROOT / "infra/terraform/main.foundry.tf").read_text() + for source in (bicep, terraform): + self.assertIn("Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview", source) + self.assertIn("RemoteTool", source) + self.assertIn("ProjectManagedIdentity", source) + self.assertIn("https://search.azure.com/", source) + self.assertIn("/knowledgebases/", source) + self.assertIn("/mcp?api-version=", source) + main = (ROOT / "infra/bicep/main.bicep").read_text() + self.assertIn("'modules/ai-foundry/connection-knowledge-base.bicep'", main) + self.assertIn("aiFoundryKnowledgeBaseSearchConnection.outputs.connectionId", main) + # Direct Search tool connections remain CognitiveSearch, not MCP. + direct = (ROOT / "infra/bicep/modules/ai-foundry/connection-ai-search.bicep").read_text() + self.assertIn("category: 'CognitiveSearch'", direct) + + def test_caller_consumer_role_is_project_scoped_and_uses_shared_catalog(self): + roles = json.loads((ROOT / "infra/shared/roles.json").read_text()) + self.assertEqual("eed3b665-ab3a-47b6-8f48-c9382fb1dad6", roles["FoundryAgentConsumer"]["guid"]) + for seed in ("main.parameters.json", "infra/bicep/main.parameters.json"): + parameters = json.loads((ROOT / seed).read_text())["parameters"] + apps = parameters["containerAppsList"]["value"] + frontend = next(app for app in apps if app["canonical_name"] == "FRONTEND_APP") + self.assertIn("FoundryAgentConsumer", frontend["roles"]) + self.assertNotIn("AzureAIUser", frontend["roles"]) + terraform = (ROOT / "infra/terraform/main.identity.tf").read_text() + self.assertIn('FoundryAgentConsumer = ["agent_project"]', terraform) + self.assertIn("id = local.foundry_context.project_id", terraform) diff --git a/tests/test_retrieval_provisioning.py b/tests/test_retrieval_provisioning.py index a327426f..58a3e8c5 100644 --- a/tests/test_retrieval_provisioning.py +++ b/tests/test_retrieval_provisioning.py @@ -4,6 +4,7 @@ import traceback import unittest from dataclasses import replace +from pathlib import Path from unittest.mock import patch from config.deployment import appconfig @@ -59,11 +60,12 @@ def test_source_publication_does_not_map_superseded_sharepoint_key(self): def test_container_apps_has_no_hosted_invocation_route(self): settings = self.build( ORCHESTRATION_HOST="container_apps", RETRIEVAL_AUTH_MODE="native_search", - HOSTED_AGENT_AUTH_MODE="service_identity", ) self.assertEqual("", settings["RETRIEVAL_AUTH_MODE"]) self.assertNotIn("HOSTED_AGENT_AUTH_MODE", settings) self.assertNotIn("CHAT_BACKEND", settings) + with self.assertRaisesRegex(ValueError, "requires ORCHESTRATION_HOST=hosted"): + self.build(ORCHESTRATION_HOST="container_apps", HOSTED_AGENT_AUTH_MODE="service_identity") def test_retired_preservation_controls_fail_before_discovery(self): for key in ("PRESERVE_CLASSIC_RUNTIME", "HOSTED_CUTOVER_COMPLETE"): @@ -85,17 +87,48 @@ def test_unknown_or_unimplemented_modes_fail_before_azure_reads(self): }) read.assert_not_called() - def test_native_route_never_selects_workload_or_new_invocation_auth(self): - for auth in ("service_identity", "bff_obo", "unknown"): + def test_native_route_rejects_unknown_invocation_auth(self): + for auth in ("bff_obo", "unknown"): with self.subTest(auth=auth), self.assertRaisesRegex( - ValueError, "HOSTED_AGENT_AUTH_MODE=user_delegated" + ValueError, "HOSTED_AGENT_AUTH_MODE" ): self.build(HOSTED_AGENT_AUTH_MODE=auth) + def test_explicit_common_blob_service_profile_uses_same_native_route(self): + settings = self.build( + HOSTED_AGENT_AUTH_MODE="service_identity", + FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS="[]", + ) + self.assertEqual("service_identity", settings["HOSTED_AGENT_AUTH_MODE"]) + self.assertEqual("native_search", settings["RETRIEVAL_AUTH_MODE"]) + self.assertNotIn("FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS", settings) + + def test_service_profile_does_not_restrict_source_types_or_capabilities(self): + for override in ( + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": None}, + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": '["rbacScope"]'}, + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "null"}, + {"FOUNDRY_IQ_IS_ADLS_GEN2": "true"}, {"FOUNDRY_IQ_PATTERN": "searchIndex"}, + {"RETRIEVAL_BACKEND": "ai_search"}, {"WORK_IQ_ENABLED": "true"}, + {"SHAREPOINT_INDEXED_ENABLED": "true"}, {"FABRIC_IQ_ENABLED": "true"}, + {"FOUNDRY_IQ_CONVERSATION_UPLOAD_ENABLED": "true"}, + {"FOUNDRY_IQ_MCP_ENABLED": "true"}, {"MCP_ENABLED": "true"}, + {"MULTIMODAL_ENABLED": "true"}, {"SEARCH_RETRIEVAL_ENABLED": "true"}, + ): + with self.subTest(override=override): + settings = self.build(**{ + "HOSTED_AGENT_AUTH_MODE": "service_identity", + "DEPLOY_INGESTION_CONTAINER_APP": "true", **override, + }) + self.assertEqual("service_identity", settings["HOSTED_AGENT_AUTH_MODE"]) + self.assertEqual("native_search", settings["RETRIEVAL_AUTH_MODE"]) + self.assertTrue(override.keys().isdisjoint(settings)) + def test_native_route_rejects_anonymous_and_disabled_source_authorization(self): for override in ( {"ALLOW_ANONYMOUS": "true"}, {"FOUNDRY_IQ_FORWARD_SOURCE_AUTH": "false"}, + {"HOSTED_AGENT_AUTH_MODE": "service_identity", "ALLOW_ANONYMOUS": "true"}, ): with self.subTest(override=override), self.assertRaises(ValueError): self.build(**override) @@ -209,14 +242,48 @@ def test_invalid_source_ids_fail_before_resource_discovery_without_echoing_value class NativeSearchTransportTests(unittest.TestCase): + def test_service_bootstrap_keeps_scoped_identity_independent_of_source_policy(self): + for key, value in { + "RETRIEVAL_BACKEND": "foundry_iq", + "HOSTED_AGENT_AUTH_MODE": "service_identity", + "FOUNDRY_IQ_PATTERN": "azureBlob", "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND": "azureBlob", + "FOUNDRY_IQ_IS_ADLS_GEN2": "false", "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "[]", + }.items(): + self.azure.settings[key] = {"key": key, "label": "agent-lz", "value": value} + plan = self.discover(HOSTED_AGENT_AUTH_MODE="service_identity") + self.assertEqual([], [ + grant for grant in plan.grants if grant.role is access.Role.SEARCH + ]) + self.assertEqual([access.Grant(access.Role.TOOLBOX, PROJECT)], [ + grant for grant in plan.grants if grant.role is access.Role.TOOLBOX + ]) + self.assertTrue(plan.toolbox) + self.assertEqual(4, len(plan.grants)) + for key, value in ( + ("FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS", '["rbacScope"]'), + ("WORK_IQ_ENABLED", "true"), + ): + original = self.azure.settings.get(key) + self.azure.settings[key] = {"key": key, "label": "agent-lz", "value": value} + self.assertEqual(plan, self.discover(HOSTED_AGENT_AUTH_MODE="service_identity")) + if original is None: + del self.azure.settings[key] + else: + self.azure.settings[key] = original + self.assertEqual([], self.azure.writes) + def setUp(self): self.azure = AzureFixture() for key, value in { - "RETRIEVAL_BACKEND": "foundry_iq", + "RETRIEVAL_BACKEND": "ai_search", "RETRIEVAL_AUTH_MODE": "native_search", "SEARCH_SERVICE_RESOURCE_ID": SEARCH, "SEARCH_SERVICE_NAME": "test-search", "SEARCH_SERVICE_QUERY_ENDPOINT": SEARCH_ENDPOINT, + "AI_FOUNDRY_PROJECT_RESOURCE_ID": PROJECT, + "AI_FOUNDRY_PROJECT_ENDPOINT": ACCESS_ENV["AZURE_AI_PROJECT_ENDPOINT"], + "KNOWLEDGE_BASE_NAME": "test-kb", + "TOOLBOX_ENDPOINT": ACCESS_ENV["AZURE_AI_PROJECT_ENDPOINT"] + "/toolboxes/test-kb/mcp?api-version=v1", }.items(): self.azure.settings[key] = {"key": key, "label": "agent-lz", "value": value} self.azure.resources[SEARCH] = { @@ -248,11 +315,32 @@ def test_container_apps_and_no_retrieval_deployments_never_add_hosted_search_gra self.assertFalse(self.discover().search_transport) self.assertNotIn(["resource", "show", "--ids", SEARCH], self.azure.calls) - def test_native_default_and_both_existing_search_backends_get_transport(self): + def test_native_backends_receive_only_their_own_transport_scope(self): del self.azure.settings["RETRIEVAL_AUTH_MODE"] for backend in ("foundry_iq", "ai_search"): self.azure.settings["RETRIEVAL_BACKEND"]["value"] = backend - self.assertTrue(self.discover(RETRIEVAL_BACKEND=backend).search_transport) + plan = self.discover(RETRIEVAL_BACKEND=backend) + self.assertEqual(backend == "ai_search", plan.search_transport) + self.assertEqual(backend == "foundry_iq", plan.toolbox) + + def test_managed_toolbox_binding_and_closed_project_role_fail_before_writes(self): + self.azure.settings["RETRIEVAL_BACKEND"]["value"] = "foundry_iq" + plan = self.discover() + roles = json.loads((Path(__file__).resolve().parents[1] / "infra/shared/roles.json").read_text()) + self.assertEqual(roles["AzureAIUser"]["guid"], access.Role.TOOLBOX.value) + for key in ("TOOLBOX_ENDPOINT", "AI_FOUNDRY_PROJECT_ENDPOINT", "AI_FOUNDRY_PROJECT_RESOURCE_ID"): + original = self.azure.settings[key]["value"] + self.azure.settings[key]["value"] = original + "-unreviewed" + with self.subTest(key=key), self.assertRaises(access.AccessError): + self.discover() + self.azure.settings[key]["value"] = original + for invalid in ( + replace(plan, toolbox=False), replace(plan, toolbox="true"), + replace(plan, grants=tuple(g for g in plan.grants if g.role is not access.Role.TOOLBOX)), + ): + with self.assertRaises(access.AccessError): + invalid.validate() + self.assertEqual([], self.azure.writes) def test_split_resource_groups_keep_search_in_foundry_not_application(self): foundry_group = "foundry-group" @@ -332,7 +420,7 @@ def test_endpoint_and_arm_identity_cannot_redirect_the_grant(self): def test_environment_and_persisted_binding_conflicts_fail_closed(self): for overrides in ( - {"RETRIEVAL_BACKEND": "ai_search"}, {"SEARCH_SERVICE_RESOURCE_ID": MODEL}, + {"RETRIEVAL_BACKEND": "foundry_iq"}, {"SEARCH_SERVICE_RESOURCE_ID": MODEL}, {"SEARCH_SERVICE_ENDPOINT": "https://other.search.windows.net"}, {"RETRIEVAL_AUTH_MODE": "foundry_toolbox"}, ): diff --git a/tests/test_topology_cli.py b/tests/test_topology_cli.py index 5f183aa8..8df0e0dd 100644 --- a/tests/test_topology_cli.py +++ b/tests/test_topology_cli.py @@ -81,3 +81,19 @@ def test_resolver_never_reads_existing_deployment_state(): assert app.host == "hosted" and not app.metadata assert not hasattr(topology, "read_persisted_settings") assert not hasattr(topology, "resource_group_has_resources") + + +def test_shared_service_profile_is_validated_by_existing_cross_platform_preprovision_entrypoint(): + selection = { + "HOSTED_AGENT_AUTH_MODE": "service_identity", + "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "[]", + } + with patch("subprocess.run", side_effect=AssertionError("Azure reads are forbidden")): + assert topology.resolve_environment_plan(selection).host == "hosted" + with pytest.raises(ValueError, match="WORK_IQ_ENABLED"): + topology.resolve_environment_plan({**selection, "WORK_IQ_ENABLED": "true"}) + with pytest.raises(ValueError, match="explicitly approved"): + topology.resolve_environment_plan({**selection, "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "null"}) + for hook in ("preProvision.ps1", "preProvision.sh"): + source = (ROOT / "scripts" / hook).read_text() + assert source.index("config.deployment.topology") < source.index("config.deployment.provider") From f081799bf8812146e4b237c254b4fd831c793ab6 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 2 Nov 2026 16:38:50 -0300 Subject: [PATCH 2/5] docs: record optional user delegation platform release changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcdd1bd2..63b91655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ ## [Unreleased] +### Added + +- **Optional user delegation.** Reuse `HOSTED_AGENT_AUTH_MODE`, defaulting to + `user_delegated`, with an explicitly selected Hosted `service_identity` + profile. Both profiles require admitted UI users and preserve conversation, + citation and download ownership boundaries. +- **Managed knowledge Toolbox.** Register and promote the project Toolbox + after Search setup, publish its endpoint through App Configuration, and + apply scoped project, caller and Search identity grants in both Bicep and + Terraform. + +### Changed + +- **Source-owned authorization.** Do not restrict service identity to Blob or + a source/capability allowlist. Preserve source permissions and ingestion + metadata. Sources requiring unavailable OBO fail explicitly without + authentication fallback, consent-link replay or service-privilege escalation. + +### Migration + +- Upgrade the coordinated platform and native UI/runtime combination. + Keep the default OBO profile unless service identity is explicitly required. + Change authentication modes through coordinated redeployment and new + sessions. Roll back the complete prior combination and deployment settings. + ## [v7.0.0] - 2026-11-02 ### Changed From d1545e7c25ec594ec010c6c449198851de7ab74e Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 2 Nov 2026 16:41:52 -0300 Subject: [PATCH 3/5] test: align topology coverage with source-owned authorization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tests/test_topology_cli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/test_topology_cli.py b/tests/test_topology_cli.py index 8df0e0dd..90bcf0cf 100644 --- a/tests/test_topology_cli.py +++ b/tests/test_topology_cli.py @@ -83,17 +83,20 @@ def test_resolver_never_reads_existing_deployment_state(): assert not hasattr(topology, "resource_group_has_resources") -def test_shared_service_profile_is_validated_by_existing_cross_platform_preprovision_entrypoint(): - selection = { - "HOSTED_AGENT_AUTH_MODE": "service_identity", - "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "[]", - } +def test_service_profile_keeps_structural_guards_without_a_source_allowlist(): + selection = {"HOSTED_AGENT_AUTH_MODE": "service_identity"} with patch("subprocess.run", side_effect=AssertionError("Azure reads are forbidden")): assert topology.resolve_environment_plan(selection).host == "hosted" - with pytest.raises(ValueError, match="WORK_IQ_ENABLED"): - topology.resolve_environment_plan({**selection, "WORK_IQ_ENABLED": "true"}) - with pytest.raises(ValueError, match="explicitly approved"): - topology.resolve_environment_plan({**selection, "FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "null"}) + for source in ( + {"WORK_IQ_ENABLED": "true"}, + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "null"}, + {"FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS": "[]"}, + ): + assert topology.resolve_environment_plan({**selection, **source}).host == "hosted" + with pytest.raises(ValueError, match="ORCHESTRATION_HOST=hosted"): + topology.resolve_environment_plan({**selection, "ORCHESTRATION_HOST": "container_apps"}) + with pytest.raises(ValueError, match="ALLOW_ANONYMOUS=false"): + topology.resolve_environment_plan({**selection, "ALLOW_ANONYMOUS": "true"}) for hook in ("preProvision.ps1", "preProvision.sh"): source = (ROOT / "scripts" / hook).read_text() assert source.index("config.deployment.topology") < source.index("config.deployment.provider") From e2a7ce5cef6798b1549e40c25bec8a5b7e91fdce Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 2 Nov 2026 16:46:21 -0300 Subject: [PATCH 4/5] fix: align knowledge connection with official raw ARM auth contract Use the connection API and payload implemented by azd. Limit the AzAPI schema exception to this resource because the published typed schema omits supported Foundry authentication variants. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../modules/ai-foundry/connection-knowledge-base.bicep | 3 ++- infra/terraform/main.foundry.tf | 5 ++++- tests/test_native_knowledge_connection.py | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep b/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep index da442049..7837d6e9 100644 --- a/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep +++ b/infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep @@ -17,7 +17,8 @@ resource project 'Microsoft.CognitiveServices/accounts/projects@2025-06-01' exis name: aiProjectName } -resource connection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview' = { +// Match azd's raw ARM connection API, which supports the newer Foundry auth types. +resource connection 'Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview' = { parent: project name: knowledgeBaseConnectionName properties: { diff --git a/infra/terraform/main.foundry.tf b/infra/terraform/main.foundry.tf index 6f0007db..37d95589 100644 --- a/infra/terraform/main.foundry.tf +++ b/infra/terraform/main.foundry.tf @@ -466,9 +466,12 @@ resource "azapi_resource" "account_connection" { resource "azapi_resource" "knowledge_base_connection" { count = local.services_project && var.deploySearchService && var.retrievalBackend == "foundry_iq" ? 1 : 0 - type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview" + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" parent_id = local.foundry_context.project_id name = var.knowledgeBaseConnectionName + # Foundry supports this auth type through raw ARM, ahead of the typed schema: + # Azure/azure-dev cli/azd/extensions/azure.ai.connections/internal/cmd/raw_connection.go. + schema_validation_enabled = false body = { properties = { category = "RemoteTool" diff --git a/tests/test_native_knowledge_connection.py b/tests/test_native_knowledge_connection.py index ef008ea3..5a06ef70 100644 --- a/tests/test_native_knowledge_connection.py +++ b/tests/test_native_knowledge_connection.py @@ -37,12 +37,16 @@ def test_bicep_and_terraform_provision_project_remote_tool_with_managed_auth(sel bicep = (ROOT / "infra/bicep/modules/ai-foundry/connection-knowledge-base.bicep").read_text() terraform = (ROOT / "infra/terraform/main.foundry.tf").read_text() for source in (bicep, terraform): - self.assertIn("Microsoft.CognitiveServices/accounts/projects/connections@2025-10-01-preview", source) + self.assertIn("Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview", source) self.assertIn("RemoteTool", source) self.assertIn("ProjectManagedIdentity", source) self.assertIn("https://search.azure.com/", source) self.assertIn("/knowledgebases/", source) self.assertIn("/mcp?api-version=", source) + connection = terraform.split('resource "azapi_resource" "knowledge_base_connection" {', 1)[1] + connection = connection.split('\nresource "', 1)[0] + self.assertIn("schema_validation_enabled = false", connection) + self.assertNotIn("credentials", connection) main = (ROOT / "infra/bicep/main.bicep").read_text() self.assertIn("'modules/ai-foundry/connection-knowledge-base.bicep'", main) self.assertIn("aiFoundryKnowledgeBaseSearchConnection.outputs.connectionId", main) From 9c876d43ccd38f6918ee32c3873bdfb45c7b7509 Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Mon, 2 Nov 2026 16:53:46 -0300 Subject: [PATCH 5/5] test: bind managed Toolbox changes to exact foundation contracts Keep runtime bootstrap values declarative, recognize dedicated project caller grants, and validate every new graph delta before unchanged historical fingerprints and mutation checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- infra/bicep/main.bicep | 2 +- .../ScopedResourceGroupNormalization.ps1 | 9 + .../Test-ScopedGraphNormalizationContract.ps1 | 3 +- .../knowledge-toolbox-normalization.json | 2429 +++++++++++++++++ tests/test_app_definition_schema.py | 4 +- 5 files changed, 2444 insertions(+), 3 deletions(-) create mode 100644 infra/tests/contracts/fixtures/knowledge-toolbox-normalization.json diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 3c871f37..7769553f 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -2985,7 +2985,7 @@ var _containerRuntimeEnv = [ { name: 'ENABLE_AGENTIC_RETRIEVAL', value: toLower(string(enableAgenticRetrieval)) } { name: 'RETRIEVAL_BACKEND', value: retrievalBackend } { name: 'KNOWLEDGE_BASE_NAME', value: retrievalBackend == 'foundry_iq' ? knowledgeBaseName : '' } - { name: 'TOOLBOX_ENDPOINT', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? '${aiFoundryProjectEndpoint}/toolboxes/${knowledgeBaseName}/mcp?api-version=v1' : '' } + { name: 'TOOLBOX_ENDPOINT', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? 'https://${resourceNames.aiFoundryAccountName}.services.ai.azure.com/api/projects/${resourceNames.aiFoundryProjectName}/toolboxes/${knowledgeBaseName}/mcp?api-version=v1' : '' } { name: 'KNOWLEDGE_BASE_ENDPOINT', value: retrievalBackend == 'foundry_iq' ? 'https://${resourceNames.searchServiceName}.search.windows.net' : '' } { name: 'KNOWLEDGE_BASE_CONNECTION_ID', value: retrievalBackend == 'foundry_iq' && deployAiFoundry && deployAfProject && deploySearchService ? '/subscriptions/${subscription().subscriptionId}/resourceGroups/${_foundryResourceGroupName}/providers/Microsoft.CognitiveServices/accounts/${resourceNames.aiFoundryAccountName}/projects/${resourceNames.aiFoundryProjectName}/connections/${knowledgeBaseConnectionName}' : '' } { name: 'FOUNDRY_IQ_API_VERSION', value: retrievalBackend == 'foundry_iq' ? foundryIqApiVersion : '' } diff --git a/infra/tests/contracts/ScopedResourceGroupNormalization.ps1 b/infra/tests/contracts/ScopedResourceGroupNormalization.ps1 index 1daedc0c..05d2b963 100644 --- a/infra/tests/contracts/ScopedResourceGroupNormalization.ps1 +++ b/infra/tests/contracts/ScopedResourceGroupNormalization.ps1 @@ -130,9 +130,18 @@ function Get-NativePlatformProjection { return $fixture.projection } +function Get-KnowledgeToolboxProjection { + $fixture = Get-Content (Join-Path $PSScriptRoot 'fixtures\knowledge-toolbox-normalization.json') -Raw | + ConvertFrom-Json -AsHashtable -Depth 100 + return $fixture.projection +} + function ConvertTo-ApplicationRootResourceGroupGraph { param([Parameter(Mandatory)] $Template) $copy = ConvertTo-Json -InputObject $Template -Depth 100 -Compress | ConvertFrom-Json -AsHashtable -Depth 100 + # Validate the exact managed Toolbox connection, binding and scoped grants + # before the existing adapters. Historical fingerprints remain unchanged. + $copy = Invoke-ScopedProjection $copy (Get-KnowledgeToolboxProjection) # ADR-0036: validate the exact optional vector/source-reference delta before # projecting it away. The original fingerprints still reject other changes. $mcp = Get-Content (Join-Path $PSScriptRoot 'fixtures\mcp-component-normalization.json') -Raw | diff --git a/infra/tests/contracts/Test-ScopedGraphNormalizationContract.ps1 b/infra/tests/contracts/Test-ScopedGraphNormalizationContract.ps1 index f2a1f9be..cd62f33f 100644 --- a/infra/tests/contracts/Test-ScopedGraphNormalizationContract.ps1 +++ b/infra/tests/contracts/Test-ScopedGraphNormalizationContract.ps1 @@ -176,7 +176,8 @@ try { } $subnetPaths = @(Get-SubnetDeltaPaths (Get-ArchitectureSubnetProjection)) + @(Get-SubnetDeltaPaths (Get-FirewallBasicProjection)) + - @(Get-SubnetDeltaPaths $native) + @(Get-SubnetDeltaPaths $native) + + @(Get-SubnetDeltaPaths (Get-KnowledgeToolboxProjection)) if ($subnetPaths.Count -eq 0) { throw 'Subnet normalization has no exact deploying delta assertions.' } foreach ($path in $subnetPaths) { $changed = $json | ConvertFrom-Json -AsHashtable -Depth 100 diff --git a/infra/tests/contracts/fixtures/knowledge-toolbox-normalization.json b/infra/tests/contracts/fixtures/knowledge-toolbox-normalization.json new file mode 100644 index 00000000..491b6831 --- /dev/null +++ b/infra/tests/contracts/fixtures/knowledge-toolbox-normalization.json @@ -0,0 +1,2429 @@ +{ + "description": "Exact optional-delegation managed Toolbox connection, binding and project-scoped caller-role delta. Historical fingerprints are unchanged.", + "sourceBaseline": "b8285c4", + "bicepVersion": "0.42.1", + "projection": { + "kind": "object", + "changes": { + "metadata": { + "kind": "object", + "changes": { + "_generator": { + "kind": "object", + "changes": { + "templateHash": { + "kind": "changed", + "before": "16628016361815852887", + "after": "15595852277510516286" + } + } + } + } + }, + "outputs": { + "kind": "object", + "changes": { + "KNOWLEDGE_BASE_CONNECTION_ID": { + "kind": "object", + "changes": { + "value": { + "kind": "changed", + "before": "[if(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), parameters('deployAiFoundry')), reference('aiFoundryKnowledgeBaseSearchConnection').outputs.searchConnectionId.value, '')]", + "after": "[if(and(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), parameters('deployAiFoundry')), parameters('deployAfProject')), reference('aiFoundryKnowledgeBaseSearchConnection').outputs.connectionId.value, '')]" + } + } + } + } + }, + "resources": { + "kind": "object", + "changes": { + "aiFoundryKnowledgeBaseSearchConnection": { + "kind": "object", + "changes": { + "condition": { + "kind": "changed", + "before": "[and(and(parameters('deployAiFoundry'), parameters('deploySearchService')), equals(parameters('retrievalBackend'), 'foundry_iq'))]", + "after": "[and(and(and(parameters('deployAiFoundry'), parameters('deployAfProject')), parameters('deploySearchService')), equals(parameters('retrievalBackend'), 'foundry_iq'))]" + }, + "properties": { + "kind": "object", + "changes": { + "parameters": { + "kind": "object", + "changes": { + "aiSearchConnectionName": { + "kind": "removed", + "before": { + "value": "[parameters('knowledgeBaseConnectionName')]" + } + }, + "connectedResourceName": { + "kind": "removed", + "before": { + "value": "[reference('searchService').outputs.name.value]" + } + }, + "knowledgeApiVersion": { + "kind": "added", + "after": { + "value": "[parameters('foundryIqApiVersion')]" + } + }, + "knowledgeBaseConnectionName": { + "kind": "added", + "after": { + "value": "[parameters('knowledgeBaseConnectionName')]" + } + }, + "knowledgeBaseName": { + "kind": "added", + "after": { + "value": "[parameters('knowledgeBaseName')]" + } + }, + "searchEndpoint": { + "kind": "added", + "after": { + "value": "[reference('searchService').outputs.endpoint.value]" + } + } + }, + "beforeKeys": [ + "aiFoundryName", + "aiProjectName", + "connectedResourceName", + "aiSearchConnectionName" + ], + "afterKeys": [ + "aiFoundryName", + "aiProjectName", + "knowledgeBaseConnectionName", + "searchEndpoint", + "knowledgeBaseName", + "knowledgeApiVersion" + ] + }, + "template": { + "kind": "object", + "changes": { + "metadata": { + "kind": "object", + "changes": { + "_generator": { + "kind": "object", + "changes": { + "templateHash": { + "kind": "changed", + "before": "4127944734531634783", + "after": "2530841008605255669" + } + } + }, + "description": { + "kind": "changed", + "before": "Create an Azure AI Search connection in Azure AI Foundry account.", + "after": "Project MCP connection to a Search knowledge base using project managed identity." + }, + "name": { + "kind": "changed", + "before": "ai-foundry-connection-ai-search", + "after": "ai-foundry-connection-knowledge-base" + } + } + }, + "outputs": { + "kind": "object", + "changes": { + "connectionId": { + "kind": "added", + "after": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects/connections', parameters('aiFoundryName'), parameters('aiProjectName'), parameters('knowledgeBaseConnectionName'))]" + } + }, + "connectionName": { + "kind": "removed", + "before": { + "type": "string", + "metadata": { + "description": "The name of the AI Search connection." + }, + "value": "[parameters('aiSearchConnectionName')]" + } + }, + "searchConnectionId": { + "kind": "removed", + "before": { + "type": "string", + "metadata": { + "description": "Connection ID path under the AI services project." + }, + "value": "[format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.CognitiveServices/accounts/{2}/projects/{3}/connections/{4}', subscription().subscriptionId, resourceGroup().name, parameters('aiFoundryName'), parameters('aiProjectName'), parameters('aiSearchConnectionName'))]" + } + } + }, + "beforeKeys": [ + "searchConnectionId", + "connectionName" + ], + "afterKeys": [ + "connectionId" + ] + }, + "parameters": { + "kind": "object", + "changes": { + "aiFoundryName": { + "kind": "object", + "changes": { + "metadata": { + "kind": "removed", + "before": { + "description": "Required. The name of the Azure AI Foundry account." + } + } + }, + "beforeKeys": [ + "type", + "metadata" + ], + "afterKeys": [ + "type" + ] + }, + "aiProjectName": { + "kind": "object", + "changes": { + "metadata": { + "kind": "removed", + "before": { + "description": "Required. The name of the Azure AI Foundry project." + } + } + }, + "beforeKeys": [ + "type", + "metadata" + ], + "afterKeys": [ + "type" + ] + }, + "aiSearchConnectionName": { + "kind": "removed", + "before": { + "type": "string", + "defaultValue": "[format('{0}-connection', parameters('connectedResourceName'))]", + "metadata": { + "description": "Optional. The name to assign to the AI Search connection." + } + } + }, + "connectedResourceName": { + "kind": "removed", + "before": { + "type": "string", + "metadata": { + "description": "Required. The name of the Azure AI Search service to connect." + } + } + }, + "knowledgeApiVersion": { + "kind": "added", + "after": { + "type": "string" + } + }, + "knowledgeBaseConnectionName": { + "kind": "added", + "after": { + "type": "string" + } + }, + "knowledgeBaseName": { + "kind": "added", + "after": { + "type": "string" + } + }, + "searchEndpoint": { + "kind": "added", + "after": { + "type": "string" + } + } + }, + "beforeKeys": [ + "aiFoundryName", + "aiProjectName", + "connectedResourceName", + "aiSearchConnectionName" + ], + "afterKeys": [ + "aiFoundryName", + "aiProjectName", + "knowledgeBaseConnectionName", + "searchEndpoint", + "knowledgeBaseName", + "knowledgeApiVersion" + ] + }, + "resources": { + "kind": "array", + "count": 1, + "changes": { + "0": { + "kind": "object", + "changes": { + "apiVersion": { + "kind": "changed", + "before": "2025-06-01", + "after": "2025-04-01-preview" + }, + "name": { + "kind": "changed", + "before": "[format('{0}/{1}', parameters('aiFoundryName'), parameters('aiSearchConnectionName'))]", + "after": "[format('{0}/{1}/{2}', parameters('aiFoundryName'), parameters('aiProjectName'), parameters('knowledgeBaseConnectionName'))]" + }, + "properties": { + "kind": "object", + "changes": { + "audience": { + "kind": "added", + "after": "https://search.azure.com/" + }, + "authType": { + "kind": "changed", + "before": "AAD", + "after": "ProjectManagedIdentity" + }, + "category": { + "kind": "changed", + "before": "CognitiveSearch", + "after": "RemoteTool" + }, + "metadata": { + "kind": "object", + "changes": { + "ResourceId": { + "kind": "removed", + "before": "[resourceId('Microsoft.Search/searchServices', parameters('connectedResourceName'))]" + }, + "location": { + "kind": "removed", + "before": "[reference(resourceId('Microsoft.Search/searchServices', parameters('connectedResourceName')), '2025-02-01-preview', 'full').location]" + } + }, + "beforeKeys": [ + "ApiType", + "ResourceId", + "location" + ], + "afterKeys": [ + "ApiType" + ] + }, + "target": { + "kind": "changed", + "before": "[reference(resourceId('Microsoft.Search/searchServices', parameters('connectedResourceName')), '2025-02-01-preview').endpoint]", + "after": "[format('{0}/knowledgebases/{1}/mcp?api-version={2}', parameters('searchEndpoint'), parameters('knowledgeBaseName'), parameters('knowledgeApiVersion'))]" + } + }, + "beforeKeys": [ + "category", + "target", + "authType", + "isSharedToAll", + "metadata" + ], + "afterKeys": [ + "category", + "authType", + "target", + "audience", + "isSharedToAll", + "metadata" + ] + }, + "type": { + "kind": "changed", + "before": "Microsoft.CognitiveServices/accounts/connections", + "after": "Microsoft.CognitiveServices/accounts/projects/connections" + } + } + } + } + } + } + } + } + } + } + }, + "appConfigPopulate": { + "kind": "object", + "changes": { + "properties": { + "kind": "object", + "changes": { + "parameters": { + "kind": "object", + "changes": { + "keyValues": { + "kind": "object", + "changes": { + "value": { + "kind": "changed", + "before": "[concat(filter(concat(if(variables('_deployContainerApps'), reference('containerAppsSettings').outputs.containerAppsEndpoints.value, createArray()), if(variables('_deployContainerApps'), reference('containerAppsSettings').outputs.containerAppsName.value, createArray()), variables('_modelDeploymentNamesSettings'), variables('_databaseContainerNamesSettings'), variables('_storageContainerNamesSettings'), createArray(createObject('name', 'AZURE_TENANT_ID', 'value', tenant().tenantId, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SUBSCRIPTION_ID', 'value', subscription().subscriptionId, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_RESOURCE_GROUP', 'value', resourceGroup().name, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_APP_RESOURCE_GROUP', 'value', variables('_applicationResourceGroupName'), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'AZURE_NETWORK_RESOURCE_GROUP', 'value', variables('_networkResourceGroupName'), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'LOCATION', 'value', parameters('location'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENVIRONMENT_NAME', 'value', parameters('environmentName'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOYMENT_NAME', 'value', deployment().name, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RESOURCE_TOKEN', 'value', parameters('resourceToken'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'NETWORK_ISOLATION', 'value', toLower(string(variables('_networkIsolation'))), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_RAG_INDEX_NAME', 'value', format('ragindex-{0}', parameters('resourceToken')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENABLE_AGENTIC_RETRIEVAL', 'value', toLower(string(parameters('enableAgenticRetrieval'))), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RETRIEVAL_BACKEND', 'value', parameters('retrievalBackend'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_PATTERN', 'value', parameters('foundryIqPattern'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_API_VERSION', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqApiVersion'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeRetrievalBillingPlan'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeSourceName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND', 'value', variables('effectiveFoundryIqKnowledgeSourceKind'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_STORAGE_CONTAINER_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqStorageContainerName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_STORAGE_FOLDER_PATH', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqStorageFolderPath'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_IS_ADLS_GEN2', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), toLower(string(parameters('foundryIqIsAdlsGen2'))), 'false'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_CONTENT_EXTRACTION_MODE', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqContentExtractionMode'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_AI_SERVICES_ENDPOINT', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), if(not(empty(parameters('foundryIqAiServicesEndpoint'))), parameters('foundryIqAiServicesEndpoint'), format('https://{0}.services.ai.azure.com/', variables('resourceNames').aiFoundryAccountName)), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), string(variables('effectiveFoundryIqIngestionPermissionOptions')), '[]'), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_SEARCH_INDEX_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSearchIndexName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SEMANTIC_CONFIGURATION_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSemanticConfigurationName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SOURCE_DATA_FIELDS', 'value', string(parameters('foundryIqSourceDataFields')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_SEARCH_FIELDS', 'value', string(parameters('foundryIqSearchFields')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_BASE_FILTER', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqBaseFilter'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_FILTER_ADD_ON_ENABLED', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), toLower(string(parameters('foundryIqFilterAddOnEnabled'))), 'false'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SECURITY_FIELD_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSecurityFieldName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_MAX_OUTPUT_DOCUMENTS', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqMaxOutputDocuments'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'USE_UAI', 'value', string(variables('_useUAI')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'LOG_LEVEL', 'value', 'INFO', 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENABLE_CONSOLE_LOGGING', 'value', 'true', 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RELEASE', 'value', variables('_manifest').tag, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APPLICATIONINSIGHTS__INSTRUMENTATIONKEY', 'value', if(not(empty(if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')))), split(split(if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')), ';')[0], '=')[1], ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KEY_VAULT_RESOURCE_ID', 'value', if(parameters('deployKeyVault'), reference('keyVault').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_ACCOUNT_RESOURCE_ID', 'value', if(parameters('deployStorageAccount'), reference('storageAccount').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DOCUMENTS_STORAGE_CONTAINER_RESOURCE_ID', 'value', if(and(parameters('deployStorageAccount'), equals(length(variables('_documentsContainers')), 1)), format('{0}/blobServices/default/containers/{1}', reference('storageAccount').outputs.resourceId.value, variables('_documentsContainers')[0].name), ''), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'APP_INSIGHTS_RESOURCE_ID', 'value', if(variables('_hasExistingAI'), parameters('existingApplicationInsightsResourceId'), if(variables('_createAppInsights'), reference('appInsights').outputs.resourceId.value, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'LOG_ANALYTICS_RESOURCE_ID', 'value', if(variables('_hasExistingLaw'), parameters('existingLogAnalyticsWorkspaceResourceId'), if(variables('_createLogAnalytics'), reference('logAnalytics').outputs.resourceId.value, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_RESOURCE_ID', 'value', if(parameters('deployContainerEnv'), reference('containerEnv').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_RESOURCE_ID', 'value', if(parameters('deployAiFoundry'), reference('aiFoundry').outputs.accountResourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_RESOURCE_ID', 'value', if(parameters('deployAiFoundry'), reference('aiFoundry').outputs.projectResourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_UAI_RESOURCE_ID', 'value', if(and(parameters('deploySearchService'), variables('_useUAI')), reference('searchServiceUAI').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_RESOURCE_ID', 'value', if(parameters('deploySearchService'), reference('searchService').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SHARED_PRIVATE_LINKS', 'value', string(variables('searchSharedPrivateLinkReadinessManifest')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'KNOWLEDGE_BASE_CONNECTION_ID', 'value', if(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), parameters('deployAiFoundry')), reference('aiFoundryKnowledgeBaseSearchConnection').outputs.searchConnectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_RESOURCE_ID', 'value', if(parameters('deploySpeechService'), reference('speechService').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_NAME', 'value', variables('resourceNames').aiFoundryAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_NAME', 'value', variables('resourceNames').aiFoundryProjectName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_STORAGE_ACCOUNT_NAME', 'value', variables('resourceNames').aiFoundryStorageAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APP_CONFIG_NAME', 'value', variables('resourceNames').appConfigName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APP_INSIGHTS_NAME', 'value', variables('resourceNames').appInsightsName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_NAME', 'value', variables('resourceNames').containerEnvName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_REGISTRY_NAME', 'value', variables('resourceNames').containerRegistryName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_REGISTRY_LOGIN_SERVER', 'value', format('{0}.azurecr.io', variables('resourceNames').containerRegistryName), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DATABASE_ACCOUNT_NAME', 'value', if(parameters('deployCosmosDb'), variables('resourceNames').dbAccountName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DATABASE_NAME', 'value', if(parameters('deployCosmosDb'), variables('resourceNames').dbDatabaseName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_NAME', 'value', variables('resourceNames').searchServiceName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_RESOURCE_NAME', 'value', if(parameters('deploySpeechService'), variables('resourceNames').speechServiceName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_REGION', 'value', if(parameters('deploySpeechService'), variables('_speechServiceLocation'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_ACCOUNT_NAME', 'value', variables('resourceNames').storageAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_APP_CONFIG', 'value', string(parameters('deployAppConfig')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_KEY_VAULT', 'value', string(parameters('deployKeyVault')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_LOG_ANALYTICS', 'value', string(parameters('deployLogAnalytics')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_APP_INSIGHTS', 'value', string(parameters('deployAppInsights')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_SEARCH_SERVICE', 'value', string(parameters('deploySearchService')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_SPEECH_SERVICE', 'value', string(parameters('deploySpeechService')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_STORAGE_ACCOUNT', 'value', string(parameters('deployStorageAccount')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_COSMOS_DB', 'value', string(parameters('deployCosmosDb')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_APPS', 'value', string(parameters('deployContainerApps')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_REGISTRY', 'value', string(parameters('deployContainerRegistry')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_ENV', 'value', string(parameters('deployContainerEnv')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KEY_VAULT_URI', 'value', if(parameters('deployKeyVault'), reference('keyVault').outputs.properties.value.vaultUri, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_BLOB_ENDPOINT', 'value', if(parameters('deployStorageAccount'), reference('storageAccount').outputs.primaryBlobEndpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_ENDPOINT', 'value', if(parameters('deployAiFoundry'), format('https://{0}.cognitiveservices.azure.com/', reference('aiFoundry').outputs.aiServicesName.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_ENDPOINT', 'value', if(parameters('deployAiFoundry'), format('https://{0}.services.ai.azure.com/api/projects/{1}', reference('aiFoundry').outputs.aiServicesName.value, reference('aiFoundry').outputs.aiProjectName.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_QUERY_ENDPOINT', 'value', if(parameters('deploySearchService'), reference('searchService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KNOWLEDGE_BASE_ENDPOINT', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), reference('searchService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_ENDPOINT', 'value', if(parameters('deploySpeechService'), reference('speechService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_CONNECTION_ID', 'value', if(and(parameters('deploySearchService'), parameters('deployAiFoundry')), reference('aiFoundryConnectionSearch').outputs.searchConnectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KNOWLEDGE_BASE_NAME', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('knowledgeBaseName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'BING_CONNECTION_ID', 'value', if(and(parameters('deployGroundingWithBing'), parameters('deployAiFoundry')), reference('bingSearchConnection').outputs.bingConnectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_PRINCIPAL_ID', 'value', if(parameters('deployContainerEnv'), if(variables('_useUAI'), reference('containerEnvUAI').outputs.principalId.value, coalesce(tryGet(reference('containerEnv').outputs.identity.value, 'principalId'), '')), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_PRINCIPAL_ID', 'value', if(parameters('deploySearchService'), if(variables('_useUAI'), reference('searchServiceUAI').outputs.principalId.value, reference('searchService').outputs.systemAssignedMIPrincipalId.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_APPS', 'value', if(variables('_deployContainerApps'), string(reference('containerAppsSettings').outputs.containerAppsList.value), '[]'), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'MODEL_DEPLOYMENTS', 'value', string(variables('_modelDeploymentSettings')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'))), lambda('kv', not(contains(variables('_additionalAppConfigKeys'), if(empty(lambdaVariables('kv').label), lambdaVariables('kv').name, format('{0}${1}', lambdaVariables('kv').name, lambdaVariables('kv').label)))))), variables('_normalizedAdditionalAppConfigSettings'))]", + "after": "[concat(filter(concat(if(variables('_deployContainerApps'), reference('containerAppsSettings').outputs.containerAppsEndpoints.value, createArray()), if(variables('_deployContainerApps'), reference('containerAppsSettings').outputs.containerAppsName.value, createArray()), variables('_modelDeploymentNamesSettings'), variables('_databaseContainerNamesSettings'), variables('_storageContainerNamesSettings'), createArray(createObject('name', 'AZURE_TENANT_ID', 'value', tenant().tenantId, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SUBSCRIPTION_ID', 'value', subscription().subscriptionId, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_RESOURCE_GROUP', 'value', resourceGroup().name, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_APP_RESOURCE_GROUP', 'value', variables('_applicationResourceGroupName'), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'AZURE_NETWORK_RESOURCE_GROUP', 'value', variables('_networkResourceGroupName'), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'LOCATION', 'value', parameters('location'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENVIRONMENT_NAME', 'value', parameters('environmentName'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOYMENT_NAME', 'value', deployment().name, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RESOURCE_TOKEN', 'value', parameters('resourceToken'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'NETWORK_ISOLATION', 'value', toLower(string(variables('_networkIsolation'))), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_RAG_INDEX_NAME', 'value', format('ragindex-{0}', parameters('resourceToken')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENABLE_AGENTIC_RETRIEVAL', 'value', toLower(string(parameters('enableAgenticRetrieval'))), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RETRIEVAL_BACKEND', 'value', parameters('retrievalBackend'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_PATTERN', 'value', parameters('foundryIqPattern'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_API_VERSION', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqApiVersion'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeRetrievalBillingPlan'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeSourceName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND', 'value', variables('effectiveFoundryIqKnowledgeSourceKind'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_STORAGE_CONTAINER_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqStorageContainerName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_STORAGE_FOLDER_PATH', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqStorageFolderPath'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_IS_ADLS_GEN2', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), toLower(string(parameters('foundryIqIsAdlsGen2'))), 'false'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_CONTENT_EXTRACTION_MODE', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), parameters('foundryIqContentExtractionMode'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_AI_SERVICES_ENDPOINT', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), if(not(empty(parameters('foundryIqAiServicesEndpoint'))), parameters('foundryIqAiServicesEndpoint'), format('https://{0}.services.ai.azure.com/', variables('resourceNames').aiFoundryAccountName)), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_INGESTION_PERMISSION_OPTIONS', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), not(equals(parameters('foundryIqPattern'), 'searchIndex'))), string(variables('effectiveFoundryIqIngestionPermissionOptions')), '[]'), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_SEARCH_INDEX_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSearchIndexName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SEMANTIC_CONFIGURATION_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSemanticConfigurationName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SOURCE_DATA_FIELDS', 'value', string(parameters('foundryIqSourceDataFields')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_SEARCH_FIELDS', 'value', string(parameters('foundryIqSearchFields')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'FOUNDRY_IQ_BASE_FILTER', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqBaseFilter'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_FILTER_ADD_ON_ENABLED', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), toLower(string(parameters('foundryIqFilterAddOnEnabled'))), 'false'), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_SECURITY_FIELD_NAME', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSecurityFieldName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'FOUNDRY_IQ_MAX_OUTPUT_DOCUMENTS', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqMaxOutputDocuments'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'USE_UAI', 'value', string(variables('_useUAI')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'LOG_LEVEL', 'value', 'INFO', 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'ENABLE_CONSOLE_LOGGING', 'value', 'true', 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'RELEASE', 'value', variables('_manifest').tag, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APPLICATIONINSIGHTS__INSTRUMENTATIONKEY', 'value', if(not(empty(if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')))), split(split(if(variables('_hasExistingAI'), coalesce(parameters('existingApplicationInsightsConnectionString'), ''), if(variables('_createAppInsights'), reference('appInsights').outputs.properties.value.ConnectionString, '')), ';')[0], '=')[1], ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KEY_VAULT_RESOURCE_ID', 'value', if(parameters('deployKeyVault'), reference('keyVault').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_ACCOUNT_RESOURCE_ID', 'value', if(parameters('deployStorageAccount'), reference('storageAccount').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DOCUMENTS_STORAGE_CONTAINER_RESOURCE_ID', 'value', if(and(parameters('deployStorageAccount'), equals(length(variables('_documentsContainers')), 1)), format('{0}/blobServices/default/containers/{1}', reference('storageAccount').outputs.resourceId.value, variables('_documentsContainers')[0].name), ''), 'label', 'agent-lz', 'contentType', 'text/plain'), createObject('name', 'APP_INSIGHTS_RESOURCE_ID', 'value', if(variables('_hasExistingAI'), parameters('existingApplicationInsightsResourceId'), if(variables('_createAppInsights'), reference('appInsights').outputs.resourceId.value, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'LOG_ANALYTICS_RESOURCE_ID', 'value', if(variables('_hasExistingLaw'), parameters('existingLogAnalyticsWorkspaceResourceId'), if(variables('_createLogAnalytics'), reference('logAnalytics').outputs.resourceId.value, '')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_RESOURCE_ID', 'value', if(parameters('deployContainerEnv'), reference('containerEnv').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_RESOURCE_ID', 'value', if(parameters('deployAiFoundry'), reference('aiFoundry').outputs.accountResourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_RESOURCE_ID', 'value', if(parameters('deployAiFoundry'), reference('aiFoundry').outputs.projectResourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_UAI_RESOURCE_ID', 'value', if(and(parameters('deploySearchService'), variables('_useUAI')), reference('searchServiceUAI').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_RESOURCE_ID', 'value', if(parameters('deploySearchService'), reference('searchService').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SHARED_PRIVATE_LINKS', 'value', string(variables('searchSharedPrivateLinkReadinessManifest')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'KNOWLEDGE_BASE_CONNECTION_ID', 'value', if(and(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), parameters('deployAiFoundry')), parameters('deployAfProject')), reference('aiFoundryKnowledgeBaseSearchConnection').outputs.connectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_RESOURCE_ID', 'value', if(parameters('deploySpeechService'), reference('speechService').outputs.resourceId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_NAME', 'value', variables('resourceNames').aiFoundryAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_NAME', 'value', variables('resourceNames').aiFoundryProjectName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_STORAGE_ACCOUNT_NAME', 'value', variables('resourceNames').aiFoundryStorageAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APP_CONFIG_NAME', 'value', variables('resourceNames').appConfigName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'APP_INSIGHTS_NAME', 'value', variables('resourceNames').appInsightsName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_NAME', 'value', variables('resourceNames').containerEnvName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_REGISTRY_NAME', 'value', variables('resourceNames').containerRegistryName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_REGISTRY_LOGIN_SERVER', 'value', format('{0}.azurecr.io', variables('resourceNames').containerRegistryName), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DATABASE_ACCOUNT_NAME', 'value', if(parameters('deployCosmosDb'), variables('resourceNames').dbAccountName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DATABASE_NAME', 'value', if(parameters('deployCosmosDb'), variables('resourceNames').dbDatabaseName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_NAME', 'value', variables('resourceNames').searchServiceName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_RESOURCE_NAME', 'value', if(parameters('deploySpeechService'), variables('resourceNames').speechServiceName, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_REGION', 'value', if(parameters('deploySpeechService'), variables('_speechServiceLocation'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_ACCOUNT_NAME', 'value', variables('resourceNames').storageAccountName, 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_APP_CONFIG', 'value', string(parameters('deployAppConfig')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_KEY_VAULT', 'value', string(parameters('deployKeyVault')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_LOG_ANALYTICS', 'value', string(parameters('deployLogAnalytics')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_APP_INSIGHTS', 'value', string(parameters('deployAppInsights')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_SEARCH_SERVICE', 'value', string(parameters('deploySearchService')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_SPEECH_SERVICE', 'value', string(parameters('deploySpeechService')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_STORAGE_ACCOUNT', 'value', string(parameters('deployStorageAccount')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_COSMOS_DB', 'value', string(parameters('deployCosmosDb')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_APPS', 'value', string(parameters('deployContainerApps')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_REGISTRY', 'value', string(parameters('deployContainerRegistry')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'DEPLOY_CONTAINER_ENV', 'value', string(parameters('deployContainerEnv')), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KEY_VAULT_URI', 'value', if(parameters('deployKeyVault'), reference('keyVault').outputs.properties.value.vaultUri, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'STORAGE_BLOB_ENDPOINT', 'value', if(parameters('deployStorageAccount'), reference('storageAccount').outputs.primaryBlobEndpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_ACCOUNT_ENDPOINT', 'value', if(parameters('deployAiFoundry'), format('https://{0}.cognitiveservices.azure.com/', reference('aiFoundry').outputs.aiServicesName.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AI_FOUNDRY_PROJECT_ENDPOINT', 'value', if(parameters('deployAiFoundry'), format('https://{0}.services.ai.azure.com/api/projects/{1}', reference('aiFoundry').outputs.aiServicesName.value, reference('aiFoundry').outputs.aiProjectName.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_QUERY_ENDPOINT', 'value', if(parameters('deploySearchService'), reference('searchService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KNOWLEDGE_BASE_ENDPOINT', 'value', if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deploySearchService')), reference('searchService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'AZURE_SPEECH_ENDPOINT', 'value', if(parameters('deploySpeechService'), reference('speechService').outputs.endpoint.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_CONNECTION_ID', 'value', if(and(parameters('deploySearchService'), parameters('deployAiFoundry')), reference('aiFoundryConnectionSearch').outputs.searchConnectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'KNOWLEDGE_BASE_NAME', 'value', if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('knowledgeBaseName'), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'TOOLBOX_ENDPOINT', 'value', if(and(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deployAiFoundry')), parameters('deployAfProject')), parameters('deploySearchService')), format('{0}/toolboxes/{1}/mcp?api-version=v1', format('https://{0}.services.ai.azure.com/api/projects/{1}', reference('aiFoundry').outputs.aiServicesName.value, reference('aiFoundry').outputs.aiProjectName.value), parameters('knowledgeBaseName')), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'BING_CONNECTION_ID', 'value', if(and(parameters('deployGroundingWithBing'), parameters('deployAiFoundry')), reference('bingSearchConnection').outputs.bingConnectionId.value, ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_ENV_PRINCIPAL_ID', 'value', if(parameters('deployContainerEnv'), if(variables('_useUAI'), reference('containerEnvUAI').outputs.principalId.value, coalesce(tryGet(reference('containerEnv').outputs.identity.value, 'principalId'), '')), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'SEARCH_SERVICE_PRINCIPAL_ID', 'value', if(parameters('deploySearchService'), if(variables('_useUAI'), reference('searchServiceUAI').outputs.principalId.value, reference('searchService').outputs.systemAssignedMIPrincipalId.value), ''), 'label', parameters('appConfigLabel'), 'contentType', 'text/plain'), createObject('name', 'CONTAINER_APPS', 'value', if(variables('_deployContainerApps'), string(reference('containerAppsSettings').outputs.containerAppsList.value), '[]'), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'), createObject('name', 'MODEL_DEPLOYMENTS', 'value', string(variables('_modelDeploymentSettings')), 'label', parameters('appConfigLabel'), 'contentType', 'application/json'))), lambda('kv', not(contains(variables('_additionalAppConfigKeys'), if(empty(lambdaVariables('kv').label), lambdaVariables('kv').name, format('{0}${1}', lambdaVariables('kv').name, lambdaVariables('kv').label)))))), variables('_normalizedAdditionalAppConfigSettings'))]" + } + } + } + } + } + } + } + } + }, + "assignAgentConsumerRoles": { + "kind": "added", + "after": { + "copy": { + "name": "assignAgentConsumerRoles", + "count": "[length(parameters('containerAppsList'))]" + }, + "condition": "[and(and(and(variables('_deployContainerApps'), parameters('deployAiFoundry')), parameters('deployAfProject')), contains(parameters('containerAppsList')[copyIndex()].roles, variables('_1.roles').FoundryAgentConsumer.key))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('agent-consumer-{0}-{1}', parameters('containerAppsList')[copyIndex()].service_name, parameters('resourceToken'))]", + "resourceGroup": "[variables('_foundryResourceGroupName')]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "roleAssignments": { + "value": [ + { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('_1.roles').FoundryAgentConsumer.guid)]", + "principalId": "[if(variables('_useUAI'), reference(format('containerAppsUAI[{0}]', copyIndex())).outputs.principalId.value, reference(format('containerApps[{0}]', copyIndex())).outputs.systemAssignedMIPrincipalId.value)]", + "resourceId": "[reference('aiFoundry').outputs.projectResourceId.value]", + "principalType": "ServicePrincipal" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.42.1.51946", + "templateHash": "4447600592370161114" + } + }, + "definitions": { + "roleAssignmentInfo": { + "type": "object", + "properties": { + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Role definition ID for the RBAC role to assign to the identity." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the identity to assign to." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID to assing too." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "metadata": { + "description": "Type of principal to assign the role to." + } + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "" + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentInfo" + }, + "defaultValue": [], + "metadata": { + "description": "Role assignments to create." + } + } + }, + "resources": { + "roleAssignment": { + "copy": { + "name": "roleAssignment", + "count": "[length(parameters('roleAssignments'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('role-{0}-{1}-{2}', take(parameters('name'), 31), uniqueString(parameters('name')), copyIndex())]", + "subscriptionId": "[if(empty(parameters('roleAssignments')[copyIndex()].resourceId), subscription().subscriptionId, split(parameters('roleAssignments')[copyIndex()].resourceId, '/')[2])]", + "resourceGroup": "[if(empty(parameters('roleAssignments')[copyIndex()].resourceId), resourceGroup().name, split(parameters('roleAssignments')[copyIndex()].resourceId, '/')[4])]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "roleAssignments": { + "value": [ + "[parameters('roleAssignments')[copyIndex()]]" + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "definitions": { + "roleAssignmentInfo": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID for target role assingment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Role definition ID for the RBAC role to assign to the identity." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Principal ID of the identity to assign to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "metadata": { + "description": "Type of the principal ID." + } + } + }, + "metadata": { + "description": "Role assignment information for an identity." + } + } + }, + "parameters": { + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentInfo" + }, + "defaultValue": [], + "metadata": { + "description": "Role assignments to create for the resource." + } + } + }, + "resources": { + "assignment": { + "copy": { + "name": "assignment", + "count": "[length(parameters('roleAssignments'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('roleAssignments')[copyIndex()].principalId, parameters('roleAssignments')[copyIndex()].roleDefinitionId, parameters('roleAssignments')[copyIndex()].resourceId)]", + "scope": "[if(or(empty(parameters('roleAssignments')[copyIndex()].resourceId), equals(toLower(parameters('roleAssignments')[copyIndex()].resourceId), toLower(resourceGroup().id))), '', parameters('roleAssignments')[copyIndex()].resourceId)]", + "properties": { + "principalId": "[parameters('roleAssignments')[copyIndex()].principalId]", + "roleDefinitionId": "[parameters('roleAssignments')[copyIndex()].roleDefinitionId]", + "principalType": "[parameters('roleAssignments')[copyIndex()].principalType]" + } + } + } + } + } + } + } + } + }, + "dependsOn": [ + "aiFoundry", + "[format('containerApps[{0}]', copyIndex())]", + "[format('containerAppsUAI[{0}]', copyIndex())]" + ] + } + }, + "firewall": { + "kind": "object", + "changes": { + "properties": { + "kind": "object", + "changes": { + "template": { + "kind": "object", + "changes": { + "metadata": { + "kind": "object", + "changes": { + "_generator": { + "kind": "object", + "changes": { + "templateHash": { + "kind": "changed", + "before": "12606235335634393575", + "after": "15686777219755594489" + } + } + } + } + }, + "variables": { + "kind": "object", + "changes": { + "_1._3": { + "kind": "object", + "changes": { + "FoundryAgentConsumer": { + "kind": "added", + "after": { + "guid": "eed3b665-ab3a-47b6-8f48-c9382fb1dad6", + "key": "FoundryAgentConsumer" + } + } + }, + "beforeKeys": [ + "DurableTaskDataContributor", + "AcrDelete", + "AcrImageSigner", + "AcrPull", + "AcrPush", + "AcrQuarantineReader", + "AcrQuarantineWriter", + "AksClusterAdminRole", + "AksClusterMonitoringUser", + "AksClusterUserRole", + "AksContributorRole", + "AksFleetManagerContributorRole", + "AksFleetManagerRBACAdmin", + "AksFleetManagerRBACClusterAdmin", + "AksFleetManagerRBACReader", + "AksFleetManagerRBACWriter", + "AksRBACAdmin", + "AksRBACClusterAdmin", + "AksRBACReader", + "AksRBACWriter", + "AppComplianceAutomationAdministrator", + "AppComplianceAutomationReader", + "AppConfigurationDataOwner", + "AppConfigurationDataReader", + "ApplicationInsightsComponentContributor", + "ApplicationInsightsSnapshotDebugger", + "AttestationContributor", + "AttestationReader", + "AutomationContributor", + "AutomationJobOperator", + "AutomationOperator", + "AutomationRunbookOperator", + "AvereContributor", + "AvereOperator", + "AzureAIAdministrator", + "AzureAIDeveloper", + "AzureAIEnterpriseNetworkConnectionApprover", + "AzureAIInferenceDeploymentOperator", + "AzureAIProjectManager", + "AzureAISafetyEvaluator", + "AzureAIUser", + "AzureApiCenterComplianceManager", + "AzureApiCenterDataReader", + "AzureApiCenterServiceContributor", + "AzureApiCenterServiceReader", + "AzureArcEnabledKubernetesClusterUser", + "AzureArcKubernetesAdmin", + "AzureArcKubernetesClusterAdmin", + "AzureArcKubernetesViewer", + "AzureArcKubernetesWriter", + "AzureConnectedMachineOnboarding", + "AzureConnectedMachineResourceAdministrator", + "AzureConnectedMachineResourceManager", + "AzureConnectedSQLServerOnboarding", + "AzureContainerStorageOperator", + "AzureContainerStorageOwner", + "AzureDigitalTwinsDataOwner", + "AzureDigitalTwinsDataReader", + "AzureEventHubsDataOwner", + "AzureEventHubsDataReceiver", + "AzureEventHubsDataSender", + "AzureFrontDoorDomainContributor", + "AzureFrontDoorDomainReader", + "AzureFrontDoorProfileReader", + "AzureFrontDoorSecretReader", + "AzureMLComputeOperator", + "AzureMLDataScientist", + "AzureMapsDataContributor", + "AzureMapsDataReader", + "AzureRelayListener", + "AzureRelayOwner", + "AzureRelaySender", + "AzureServiceBusDataOwner", + "AzureServiceBusDataReceiver", + "AzureServiceBusDataSender", + "AzureSpringCloudConfigServerContributor", + "AzureSpringCloudConfigServerReader", + "AzureSpringCloudDataReader", + "AzureSpringCloudServiceRegistryContributor", + "AzureSpringCloudServiceRegistryReader", + "AzureStackRegistrationOwner", + "BackupContributor", + "BackupOperator", + "BackupReader", + "BillingReader", + "BizTalkContributor", + "BlueprintContributor", + "BlueprintOperator", + "CarbonOptimizationReader", + "CdnEndpointContributor", + "CdnEndpointReader", + "CdnProfileContributor", + "CdnProfileReader", + "ClassicNetworkContributor", + "ClassicStorageAccountContributor", + "ClassicStorageAccountKeyOperatorServiceRole", + "ClassicVirtualMachineContributor", + "CognitiveServicesContributor", + "CognitiveServicesCustomVisionContributor", + "CognitiveServicesCustomVisionDeployment", + "CognitiveServicesCustomVisionLabeler", + "CognitiveServicesCustomVisionReader", + "CognitiveServicesCustomVisionTrainer", + "CognitiveServicesDataReader", + "CognitiveServicesFaceRecognizer", + "CognitiveServicesMetricsAdvisorAdministrator", + "CognitiveServicesOpenAIContributor", + "CognitiveServicesOpenAIUser", + "CognitiveServicesQnAMakerEditor", + "CognitiveServicesQnAMakerReader", + "CognitiveServicesUsagesReader", + "CognitiveServicesUser", + "ContainerAppReader", + "ContainerAppsConnectedEnvironmentsContributor", + "ContainerAppsConnectedEnvironmentsReader", + "ContainerAppsContributor", + "ContainerAppsJobsContributor", + "ContainerAppsJobsOperator", + "ContainerAppsJobsReader", + "ContainerAppsManagedEnvironmentsContributor", + "ContainerAppsManagedEnvironmentsReader", + "ContainerAppsOperator", + "ContainerAppsSessionExecutor", + "ContainerAppsSessionPoolsContributor", + "ContainerRegistryRepositoryWriter", + "ContainerRegistryTasksContributor", + "ContainerRegistryContributorDataAccessConfigurationAdministrator", + "Contributor", + "CosmosBackupOperator", + "CosmosDBAccountReaderRole", + "CosmosDBBuiltInDataContributor", + "CosmosDBBuiltInDataReader", + "CosmosDBOperator", + "CosmosRestoreOperator", + "CostManagementContributor", + "CostManagementReader", + "DataBoxContributor", + "DataBoxReader", + "DataFactoryContributor", + "DataLakeAnalyticsDeveloper", + "DataOperatorForManagedDisks", + "DataPurger", + "DefenderForStorageDataScanner", + "DesktopVirtualizationApplicationGroupContributor", + "DesktopVirtualizationApplicationGroupReader", + "DesktopVirtualizationContributor", + "DesktopVirtualizationHostPoolContributor", + "DesktopVirtualizationHostPoolReader", + "DesktopVirtualizationReader", + "DesktopVirtualizationSessionHostOperator", + "DesktopVirtualizationUser", + "DesktopVirtualizationUserSessionOperator", + "DesktopVirtualizationWorkspaceContributor", + "DesktopVirtualizationWorkspaceReader", + "DevTestLabsUser", + "DeviceUpdateAdministrator", + "DeviceUpdateContentAdministrator", + "DeviceUpdateContentReader", + "DeviceUpdateDeploymentsAdministrator", + "DeviceUpdateDeploymentsReader", + "DeviceUpdateReader", + "DiskBackupReader", + "DiskPoolOperator", + "DiskRestoreOperator", + "DiskSnapshotContributor", + "DnsZoneContributor", + "DocumentDBAccountContributor", + "DomainServicesContributor", + "DomainServicesReader", + "ElasticSANOwner", + "ElasticSANReader", + "ElasticSANVolumeGroupOwner", + "EventGridContributor", + "EventGridDataSender", + "EventGridEventSubscriptionContributor", + "EventGridEventSubscriptionReader", + "FhirDataContributor", + "FhirDataExporter", + "FhirDataImporter", + "FhirDataReader", + "FhirDataWriter", + "GrafanaAdmin", + "GrafanaEditor", + "GrafanaViewer", + "HdInsightClusterOperator", + "HdInsightDomainServicesContributor", + "HierarchySettingsAdministrator", + "IntegrationServiceEnvironmentContributor", + "IntegrationServiceEnvironmentDeveloper", + "IntelligentSystemsAccountContributor", + "IotHubDataContributor", + "IotHubDataReader", + "IotHubRegistryContributor", + "IotHubTwinContributor", + "KeyVaultAdministrator", + "KeyVaultCertificateUser", + "KeyVaultCertificatesOfficer", + "KeyVaultContributor", + "KeyVaultCryptoOfficer", + "KeyVaultCryptoServiceEncryptionUser", + "KeyVaultCryptoServiceReleaseUser", + "KeyVaultCryptoUser", + "KeyVaultDataAccessAdministrator", + "KeyVaultReader", + "KeyVaultSecretsOfficer", + "KeyVaultSecretsUser", + "KubernetesAgentlessOperator", + "KubernetesClusterAzureArcOnboarding", + "KubernetesExtensionContributor", + "LabAssistant", + "LabContributor", + "LabCreator", + "LabOperator", + "LabServicesContributor", + "LabServicesReader", + "LoadTestContributor", + "LoadTestOwner", + "LoadTestReader", + "LogAnalyticsContributor", + "LogAnalyticsReader", + "LogicAppContributor", + "LogicAppOperator", + "LogicAppsStandardContributor", + "LogicAppsStandardDeveloper", + "LogicAppsStandardOperator", + "LogicAppsStandardReader", + "ManagedApplicationContributorRole", + "ManagedApplicationOperatorRole", + "ManagedApplicationsReader", + "ManagedHSMContributor", + "ManagedIdentityContributor", + "ManagedIdentityOperator", + "ManagedServicesRegistrationAssignmentDeleteRole", + "ManagementGroupContributor", + "ManagementGroupReader", + "MediaServicesAccountAdministrator", + "MediaServicesLiveEventsAdministrator", + "MediaServicesMediaOperator", + "MediaServicesPolicyAdministrator", + "MediaServicesStreamingEndpointsAdministrator", + "MicrosoftSentinelAutomationContributor", + "MicrosoftSentinelContributor", + "MicrosoftSentinelPlaybookOperator", + "MicrosoftSentinelReader", + "MicrosoftSentinelResponder", + "MonitoringContributor", + "MonitoringMetricsPublisher", + "MonitoringReader", + "NetworkContributor", + "NewRelicAPMAccountContributor", + "Owner", + "PolicyInsightsDataWriter", + "PrivateDNSZoneContributor", + "QuotaRequestOperator", + "Reader", + "ReaderAndDataAccess", + "RedisCacheContributor", + "RemoteRenderingAdministrator", + "RemoteRenderingClient", + "ReservationPurchaser", + "ReservationsAdministrator", + "ReservationsReader", + "ResourcePolicyContributor", + "RoleBasedAccessControlAdministrator", + "ScheduledPatchingContributor", + "SchedulerJobCollectionsContributor", + "SchemaRegistryContributor", + "SchemaRegistryReader", + "SearchIndexDataContributor", + "SearchIndexDataReader", + "SearchServiceContributor", + "SecurityAdmin", + "SecurityAssessmentContributor", + "SecurityReader", + "ServicesHubOperator", + "SignalRAccessKeyReader", + "SignalRAppServer", + "SignalRRestAPIOwner", + "SignalRRestAPIReader", + "SignalRServiceOwner", + "SignalRWebPubSubContributor", + "SiteRecoveryContributor", + "SiteRecoveryOperator", + "SiteRecoveryReader", + "SpatialAnchorsAccountContributor", + "SpatialAnchorsAccountOwner", + "SpatialAnchorsAccountReader", + "SqlDBContributor", + "SqlManagedInstanceContributor", + "SqlSecurityManager", + "SqlServerContributor", + "StorageAccountBackupContributor", + "StorageAccountContributor", + "StorageAccountKeyOperatorServiceRole", + "StorageBlobDataContributor", + "StorageBlobDataOwner", + "StorageBlobDataReader", + "StorageBlobDelegator", + "StorageFileDataPrivilegedContributor", + "StorageFileDataPrivilegedReader", + "StorageFileDataSMBShareContributor", + "StorageFileDataSMBShareElevatedContributor", + "StorageFileDataSMBShareReader", + "StorageQueueDataContributor", + "StorageQueueDataMessageProcessor", + "StorageQueueDataMessageSender", + "StorageQueueDataReader", + "StorageTableDataContributor", + "StorageTableDataReader", + "StreamAnalyticsQueryTester", + "SupportRequestContributor", + "TagContributor", + "TemplateSpecContributor", + "TemplateSpecReader", + "TrafficManagerContributor", + "UserAccessAdministrator", + "VirtualMachineAdministratorLogin", + "VirtualMachineContributor", + "VirtualMachineDataAccessAdministrator", + "VirtualMachineLocalUserLogin", + "VirtualMachineUserLogin", + "WebPlanContributor", + "WebsiteContributor", + "WindowsAdminCenterAdministratorLogin", + "WorkbookContributor", + "WorkbookReader" + ], + "afterKeys": [ + "DurableTaskDataContributor", + "AcrDelete", + "AcrImageSigner", + "AcrPull", + "AcrPush", + "AcrQuarantineReader", + "AcrQuarantineWriter", + "AksClusterAdminRole", + "AksClusterMonitoringUser", + "AksClusterUserRole", + "AksContributorRole", + "AksFleetManagerContributorRole", + "AksFleetManagerRBACAdmin", + "AksFleetManagerRBACClusterAdmin", + "AksFleetManagerRBACReader", + "AksFleetManagerRBACWriter", + "AksRBACAdmin", + "AksRBACClusterAdmin", + "AksRBACReader", + "AksRBACWriter", + "AppComplianceAutomationAdministrator", + "AppComplianceAutomationReader", + "AppConfigurationDataOwner", + "AppConfigurationDataReader", + "ApplicationInsightsComponentContributor", + "ApplicationInsightsSnapshotDebugger", + "AttestationContributor", + "AttestationReader", + "AutomationContributor", + "AutomationJobOperator", + "AutomationOperator", + "AutomationRunbookOperator", + "AvereContributor", + "AvereOperator", + "AzureAIAdministrator", + "AzureAIDeveloper", + "AzureAIEnterpriseNetworkConnectionApprover", + "AzureAIInferenceDeploymentOperator", + "AzureAIProjectManager", + "AzureAISafetyEvaluator", + "AzureAIUser", + "FoundryAgentConsumer", + "AzureApiCenterComplianceManager", + "AzureApiCenterDataReader", + "AzureApiCenterServiceContributor", + "AzureApiCenterServiceReader", + "AzureArcEnabledKubernetesClusterUser", + "AzureArcKubernetesAdmin", + "AzureArcKubernetesClusterAdmin", + "AzureArcKubernetesViewer", + "AzureArcKubernetesWriter", + "AzureConnectedMachineOnboarding", + "AzureConnectedMachineResourceAdministrator", + "AzureConnectedMachineResourceManager", + "AzureConnectedSQLServerOnboarding", + "AzureContainerStorageOperator", + "AzureContainerStorageOwner", + "AzureDigitalTwinsDataOwner", + "AzureDigitalTwinsDataReader", + "AzureEventHubsDataOwner", + "AzureEventHubsDataReceiver", + "AzureEventHubsDataSender", + "AzureFrontDoorDomainContributor", + "AzureFrontDoorDomainReader", + "AzureFrontDoorProfileReader", + "AzureFrontDoorSecretReader", + "AzureMLComputeOperator", + "AzureMLDataScientist", + "AzureMapsDataContributor", + "AzureMapsDataReader", + "AzureRelayListener", + "AzureRelayOwner", + "AzureRelaySender", + "AzureServiceBusDataOwner", + "AzureServiceBusDataReceiver", + "AzureServiceBusDataSender", + "AzureSpringCloudConfigServerContributor", + "AzureSpringCloudConfigServerReader", + "AzureSpringCloudDataReader", + "AzureSpringCloudServiceRegistryContributor", + "AzureSpringCloudServiceRegistryReader", + "AzureStackRegistrationOwner", + "BackupContributor", + "BackupOperator", + "BackupReader", + "BillingReader", + "BizTalkContributor", + "BlueprintContributor", + "BlueprintOperator", + "CarbonOptimizationReader", + "CdnEndpointContributor", + "CdnEndpointReader", + "CdnProfileContributor", + "CdnProfileReader", + "ClassicNetworkContributor", + "ClassicStorageAccountContributor", + "ClassicStorageAccountKeyOperatorServiceRole", + "ClassicVirtualMachineContributor", + "CognitiveServicesContributor", + "CognitiveServicesCustomVisionContributor", + "CognitiveServicesCustomVisionDeployment", + "CognitiveServicesCustomVisionLabeler", + "CognitiveServicesCustomVisionReader", + "CognitiveServicesCustomVisionTrainer", + "CognitiveServicesDataReader", + "CognitiveServicesFaceRecognizer", + "CognitiveServicesMetricsAdvisorAdministrator", + "CognitiveServicesOpenAIContributor", + "CognitiveServicesOpenAIUser", + "CognitiveServicesQnAMakerEditor", + "CognitiveServicesQnAMakerReader", + "CognitiveServicesUsagesReader", + "CognitiveServicesUser", + "ContainerAppReader", + "ContainerAppsConnectedEnvironmentsContributor", + "ContainerAppsConnectedEnvironmentsReader", + "ContainerAppsContributor", + "ContainerAppsJobsContributor", + "ContainerAppsJobsOperator", + "ContainerAppsJobsReader", + "ContainerAppsManagedEnvironmentsContributor", + "ContainerAppsManagedEnvironmentsReader", + "ContainerAppsOperator", + "ContainerAppsSessionExecutor", + "ContainerAppsSessionPoolsContributor", + "ContainerRegistryRepositoryWriter", + "ContainerRegistryTasksContributor", + "ContainerRegistryContributorDataAccessConfigurationAdministrator", + "Contributor", + "CosmosBackupOperator", + "CosmosDBAccountReaderRole", + "CosmosDBBuiltInDataContributor", + "CosmosDBBuiltInDataReader", + "CosmosDBOperator", + "CosmosRestoreOperator", + "CostManagementContributor", + "CostManagementReader", + "DataBoxContributor", + "DataBoxReader", + "DataFactoryContributor", + "DataLakeAnalyticsDeveloper", + "DataOperatorForManagedDisks", + "DataPurger", + "DefenderForStorageDataScanner", + "DesktopVirtualizationApplicationGroupContributor", + "DesktopVirtualizationApplicationGroupReader", + "DesktopVirtualizationContributor", + "DesktopVirtualizationHostPoolContributor", + "DesktopVirtualizationHostPoolReader", + "DesktopVirtualizationReader", + "DesktopVirtualizationSessionHostOperator", + "DesktopVirtualizationUser", + "DesktopVirtualizationUserSessionOperator", + "DesktopVirtualizationWorkspaceContributor", + "DesktopVirtualizationWorkspaceReader", + "DevTestLabsUser", + "DeviceUpdateAdministrator", + "DeviceUpdateContentAdministrator", + "DeviceUpdateContentReader", + "DeviceUpdateDeploymentsAdministrator", + "DeviceUpdateDeploymentsReader", + "DeviceUpdateReader", + "DiskBackupReader", + "DiskPoolOperator", + "DiskRestoreOperator", + "DiskSnapshotContributor", + "DnsZoneContributor", + "DocumentDBAccountContributor", + "DomainServicesContributor", + "DomainServicesReader", + "ElasticSANOwner", + "ElasticSANReader", + "ElasticSANVolumeGroupOwner", + "EventGridContributor", + "EventGridDataSender", + "EventGridEventSubscriptionContributor", + "EventGridEventSubscriptionReader", + "FhirDataContributor", + "FhirDataExporter", + "FhirDataImporter", + "FhirDataReader", + "FhirDataWriter", + "GrafanaAdmin", + "GrafanaEditor", + "GrafanaViewer", + "HdInsightClusterOperator", + "HdInsightDomainServicesContributor", + "HierarchySettingsAdministrator", + "IntegrationServiceEnvironmentContributor", + "IntegrationServiceEnvironmentDeveloper", + "IntelligentSystemsAccountContributor", + "IotHubDataContributor", + "IotHubDataReader", + "IotHubRegistryContributor", + "IotHubTwinContributor", + "KeyVaultAdministrator", + "KeyVaultCertificateUser", + "KeyVaultCertificatesOfficer", + "KeyVaultContributor", + "KeyVaultCryptoOfficer", + "KeyVaultCryptoServiceEncryptionUser", + "KeyVaultCryptoServiceReleaseUser", + "KeyVaultCryptoUser", + "KeyVaultDataAccessAdministrator", + "KeyVaultReader", + "KeyVaultSecretsOfficer", + "KeyVaultSecretsUser", + "KubernetesAgentlessOperator", + "KubernetesClusterAzureArcOnboarding", + "KubernetesExtensionContributor", + "LabAssistant", + "LabContributor", + "LabCreator", + "LabOperator", + "LabServicesContributor", + "LabServicesReader", + "LoadTestContributor", + "LoadTestOwner", + "LoadTestReader", + "LogAnalyticsContributor", + "LogAnalyticsReader", + "LogicAppContributor", + "LogicAppOperator", + "LogicAppsStandardContributor", + "LogicAppsStandardDeveloper", + "LogicAppsStandardOperator", + "LogicAppsStandardReader", + "ManagedApplicationContributorRole", + "ManagedApplicationOperatorRole", + "ManagedApplicationsReader", + "ManagedHSMContributor", + "ManagedIdentityContributor", + "ManagedIdentityOperator", + "ManagedServicesRegistrationAssignmentDeleteRole", + "ManagementGroupContributor", + "ManagementGroupReader", + "MediaServicesAccountAdministrator", + "MediaServicesLiveEventsAdministrator", + "MediaServicesMediaOperator", + "MediaServicesPolicyAdministrator", + "MediaServicesStreamingEndpointsAdministrator", + "MicrosoftSentinelAutomationContributor", + "MicrosoftSentinelContributor", + "MicrosoftSentinelPlaybookOperator", + "MicrosoftSentinelReader", + "MicrosoftSentinelResponder", + "MonitoringContributor", + "MonitoringMetricsPublisher", + "MonitoringReader", + "NetworkContributor", + "NewRelicAPMAccountContributor", + "Owner", + "PolicyInsightsDataWriter", + "PrivateDNSZoneContributor", + "QuotaRequestOperator", + "Reader", + "ReaderAndDataAccess", + "RedisCacheContributor", + "RemoteRenderingAdministrator", + "RemoteRenderingClient", + "ReservationPurchaser", + "ReservationsAdministrator", + "ReservationsReader", + "ResourcePolicyContributor", + "RoleBasedAccessControlAdministrator", + "ScheduledPatchingContributor", + "SchedulerJobCollectionsContributor", + "SchemaRegistryContributor", + "SchemaRegistryReader", + "SearchIndexDataContributor", + "SearchIndexDataReader", + "SearchServiceContributor", + "SecurityAdmin", + "SecurityAssessmentContributor", + "SecurityReader", + "ServicesHubOperator", + "SignalRAccessKeyReader", + "SignalRAppServer", + "SignalRRestAPIOwner", + "SignalRRestAPIReader", + "SignalRServiceOwner", + "SignalRWebPubSubContributor", + "SiteRecoveryContributor", + "SiteRecoveryOperator", + "SiteRecoveryReader", + "SpatialAnchorsAccountContributor", + "SpatialAnchorsAccountOwner", + "SpatialAnchorsAccountReader", + "SqlDBContributor", + "SqlManagedInstanceContributor", + "SqlSecurityManager", + "SqlServerContributor", + "StorageAccountBackupContributor", + "StorageAccountContributor", + "StorageAccountKeyOperatorServiceRole", + "StorageBlobDataContributor", + "StorageBlobDataOwner", + "StorageBlobDataReader", + "StorageBlobDelegator", + "StorageFileDataPrivilegedContributor", + "StorageFileDataPrivilegedReader", + "StorageFileDataSMBShareContributor", + "StorageFileDataSMBShareElevatedContributor", + "StorageFileDataSMBShareReader", + "StorageQueueDataContributor", + "StorageQueueDataMessageProcessor", + "StorageQueueDataMessageSender", + "StorageQueueDataReader", + "StorageTableDataContributor", + "StorageTableDataReader", + "StreamAnalyticsQueryTester", + "SupportRequestContributor", + "TagContributor", + "TemplateSpecContributor", + "TemplateSpecReader", + "TrafficManagerContributor", + "UserAccessAdministrator", + "VirtualMachineAdministratorLogin", + "VirtualMachineContributor", + "VirtualMachineDataAccessAdministrator", + "VirtualMachineLocalUserLogin", + "VirtualMachineUserLogin", + "WebPlanContributor", + "WebsiteContributor", + "WindowsAdminCenterAdministratorLogin", + "WorkbookContributor", + "WorkbookReader" + ] + } + } + } + } + } + } + } + } + } + }, + "beforeKeys": [ + "componentFlagValidation", + "bastionNsg", + "appGwNsg", + "aiGatewayNsg", + "routeTable", + "networkLayoutValidation", + "virtualNetworkSubnets", + "virtualNetwork", + "spokeToHubPeering", + "testVmBastionHost", + "firewall", + "defaultRoute", + "testVmUAI", + "testVm", + "natGateway", + "assignTestVmRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorTestVm", + "cse", + "privateDnsZones", + "privateEndpoints", + "aiFoundryUAI", + "aiFoundryStorageAccount", + "aiFoundry", + "bingSearchConnection", + "aiFoundryBingConnection", + "aiFoundryConnectionSearch", + "aiFoundryKnowledgeBaseSearchConnection", + "aiFoundryConnectionInsights", + "aiFoundryConnectionStorage", + "appInsights", + "privateLinkScope", + "privateEndpointPrivateLinkScope", + "containerEnvUAI", + "containerEnv", + "containerRegistryUAI", + "containerRegistry", + "acrTaskAgentPool", + "durableTask", + "durableTaskRoles", + "containerAppsUAI", + "containerApps", + "cosmosUAI", + "cosmosDBAccount", + "cosmosSqlDatabase", + "keyVault", + "logAnalytics", + "searchServiceUAI", + "searchService", + "searchFoundrySharedPrivateLinks", + "searchServiceAIFoundry", + "speechService", + "storageAccount", + "assignExecutorRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorExecutor", + "assignContainerAppRoles", + "assignCrossServiceRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorContainerApps", + "appConfig", + "containerAppsSettings", + "publicIngressM", + "appConfigKeyVaultPopulate", + "cosmosConfigKeyVaultPopulate", + "appConfigPopulate" + ], + "afterKeys": [ + "componentFlagValidation", + "bastionNsg", + "appGwNsg", + "aiGatewayNsg", + "routeTable", + "networkLayoutValidation", + "virtualNetworkSubnets", + "virtualNetwork", + "spokeToHubPeering", + "testVmBastionHost", + "firewall", + "defaultRoute", + "testVmUAI", + "testVm", + "natGateway", + "assignTestVmRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorTestVm", + "cse", + "privateDnsZones", + "privateEndpoints", + "aiFoundryUAI", + "aiFoundryStorageAccount", + "aiFoundry", + "bingSearchConnection", + "aiFoundryBingConnection", + "aiFoundryConnectionSearch", + "aiFoundryKnowledgeBaseSearchConnection", + "aiFoundryConnectionInsights", + "aiFoundryConnectionStorage", + "appInsights", + "privateLinkScope", + "privateEndpointPrivateLinkScope", + "containerEnvUAI", + "containerEnv", + "containerRegistryUAI", + "containerRegistry", + "acrTaskAgentPool", + "durableTask", + "durableTaskRoles", + "containerAppsUAI", + "containerApps", + "cosmosUAI", + "cosmosDBAccount", + "cosmosSqlDatabase", + "keyVault", + "logAnalytics", + "searchServiceUAI", + "searchService", + "searchFoundrySharedPrivateLinks", + "searchServiceAIFoundry", + "speechService", + "storageAccount", + "assignExecutorRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorExecutor", + "assignAgentConsumerRoles", + "assignContainerAppRoles", + "assignCrossServiceRoles", + "assignCosmosDBCosmosDbBuiltInDataContributorContainerApps", + "appConfig", + "containerAppsSettings", + "publicIngressM", + "appConfigKeyVaultPopulate", + "cosmosConfigKeyVaultPopulate", + "appConfigPopulate" + ] + }, + "variables": { + "kind": "object", + "changes": { + "_1._3": { + "kind": "object", + "changes": { + "FoundryAgentConsumer": { + "kind": "added", + "after": { + "guid": "eed3b665-ab3a-47b6-8f48-c9382fb1dad6", + "key": "FoundryAgentConsumer" + } + } + }, + "beforeKeys": [ + "DurableTaskDataContributor", + "AcrDelete", + "AcrImageSigner", + "AcrPull", + "AcrPush", + "AcrQuarantineReader", + "AcrQuarantineWriter", + "AksClusterAdminRole", + "AksClusterMonitoringUser", + "AksClusterUserRole", + "AksContributorRole", + "AksFleetManagerContributorRole", + "AksFleetManagerRBACAdmin", + "AksFleetManagerRBACClusterAdmin", + "AksFleetManagerRBACReader", + "AksFleetManagerRBACWriter", + "AksRBACAdmin", + "AksRBACClusterAdmin", + "AksRBACReader", + "AksRBACWriter", + "AppComplianceAutomationAdministrator", + "AppComplianceAutomationReader", + "AppConfigurationDataOwner", + "AppConfigurationDataReader", + "ApplicationInsightsComponentContributor", + "ApplicationInsightsSnapshotDebugger", + "AttestationContributor", + "AttestationReader", + "AutomationContributor", + "AutomationJobOperator", + "AutomationOperator", + "AutomationRunbookOperator", + "AvereContributor", + "AvereOperator", + "AzureAIAdministrator", + "AzureAIDeveloper", + "AzureAIEnterpriseNetworkConnectionApprover", + "AzureAIInferenceDeploymentOperator", + "AzureAIProjectManager", + "AzureAISafetyEvaluator", + "AzureAIUser", + "AzureApiCenterComplianceManager", + "AzureApiCenterDataReader", + "AzureApiCenterServiceContributor", + "AzureApiCenterServiceReader", + "AzureArcEnabledKubernetesClusterUser", + "AzureArcKubernetesAdmin", + "AzureArcKubernetesClusterAdmin", + "AzureArcKubernetesViewer", + "AzureArcKubernetesWriter", + "AzureConnectedMachineOnboarding", + "AzureConnectedMachineResourceAdministrator", + "AzureConnectedMachineResourceManager", + "AzureConnectedSQLServerOnboarding", + "AzureContainerStorageOperator", + "AzureContainerStorageOwner", + "AzureDigitalTwinsDataOwner", + "AzureDigitalTwinsDataReader", + "AzureEventHubsDataOwner", + "AzureEventHubsDataReceiver", + "AzureEventHubsDataSender", + "AzureFrontDoorDomainContributor", + "AzureFrontDoorDomainReader", + "AzureFrontDoorProfileReader", + "AzureFrontDoorSecretReader", + "AzureMLComputeOperator", + "AzureMLDataScientist", + "AzureMapsDataContributor", + "AzureMapsDataReader", + "AzureRelayListener", + "AzureRelayOwner", + "AzureRelaySender", + "AzureServiceBusDataOwner", + "AzureServiceBusDataReceiver", + "AzureServiceBusDataSender", + "AzureSpringCloudConfigServerContributor", + "AzureSpringCloudConfigServerReader", + "AzureSpringCloudDataReader", + "AzureSpringCloudServiceRegistryContributor", + "AzureSpringCloudServiceRegistryReader", + "AzureStackRegistrationOwner", + "BackupContributor", + "BackupOperator", + "BackupReader", + "BillingReader", + "BizTalkContributor", + "BlueprintContributor", + "BlueprintOperator", + "CarbonOptimizationReader", + "CdnEndpointContributor", + "CdnEndpointReader", + "CdnProfileContributor", + "CdnProfileReader", + "ClassicNetworkContributor", + "ClassicStorageAccountContributor", + "ClassicStorageAccountKeyOperatorServiceRole", + "ClassicVirtualMachineContributor", + "CognitiveServicesContributor", + "CognitiveServicesCustomVisionContributor", + "CognitiveServicesCustomVisionDeployment", + "CognitiveServicesCustomVisionLabeler", + "CognitiveServicesCustomVisionReader", + "CognitiveServicesCustomVisionTrainer", + "CognitiveServicesDataReader", + "CognitiveServicesFaceRecognizer", + "CognitiveServicesMetricsAdvisorAdministrator", + "CognitiveServicesOpenAIContributor", + "CognitiveServicesOpenAIUser", + "CognitiveServicesQnAMakerEditor", + "CognitiveServicesQnAMakerReader", + "CognitiveServicesUsagesReader", + "CognitiveServicesUser", + "ContainerAppReader", + "ContainerAppsConnectedEnvironmentsContributor", + "ContainerAppsConnectedEnvironmentsReader", + "ContainerAppsContributor", + "ContainerAppsJobsContributor", + "ContainerAppsJobsOperator", + "ContainerAppsJobsReader", + "ContainerAppsManagedEnvironmentsContributor", + "ContainerAppsManagedEnvironmentsReader", + "ContainerAppsOperator", + "ContainerAppsSessionExecutor", + "ContainerAppsSessionPoolsContributor", + "ContainerRegistryRepositoryWriter", + "ContainerRegistryTasksContributor", + "ContainerRegistryContributorDataAccessConfigurationAdministrator", + "Contributor", + "CosmosBackupOperator", + "CosmosDBAccountReaderRole", + "CosmosDBBuiltInDataContributor", + "CosmosDBBuiltInDataReader", + "CosmosDBOperator", + "CosmosRestoreOperator", + "CostManagementContributor", + "CostManagementReader", + "DataBoxContributor", + "DataBoxReader", + "DataFactoryContributor", + "DataLakeAnalyticsDeveloper", + "DataOperatorForManagedDisks", + "DataPurger", + "DefenderForStorageDataScanner", + "DesktopVirtualizationApplicationGroupContributor", + "DesktopVirtualizationApplicationGroupReader", + "DesktopVirtualizationContributor", + "DesktopVirtualizationHostPoolContributor", + "DesktopVirtualizationHostPoolReader", + "DesktopVirtualizationReader", + "DesktopVirtualizationSessionHostOperator", + "DesktopVirtualizationUser", + "DesktopVirtualizationUserSessionOperator", + "DesktopVirtualizationWorkspaceContributor", + "DesktopVirtualizationWorkspaceReader", + "DevTestLabsUser", + "DeviceUpdateAdministrator", + "DeviceUpdateContentAdministrator", + "DeviceUpdateContentReader", + "DeviceUpdateDeploymentsAdministrator", + "DeviceUpdateDeploymentsReader", + "DeviceUpdateReader", + "DiskBackupReader", + "DiskPoolOperator", + "DiskRestoreOperator", + "DiskSnapshotContributor", + "DnsZoneContributor", + "DocumentDBAccountContributor", + "DomainServicesContributor", + "DomainServicesReader", + "ElasticSANOwner", + "ElasticSANReader", + "ElasticSANVolumeGroupOwner", + "EventGridContributor", + "EventGridDataSender", + "EventGridEventSubscriptionContributor", + "EventGridEventSubscriptionReader", + "FhirDataContributor", + "FhirDataExporter", + "FhirDataImporter", + "FhirDataReader", + "FhirDataWriter", + "GrafanaAdmin", + "GrafanaEditor", + "GrafanaViewer", + "HdInsightClusterOperator", + "HdInsightDomainServicesContributor", + "HierarchySettingsAdministrator", + "IntegrationServiceEnvironmentContributor", + "IntegrationServiceEnvironmentDeveloper", + "IntelligentSystemsAccountContributor", + "IotHubDataContributor", + "IotHubDataReader", + "IotHubRegistryContributor", + "IotHubTwinContributor", + "KeyVaultAdministrator", + "KeyVaultCertificateUser", + "KeyVaultCertificatesOfficer", + "KeyVaultContributor", + "KeyVaultCryptoOfficer", + "KeyVaultCryptoServiceEncryptionUser", + "KeyVaultCryptoServiceReleaseUser", + "KeyVaultCryptoUser", + "KeyVaultDataAccessAdministrator", + "KeyVaultReader", + "KeyVaultSecretsOfficer", + "KeyVaultSecretsUser", + "KubernetesAgentlessOperator", + "KubernetesClusterAzureArcOnboarding", + "KubernetesExtensionContributor", + "LabAssistant", + "LabContributor", + "LabCreator", + "LabOperator", + "LabServicesContributor", + "LabServicesReader", + "LoadTestContributor", + "LoadTestOwner", + "LoadTestReader", + "LogAnalyticsContributor", + "LogAnalyticsReader", + "LogicAppContributor", + "LogicAppOperator", + "LogicAppsStandardContributor", + "LogicAppsStandardDeveloper", + "LogicAppsStandardOperator", + "LogicAppsStandardReader", + "ManagedApplicationContributorRole", + "ManagedApplicationOperatorRole", + "ManagedApplicationsReader", + "ManagedHSMContributor", + "ManagedIdentityContributor", + "ManagedIdentityOperator", + "ManagedServicesRegistrationAssignmentDeleteRole", + "ManagementGroupContributor", + "ManagementGroupReader", + "MediaServicesAccountAdministrator", + "MediaServicesLiveEventsAdministrator", + "MediaServicesMediaOperator", + "MediaServicesPolicyAdministrator", + "MediaServicesStreamingEndpointsAdministrator", + "MicrosoftSentinelAutomationContributor", + "MicrosoftSentinelContributor", + "MicrosoftSentinelPlaybookOperator", + "MicrosoftSentinelReader", + "MicrosoftSentinelResponder", + "MonitoringContributor", + "MonitoringMetricsPublisher", + "MonitoringReader", + "NetworkContributor", + "NewRelicAPMAccountContributor", + "Owner", + "PolicyInsightsDataWriter", + "PrivateDNSZoneContributor", + "QuotaRequestOperator", + "Reader", + "ReaderAndDataAccess", + "RedisCacheContributor", + "RemoteRenderingAdministrator", + "RemoteRenderingClient", + "ReservationPurchaser", + "ReservationsAdministrator", + "ReservationsReader", + "ResourcePolicyContributor", + "RoleBasedAccessControlAdministrator", + "ScheduledPatchingContributor", + "SchedulerJobCollectionsContributor", + "SchemaRegistryContributor", + "SchemaRegistryReader", + "SearchIndexDataContributor", + "SearchIndexDataReader", + "SearchServiceContributor", + "SecurityAdmin", + "SecurityAssessmentContributor", + "SecurityReader", + "ServicesHubOperator", + "SignalRAccessKeyReader", + "SignalRAppServer", + "SignalRRestAPIOwner", + "SignalRRestAPIReader", + "SignalRServiceOwner", + "SignalRWebPubSubContributor", + "SiteRecoveryContributor", + "SiteRecoveryOperator", + "SiteRecoveryReader", + "SpatialAnchorsAccountContributor", + "SpatialAnchorsAccountOwner", + "SpatialAnchorsAccountReader", + "SqlDBContributor", + "SqlManagedInstanceContributor", + "SqlSecurityManager", + "SqlServerContributor", + "StorageAccountBackupContributor", + "StorageAccountContributor", + "StorageAccountKeyOperatorServiceRole", + "StorageBlobDataContributor", + "StorageBlobDataOwner", + "StorageBlobDataReader", + "StorageBlobDelegator", + "StorageFileDataPrivilegedContributor", + "StorageFileDataPrivilegedReader", + "StorageFileDataSMBShareContributor", + "StorageFileDataSMBShareElevatedContributor", + "StorageFileDataSMBShareReader", + "StorageQueueDataContributor", + "StorageQueueDataMessageProcessor", + "StorageQueueDataMessageSender", + "StorageQueueDataReader", + "StorageTableDataContributor", + "StorageTableDataReader", + "StreamAnalyticsQueryTester", + "SupportRequestContributor", + "TagContributor", + "TemplateSpecContributor", + "TemplateSpecReader", + "TrafficManagerContributor", + "UserAccessAdministrator", + "VirtualMachineAdministratorLogin", + "VirtualMachineContributor", + "VirtualMachineDataAccessAdministrator", + "VirtualMachineLocalUserLogin", + "VirtualMachineUserLogin", + "WebPlanContributor", + "WebsiteContributor", + "WindowsAdminCenterAdministratorLogin", + "WorkbookContributor", + "WorkbookReader" + ], + "afterKeys": [ + "DurableTaskDataContributor", + "AcrDelete", + "AcrImageSigner", + "AcrPull", + "AcrPush", + "AcrQuarantineReader", + "AcrQuarantineWriter", + "AksClusterAdminRole", + "AksClusterMonitoringUser", + "AksClusterUserRole", + "AksContributorRole", + "AksFleetManagerContributorRole", + "AksFleetManagerRBACAdmin", + "AksFleetManagerRBACClusterAdmin", + "AksFleetManagerRBACReader", + "AksFleetManagerRBACWriter", + "AksRBACAdmin", + "AksRBACClusterAdmin", + "AksRBACReader", + "AksRBACWriter", + "AppComplianceAutomationAdministrator", + "AppComplianceAutomationReader", + "AppConfigurationDataOwner", + "AppConfigurationDataReader", + "ApplicationInsightsComponentContributor", + "ApplicationInsightsSnapshotDebugger", + "AttestationContributor", + "AttestationReader", + "AutomationContributor", + "AutomationJobOperator", + "AutomationOperator", + "AutomationRunbookOperator", + "AvereContributor", + "AvereOperator", + "AzureAIAdministrator", + "AzureAIDeveloper", + "AzureAIEnterpriseNetworkConnectionApprover", + "AzureAIInferenceDeploymentOperator", + "AzureAIProjectManager", + "AzureAISafetyEvaluator", + "AzureAIUser", + "FoundryAgentConsumer", + "AzureApiCenterComplianceManager", + "AzureApiCenterDataReader", + "AzureApiCenterServiceContributor", + "AzureApiCenterServiceReader", + "AzureArcEnabledKubernetesClusterUser", + "AzureArcKubernetesAdmin", + "AzureArcKubernetesClusterAdmin", + "AzureArcKubernetesViewer", + "AzureArcKubernetesWriter", + "AzureConnectedMachineOnboarding", + "AzureConnectedMachineResourceAdministrator", + "AzureConnectedMachineResourceManager", + "AzureConnectedSQLServerOnboarding", + "AzureContainerStorageOperator", + "AzureContainerStorageOwner", + "AzureDigitalTwinsDataOwner", + "AzureDigitalTwinsDataReader", + "AzureEventHubsDataOwner", + "AzureEventHubsDataReceiver", + "AzureEventHubsDataSender", + "AzureFrontDoorDomainContributor", + "AzureFrontDoorDomainReader", + "AzureFrontDoorProfileReader", + "AzureFrontDoorSecretReader", + "AzureMLComputeOperator", + "AzureMLDataScientist", + "AzureMapsDataContributor", + "AzureMapsDataReader", + "AzureRelayListener", + "AzureRelayOwner", + "AzureRelaySender", + "AzureServiceBusDataOwner", + "AzureServiceBusDataReceiver", + "AzureServiceBusDataSender", + "AzureSpringCloudConfigServerContributor", + "AzureSpringCloudConfigServerReader", + "AzureSpringCloudDataReader", + "AzureSpringCloudServiceRegistryContributor", + "AzureSpringCloudServiceRegistryReader", + "AzureStackRegistrationOwner", + "BackupContributor", + "BackupOperator", + "BackupReader", + "BillingReader", + "BizTalkContributor", + "BlueprintContributor", + "BlueprintOperator", + "CarbonOptimizationReader", + "CdnEndpointContributor", + "CdnEndpointReader", + "CdnProfileContributor", + "CdnProfileReader", + "ClassicNetworkContributor", + "ClassicStorageAccountContributor", + "ClassicStorageAccountKeyOperatorServiceRole", + "ClassicVirtualMachineContributor", + "CognitiveServicesContributor", + "CognitiveServicesCustomVisionContributor", + "CognitiveServicesCustomVisionDeployment", + "CognitiveServicesCustomVisionLabeler", + "CognitiveServicesCustomVisionReader", + "CognitiveServicesCustomVisionTrainer", + "CognitiveServicesDataReader", + "CognitiveServicesFaceRecognizer", + "CognitiveServicesMetricsAdvisorAdministrator", + "CognitiveServicesOpenAIContributor", + "CognitiveServicesOpenAIUser", + "CognitiveServicesQnAMakerEditor", + "CognitiveServicesQnAMakerReader", + "CognitiveServicesUsagesReader", + "CognitiveServicesUser", + "ContainerAppReader", + "ContainerAppsConnectedEnvironmentsContributor", + "ContainerAppsConnectedEnvironmentsReader", + "ContainerAppsContributor", + "ContainerAppsJobsContributor", + "ContainerAppsJobsOperator", + "ContainerAppsJobsReader", + "ContainerAppsManagedEnvironmentsContributor", + "ContainerAppsManagedEnvironmentsReader", + "ContainerAppsOperator", + "ContainerAppsSessionExecutor", + "ContainerAppsSessionPoolsContributor", + "ContainerRegistryRepositoryWriter", + "ContainerRegistryTasksContributor", + "ContainerRegistryContributorDataAccessConfigurationAdministrator", + "Contributor", + "CosmosBackupOperator", + "CosmosDBAccountReaderRole", + "CosmosDBBuiltInDataContributor", + "CosmosDBBuiltInDataReader", + "CosmosDBOperator", + "CosmosRestoreOperator", + "CostManagementContributor", + "CostManagementReader", + "DataBoxContributor", + "DataBoxReader", + "DataFactoryContributor", + "DataLakeAnalyticsDeveloper", + "DataOperatorForManagedDisks", + "DataPurger", + "DefenderForStorageDataScanner", + "DesktopVirtualizationApplicationGroupContributor", + "DesktopVirtualizationApplicationGroupReader", + "DesktopVirtualizationContributor", + "DesktopVirtualizationHostPoolContributor", + "DesktopVirtualizationHostPoolReader", + "DesktopVirtualizationReader", + "DesktopVirtualizationSessionHostOperator", + "DesktopVirtualizationUser", + "DesktopVirtualizationUserSessionOperator", + "DesktopVirtualizationWorkspaceContributor", + "DesktopVirtualizationWorkspaceReader", + "DevTestLabsUser", + "DeviceUpdateAdministrator", + "DeviceUpdateContentAdministrator", + "DeviceUpdateContentReader", + "DeviceUpdateDeploymentsAdministrator", + "DeviceUpdateDeploymentsReader", + "DeviceUpdateReader", + "DiskBackupReader", + "DiskPoolOperator", + "DiskRestoreOperator", + "DiskSnapshotContributor", + "DnsZoneContributor", + "DocumentDBAccountContributor", + "DomainServicesContributor", + "DomainServicesReader", + "ElasticSANOwner", + "ElasticSANReader", + "ElasticSANVolumeGroupOwner", + "EventGridContributor", + "EventGridDataSender", + "EventGridEventSubscriptionContributor", + "EventGridEventSubscriptionReader", + "FhirDataContributor", + "FhirDataExporter", + "FhirDataImporter", + "FhirDataReader", + "FhirDataWriter", + "GrafanaAdmin", + "GrafanaEditor", + "GrafanaViewer", + "HdInsightClusterOperator", + "HdInsightDomainServicesContributor", + "HierarchySettingsAdministrator", + "IntegrationServiceEnvironmentContributor", + "IntegrationServiceEnvironmentDeveloper", + "IntelligentSystemsAccountContributor", + "IotHubDataContributor", + "IotHubDataReader", + "IotHubRegistryContributor", + "IotHubTwinContributor", + "KeyVaultAdministrator", + "KeyVaultCertificateUser", + "KeyVaultCertificatesOfficer", + "KeyVaultContributor", + "KeyVaultCryptoOfficer", + "KeyVaultCryptoServiceEncryptionUser", + "KeyVaultCryptoServiceReleaseUser", + "KeyVaultCryptoUser", + "KeyVaultDataAccessAdministrator", + "KeyVaultReader", + "KeyVaultSecretsOfficer", + "KeyVaultSecretsUser", + "KubernetesAgentlessOperator", + "KubernetesClusterAzureArcOnboarding", + "KubernetesExtensionContributor", + "LabAssistant", + "LabContributor", + "LabCreator", + "LabOperator", + "LabServicesContributor", + "LabServicesReader", + "LoadTestContributor", + "LoadTestOwner", + "LoadTestReader", + "LogAnalyticsContributor", + "LogAnalyticsReader", + "LogicAppContributor", + "LogicAppOperator", + "LogicAppsStandardContributor", + "LogicAppsStandardDeveloper", + "LogicAppsStandardOperator", + "LogicAppsStandardReader", + "ManagedApplicationContributorRole", + "ManagedApplicationOperatorRole", + "ManagedApplicationsReader", + "ManagedHSMContributor", + "ManagedIdentityContributor", + "ManagedIdentityOperator", + "ManagedServicesRegistrationAssignmentDeleteRole", + "ManagementGroupContributor", + "ManagementGroupReader", + "MediaServicesAccountAdministrator", + "MediaServicesLiveEventsAdministrator", + "MediaServicesMediaOperator", + "MediaServicesPolicyAdministrator", + "MediaServicesStreamingEndpointsAdministrator", + "MicrosoftSentinelAutomationContributor", + "MicrosoftSentinelContributor", + "MicrosoftSentinelPlaybookOperator", + "MicrosoftSentinelReader", + "MicrosoftSentinelResponder", + "MonitoringContributor", + "MonitoringMetricsPublisher", + "MonitoringReader", + "NetworkContributor", + "NewRelicAPMAccountContributor", + "Owner", + "PolicyInsightsDataWriter", + "PrivateDNSZoneContributor", + "QuotaRequestOperator", + "Reader", + "ReaderAndDataAccess", + "RedisCacheContributor", + "RemoteRenderingAdministrator", + "RemoteRenderingClient", + "ReservationPurchaser", + "ReservationsAdministrator", + "ReservationsReader", + "ResourcePolicyContributor", + "RoleBasedAccessControlAdministrator", + "ScheduledPatchingContributor", + "SchedulerJobCollectionsContributor", + "SchemaRegistryContributor", + "SchemaRegistryReader", + "SearchIndexDataContributor", + "SearchIndexDataReader", + "SearchServiceContributor", + "SecurityAdmin", + "SecurityAssessmentContributor", + "SecurityReader", + "ServicesHubOperator", + "SignalRAccessKeyReader", + "SignalRAppServer", + "SignalRRestAPIOwner", + "SignalRRestAPIReader", + "SignalRServiceOwner", + "SignalRWebPubSubContributor", + "SiteRecoveryContributor", + "SiteRecoveryOperator", + "SiteRecoveryReader", + "SpatialAnchorsAccountContributor", + "SpatialAnchorsAccountOwner", + "SpatialAnchorsAccountReader", + "SqlDBContributor", + "SqlManagedInstanceContributor", + "SqlSecurityManager", + "SqlServerContributor", + "StorageAccountBackupContributor", + "StorageAccountContributor", + "StorageAccountKeyOperatorServiceRole", + "StorageBlobDataContributor", + "StorageBlobDataOwner", + "StorageBlobDataReader", + "StorageBlobDelegator", + "StorageFileDataPrivilegedContributor", + "StorageFileDataPrivilegedReader", + "StorageFileDataSMBShareContributor", + "StorageFileDataSMBShareElevatedContributor", + "StorageFileDataSMBShareReader", + "StorageQueueDataContributor", + "StorageQueueDataMessageProcessor", + "StorageQueueDataMessageSender", + "StorageQueueDataReader", + "StorageTableDataContributor", + "StorageTableDataReader", + "StreamAnalyticsQueryTester", + "SupportRequestContributor", + "TagContributor", + "TemplateSpecContributor", + "TemplateSpecReader", + "TrafficManagerContributor", + "UserAccessAdministrator", + "VirtualMachineAdministratorLogin", + "VirtualMachineContributor", + "VirtualMachineDataAccessAdministrator", + "VirtualMachineLocalUserLogin", + "VirtualMachineUserLogin", + "WebPlanContributor", + "WebsiteContributor", + "WindowsAdminCenterAdministratorLogin", + "WorkbookContributor", + "WorkbookReader" + ] + }, + "_containerRuntimeEnv": { + "kind": "changed", + "before": [ + { + "name": "SUBSCRIPTION_ID", + "value": "[subscription().subscriptionId]" + }, + { + "name": "AZURE_RESOURCE_GROUP", + "value": "[resourceGroup().name]" + }, + { + "name": "AZURE_APP_RESOURCE_GROUP", + "value": "[variables('_applicationResourceGroupName')]" + }, + { + "name": "AZURE_NETWORK_RESOURCE_GROUP", + "value": "[variables('_networkResourceGroupName')]" + }, + { + "name": "LOCATION", + "value": "[parameters('location')]" + }, + { + "name": "ENVIRONMENT_NAME", + "value": "[parameters('environmentName')]" + }, + { + "name": "RESOURCE_TOKEN", + "value": "[parameters('resourceToken')]" + }, + { + "name": "RELEASE", + "value": "[variables('_manifest').tag]" + }, + { + "name": "NETWORK_ISOLATION", + "value": "[toLower(string(variables('_networkIsolation')))]" + }, + { + "name": "USE_UAI", + "value": "[toLower(string(variables('_useUAI')))]" + }, + { + "name": "ENABLE_AGENTIC_RETRIEVAL", + "value": "[toLower(string(parameters('enableAgenticRetrieval')))]" + }, + { + "name": "RETRIEVAL_BACKEND", + "value": "[parameters('retrievalBackend')]" + }, + { + "name": "KNOWLEDGE_BASE_NAME", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('knowledgeBaseName'), '')]" + }, + { + "name": "KNOWLEDGE_BASE_ENDPOINT", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), format('https://{0}.search.windows.net', variables('resourceNames').searchServiceName), '')]" + }, + { + "name": "KNOWLEDGE_BASE_CONNECTION_ID", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.CognitiveServices/accounts/{2}/projects/{3}/connections/{4}', subscription().subscriptionId, variables('_foundryResourceGroupName'), variables('resourceNames').aiFoundryAccountName, variables('resourceNames').aiFoundryProjectName, parameters('knowledgeBaseConnectionName')), '')]" + }, + { + "name": "FOUNDRY_IQ_API_VERSION", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqApiVersion'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeRetrievalBillingPlan'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeSourceName'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND", + "value": "[variables('effectiveFoundryIqKnowledgeSourceKind')]" + }, + { + "name": "FOUNDRY_IQ_FILTER_ADD_ON_ENABLED", + "value": "[if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), toLower(string(parameters('foundryIqFilterAddOnEnabled'))), 'false')]" + }, + { + "name": "FOUNDRY_IQ_SECURITY_FIELD_NAME", + "value": "[if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSecurityFieldName'), '')]" + }, + { + "name": "FOUNDRY_IQ_MAX_OUTPUT_DOCUMENTS", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqMaxOutputDocuments'), '')]" + }, + { + "name": "LOG_LEVEL", + "value": "INFO" + }, + { + "name": "ENABLE_CONSOLE_LOGGING", + "value": "true" + }, + { + "name": "AI_FOUNDRY_ACCOUNT_NAME", + "value": "[variables('resourceNames').aiFoundryAccountName]" + }, + { + "name": "AI_FOUNDRY_PROJECT_NAME", + "value": "[variables('resourceNames').aiFoundryProjectName]" + }, + { + "name": "AI_FOUNDRY_ACCOUNT_ENDPOINT", + "value": "[format('https://{0}.cognitiveservices.azure.com/', variables('resourceNames').aiFoundryAccountName)]" + }, + { + "name": "AI_FOUNDRY_OPENAI_ENDPOINT", + "value": "[format('https://{0}.openai.azure.com/', variables('resourceNames').aiFoundryAccountName)]" + }, + { + "name": "APP_INSIGHTS_NAME", + "value": "[variables('resourceNames').appInsightsName]" + }, + { + "name": "CONTAINER_ENV_NAME", + "value": "[variables('resourceNames').containerEnvName]" + }, + { + "name": "CONTAINER_REGISTRY_NAME", + "value": "[variables('resourceNames').containerRegistryName]" + }, + { + "name": "CONTAINER_REGISTRY_LOGIN_SERVER", + "value": "[format('{0}.azurecr.io', variables('resourceNames').containerRegistryName)]" + }, + { + "name": "DATABASE_ACCOUNT_NAME", + "value": "[if(parameters('deployCosmosDb'), variables('resourceNames').dbAccountName, '')]" + }, + { + "name": "DATABASE_NAME", + "value": "[if(parameters('deployCosmosDb'), variables('resourceNames').dbDatabaseName, '')]" + }, + { + "name": "SEARCH_SERVICE_NAME", + "value": "[variables('resourceNames').searchServiceName]" + }, + { + "name": "STORAGE_ACCOUNT_NAME", + "value": "[variables('resourceNames').storageAccountName]" + }, + { + "name": "KEY_VAULT_NAME", + "value": "[variables('resourceNames').keyVaultName]" + }, + { + "name": "APP_CONFIG_NAME", + "value": "[variables('resourceNames').appConfigName]" + }, + { + "name": "APP_RUNTIME_CONFIGURATION_MODE", + "value": "[parameters('appRuntimeConfigurationMode')]" + } + ], + "after": [ + { + "name": "SUBSCRIPTION_ID", + "value": "[subscription().subscriptionId]" + }, + { + "name": "AZURE_RESOURCE_GROUP", + "value": "[resourceGroup().name]" + }, + { + "name": "AZURE_APP_RESOURCE_GROUP", + "value": "[variables('_applicationResourceGroupName')]" + }, + { + "name": "AZURE_NETWORK_RESOURCE_GROUP", + "value": "[variables('_networkResourceGroupName')]" + }, + { + "name": "LOCATION", + "value": "[parameters('location')]" + }, + { + "name": "ENVIRONMENT_NAME", + "value": "[parameters('environmentName')]" + }, + { + "name": "RESOURCE_TOKEN", + "value": "[parameters('resourceToken')]" + }, + { + "name": "RELEASE", + "value": "[variables('_manifest').tag]" + }, + { + "name": "NETWORK_ISOLATION", + "value": "[toLower(string(variables('_networkIsolation')))]" + }, + { + "name": "USE_UAI", + "value": "[toLower(string(variables('_useUAI')))]" + }, + { + "name": "ENABLE_AGENTIC_RETRIEVAL", + "value": "[toLower(string(parameters('enableAgenticRetrieval')))]" + }, + { + "name": "RETRIEVAL_BACKEND", + "value": "[parameters('retrievalBackend')]" + }, + { + "name": "KNOWLEDGE_BASE_NAME", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('knowledgeBaseName'), '')]" + }, + { + "name": "TOOLBOX_ENDPOINT", + "value": "[if(and(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deployAiFoundry')), parameters('deployAfProject')), parameters('deploySearchService')), format('https://{0}.services.ai.azure.com/api/projects/{1}/toolboxes/{2}/mcp?api-version=v1', variables('resourceNames').aiFoundryAccountName, variables('resourceNames').aiFoundryProjectName, parameters('knowledgeBaseName')), '')]" + }, + { + "name": "KNOWLEDGE_BASE_ENDPOINT", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), format('https://{0}.search.windows.net', variables('resourceNames').searchServiceName), '')]" + }, + { + "name": "KNOWLEDGE_BASE_CONNECTION_ID", + "value": "[if(and(and(and(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('deployAiFoundry')), parameters('deployAfProject')), parameters('deploySearchService')), format('/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.CognitiveServices/accounts/{2}/projects/{3}/connections/{4}', subscription().subscriptionId, variables('_foundryResourceGroupName'), variables('resourceNames').aiFoundryAccountName, variables('resourceNames').aiFoundryProjectName, parameters('knowledgeBaseConnectionName')), '')]" + }, + { + "name": "FOUNDRY_IQ_API_VERSION", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqApiVersion'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_RETRIEVAL_BILLING_PLAN", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeRetrievalBillingPlan'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_SOURCE_NAME", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqKnowledgeSourceName'), '')]" + }, + { + "name": "FOUNDRY_IQ_KNOWLEDGE_SOURCE_KIND", + "value": "[variables('effectiveFoundryIqKnowledgeSourceKind')]" + }, + { + "name": "FOUNDRY_IQ_FILTER_ADD_ON_ENABLED", + "value": "[if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), toLower(string(parameters('foundryIqFilterAddOnEnabled'))), 'false')]" + }, + { + "name": "FOUNDRY_IQ_SECURITY_FIELD_NAME", + "value": "[if(and(equals(parameters('retrievalBackend'), 'foundry_iq'), equals(parameters('foundryIqPattern'), 'searchIndex')), parameters('foundryIqSecurityFieldName'), '')]" + }, + { + "name": "FOUNDRY_IQ_MAX_OUTPUT_DOCUMENTS", + "value": "[if(equals(parameters('retrievalBackend'), 'foundry_iq'), parameters('foundryIqMaxOutputDocuments'), '')]" + }, + { + "name": "LOG_LEVEL", + "value": "INFO" + }, + { + "name": "ENABLE_CONSOLE_LOGGING", + "value": "true" + }, + { + "name": "AI_FOUNDRY_ACCOUNT_NAME", + "value": "[variables('resourceNames').aiFoundryAccountName]" + }, + { + "name": "AI_FOUNDRY_PROJECT_NAME", + "value": "[variables('resourceNames').aiFoundryProjectName]" + }, + { + "name": "AI_FOUNDRY_ACCOUNT_ENDPOINT", + "value": "[format('https://{0}.cognitiveservices.azure.com/', variables('resourceNames').aiFoundryAccountName)]" + }, + { + "name": "AI_FOUNDRY_OPENAI_ENDPOINT", + "value": "[format('https://{0}.openai.azure.com/', variables('resourceNames').aiFoundryAccountName)]" + }, + { + "name": "APP_INSIGHTS_NAME", + "value": "[variables('resourceNames').appInsightsName]" + }, + { + "name": "CONTAINER_ENV_NAME", + "value": "[variables('resourceNames').containerEnvName]" + }, + { + "name": "CONTAINER_REGISTRY_NAME", + "value": "[variables('resourceNames').containerRegistryName]" + }, + { + "name": "CONTAINER_REGISTRY_LOGIN_SERVER", + "value": "[format('{0}.azurecr.io', variables('resourceNames').containerRegistryName)]" + }, + { + "name": "DATABASE_ACCOUNT_NAME", + "value": "[if(parameters('deployCosmosDb'), variables('resourceNames').dbAccountName, '')]" + }, + { + "name": "DATABASE_NAME", + "value": "[if(parameters('deployCosmosDb'), variables('resourceNames').dbDatabaseName, '')]" + }, + { + "name": "SEARCH_SERVICE_NAME", + "value": "[variables('resourceNames').searchServiceName]" + }, + { + "name": "STORAGE_ACCOUNT_NAME", + "value": "[variables('resourceNames').storageAccountName]" + }, + { + "name": "KEY_VAULT_NAME", + "value": "[variables('resourceNames').keyVaultName]" + }, + { + "name": "APP_CONFIG_NAME", + "value": "[variables('resourceNames').appConfigName]" + }, + { + "name": "APP_RUNTIME_CONFIGURATION_MODE", + "value": "[parameters('appRuntimeConfigurationMode')]" + } + ] + } + } + } + } + } +} diff --git a/tests/test_app_definition_schema.py b/tests/test_app_definition_schema.py index 0fba5375..46a29e6d 100644 --- a/tests/test_app_definition_schema.py +++ b/tests/test_app_definition_schema.py @@ -245,7 +245,9 @@ def test_trio_profiles_reproduce_current_role_assignments(self) -> None: continue canonical = TRIO_CANONICAL[component["name"]] with self.subTest(component=component["name"], app=canonical): - expected = current[canonical] - {"KeyVaultSecretsUser"} + # Secret access and project-scoped Hosted caller access have + # dedicated provisioning owners, not generic capability profiles. + expected = current[canonical] - {"KeyVaultSecretsUser", "FoundryAgentConsumer"} if component["name"] == "orchestrator": # Native execution state is not the legacy checkpoint store. expected -= {"CosmosDBBuiltInDataContributor"}