昨天我們用 Alice 的費用單,把 Agent 的動作提案、授權決策與工具結果拆開來看。今天先接上 Microsoft Foundry,讓 Python 程式透過 Entra ID 呼叫模型,收到一段文字。
這一步先做好,後面加入工具或權限檢查時,出問題才比較容易找到原因。
完成後,你手上會有一個 Foundry Project、一個模型部署名稱、一個 Project endpoint,以及一次 Python 呼叫的結果。訂閱權限、模型 quota 與 region 可用性都會影響進度,建立資源前先確認這些條件。
下面會先做設定檢查,再看 Python 呼叫,最後對照已保存的雲端紀錄。這三個步驟各自確認不同的事情,連不上時也比較容易找到問題。
Foundry 裡有幾個名字很容易看混。我們先把它們放回程式會用到的位置:
| 名稱 | 用途 | 今天要記住的事 |
|---|---|---|
| Foundry resource | Azure 上層的管理資源 | 建立時會選 resource group 與 region |
| Foundry Project | 組織模型、Agent 與 Project 內的資產 | 本系列使用這一層的 endpoint |
| Model deployment | 一個可以呼叫的模型部署 | 記下它的 deployment name |
| Project endpoint | 程式連到 Project 的網址 | 要包含 /api/projects/ 路徑 |
例如,Project endpoint 的格式是:
https://<foundry-resource>.services.ai.azure.com/api/projects/<project-name>
後面 Python 會用 endpoint 找到 Project,再用 deployment name 選擇要呼叫的模型。兩個值都要填對;只知道模型在 catalog 裡叫什麼,還不夠。
本文使用新 Foundry portal 的 Project。依官方說明,Azure AI Studio、Azure AI Foundry 是舊名稱;classic 的 hub-based project 則是另一套資源模型。舊網址或套件名稱仍可能保留 azure-ai,閱讀時要確認自己用的是哪一條 API 路徑。
官方 Responses API quickstart 以 Project endpoint 連接 Project 範圍的資料、工具與觀測功能。本系列後面還會用到這些能力,所以從這個入口開始。Foundry resource 也不是 Day 01 的業務租戶,工程司的資料隔離仍由應用程式處理。
本文的前置條件是 Azure CLI 2.67.0 以上。先確認版本,再登入並檢查帳號:
az version
az login
az account show --query '{name:name,tenantId:tenantId,user:user.name}' --output table
最後一行會列出 tenant ID 與使用者帳號,供你在本機核對。若要把結果放到文章或 issue,先遮蔽這些資訊。
帳號有多個訂閱時,明確選定本次使用的訂閱:
az account set --subscription '<your-subscription-name-or-id>'
稍後程式使用 AzureCliCredential,透過目前 Azure CLI 的登入身分取得 Entra token。這讓我們能知道 Python 到底用了誰的身分。Day 16 再處理正式工作負載的 Managed Identity;今天先把開發者這條路走通。
如果你已經有獲授權使用的 Project,可以直接選它。需要建立新 Project 時,依下面的 Portal 路徑操作:
magic-panda-security-lab。先確認畫面位於 Project 層級,能看到後續會用到的 Build 入口。

這張圖用來辨認 Project 首頁與導覽位置。畫面保留作者核准公開的頭像、顯示名稱與 Project 別名;能開啟首頁,還不能確認模型推論或資料存取權限。
Used with permission from Microsoft.
Microsoft Foundry 的 AI Agent 攻防實戰 is an independent publication and is neither affiliated with, nor authorized, sponsored, or approved by, Microsoft Corporation.
region 要配合模型與功能支援、quota、資料落地及網路需求選擇。可以先查文末的 Region support,不必照抄別人的區域;同一個模型未必在每個訂閱與區域都能部署。
管理 Azure 資源與呼叫模型,使用的是不同層次的權限。前者是 control plane,例如建立 Project;後者是 data plane,例如執行推論。ARM 的 Contributor 或 Owner 不保證自動包含 Foundry 所需的 data actions。
若 Project 由管理員建立,請依實際操作核對角色:Project 內的 build、test 與推論,可由Foundry User 權限表開始檢查;只需要使用已註冊 Agent endpoint 的人,則可評估範圍較窄的 Foundry Agent Consumer。建立 Project 或 deployment 還要另查管理操作所需的權限。
遇到 403,先核對「哪個身分、在哪個範圍、執行哪個操作」,並記下測試時間。角色剛指派後可能尚未傳播完成。這些資訊比反覆加大角色更有助於找到原因。
官方也提醒 Foundry User、Foundry Owner、Foundry Account Owner 與 Foundry Project Manager 等內建角色曾更名;對應的 role ID 與核心權限未變。如果 Portal 與 CLI 顯示名稱不同,回官方 RBAC 文件核對。
下面是同一項工作的 CLI 路徑。已經透過 Portal 建好 Project,就接著往模型部署做;需要以命令建立的讀者,再使用這組指令。
把 placeholder 換成自己的名稱與區域,--custom-domain 要全域唯一:
az group create \
--name '<resource-group>' \
--location '<region>'
az cognitiveservices account create \
--name '<globally-unique-foundry-resource>' \
--resource-group '<resource-group>' \
--kind AIServices \
--sku S0 \
--location '<region>' \
--custom-domain '<globally-unique-foundry-resource>' \
--allow-project-management
az cognitiveservices account project create \
--name '<globally-unique-foundry-resource>' \
--resource-group '<resource-group>' \
--project-name 'magic-panda-security-lab' \
--location '<region>'
這三個命令依序建立 resource group、允許 Project 管理的 Foundry resource,最後才建立 Project。官方文件指出,--allow-project-management 必須在建立 resource 時設定,之後不能更改。
接著查看 resource 與 Project 的建立狀態:
az cognitiveservices account show \
--name '<globally-unique-foundry-resource>' \
--resource-group '<resource-group>' \
--query properties.provisioningState --output tsv
az cognitiveservices account project show \
--name '<globally-unique-foundry-resource>' \
--resource-group '<resource-group>' \
--project-name 'magic-panda-security-lab' \
--query properties.provisioningState --output tsv
兩次都應得到:
Succeeded
若尚未成功,依建立錯誤檢查角色、region 或 quota。先排除前置條件,再繼續部署模型。
在 Foundry portal 裡,依序選 Discover → Models,找到訂閱與 region 可用、也符合組織政策的模型,再按 Deploy 完成設定。
完成後到 Build → Deployments 查看部署清單,記下 deployment name。

後面 FOUNDRY_MODEL 要填的是部署名稱。圖中的模型名稱與 Preview 標籤保留當時的介面狀態;部署列出來後,還要透過 Python 確認目前身分能否呼叫。
Used with permission from Microsoft.
Microsoft Foundry 的 AI Agent 攻防實戰 is an independent publication and is neither affiliated with, nor authorized, sponsored, or approved by, Microsoft Corporation.
本系列的通用模型統一使用 gpt-6-luna。先在 Foundry 部署這個模型,再把部署名稱填進 FOUNDRY_MODEL。下面假設部署也叫 gpt-6-luna;如果你取了別的名稱,就填自己的部署名稱,程式會照這個設定呼叫。
Microsoft 的模型目錄列出 gpt-6-luna 支援 Responses API、structured outputs 與 tool calling;本文沿用 FoundryChatClient + Agent.run() 的 Project 路徑。模型可用區域與 quota 仍要在自己的訂閱確認,模型名稱出現在目錄裡,不代表部署已經完成。
deployment name 不一定等於 model catalog 的名稱,填錯時即使模型已經部署,也可能找不到它。另有 Instant Access Models 的話,也要分開看待:它仍是 Preview,本文沿用已部署模型的操作路徑。
回 Project welcome screen,複製 Project endpoint。在稍後要執行 Python 的終端機設定:
export FOUNDRY_PROJECT_ENDPOINT='https://<resource>.services.ai.azure.com/api/projects/<project>'
export FOUNDRY_MODEL='gpt-6-luna'
兩行分別告訴程式 Project 在哪裡,以及使用哪個 deployment。Repository 的 .env.example 則只保留 placeholder:
FOUNDRY_PROJECT_ENDPOINT=https://<resource>.services.ai.azure.com/api/projects/<project>
FOUNDRY_MODEL=gpt-6-luna
endpoint 本身不是密碼,但網址包含 resource 與 Project 識別資訊,公開畫面仍要遮蔽。Access token 也不需要列印或存進範例檔。
今天不設定 FOUNDRY_API_KEY,程式會使用前面登入的 Entra 身分。後續要測試「不同身分能做哪些事」,從這裡明確指定 credential 來源,才容易追蹤權限。
GA readiness 頁面提到,API key 支援的是多數「功能面」,並另外列出 evaluations、dataset tab、Content Understanding、agents 與 workflows 等需要 Entra ID 的例外。這裡談的是認證方式;模型在哪些 region 可用,要查另一份區域與 quota 資料。
連線前先檢查環境變數,可以把拼錯網址、漏填部署名稱等問題擋在認證以前。scripts.check_foundry_env 會驗證 endpoint 的 scheme、host、/api/projects/ 路徑、deployment name,以及是否出現禁止的 key 變數。
以下是測試用的三份獨立錯誤設定,不要把它們合併貼進自己的環境檔:
# fixture 1:缺少 FOUNDRY_PROJECT_ENDPOINT
FOUNDRY_MODEL=gpt-6-luna
# fixture 2:不是 Project endpoint
FOUNDRY_PROJECT_ENDPOINT=https://example.openai.azure.com/
FOUNDRY_MODEL=gpt-6-luna
# fixture 3:合法 shape 旁出現禁用 key
FOUNDRY_PROJECT_ENDPOINT=https://magic-panda.services.ai.azure.com/api/projects/security-lab
FOUNDRY_MODEL=gpt-6-luna
FOUNDRY_API_KEY=SYNTH_MAGIC_PANDA_NOT_A_REAL_KEY
第一份缺 endpoint,第二份用了非 Project endpoint,第三份混入合成的 API key。檢查程式遇到錯誤就停止,只回固定的 reason code;一次執行回報第一個錯誤。
下圖將三次檢查並排,確認每種錯誤都被拒絕,而且輸出沒有帶出合成 key 的值。

三個結果分別是 FOUNDRY_PROJECT_ENDPOINT_REQUIRED、FOUNDRY_PROJECT_ENDPOINT_INVALID 與 FOUNDRY_KEY_AUTH_FORBIDDEN。這是本機設定檢查,沒有網路觀測資料。
接著使用前一節的正確設定,從 repository 根目錄進入 day2:
cd day2
uv sync --frozen --extra dev --extra foundry
uv run python -m scripts.check_foundry_env
uv sync --frozen 依現有 lock 安裝套件,dev 包含測試用途的相依套件,foundry 則包含這次雲端程式需要的套件。最後一行先做設定檢查,預期得到:
{
"api_surface": "FoundryChatClient + Agent.run (Project Responses API)",
"authentication": "AzureCliCredential",
"cloud_calls": 0,
"endpoint_redacted": true,
"model_redacted": true,
"status": "configuration_valid"
}
configuration_valid 表示設定符合程式接受的格式。這一步不建立 credential、不取得 token,也不呼叫模型,所以還不能判斷登入與角色是否正確。

合法設定會顯示 AzureCliCredential,endpoint 與模型名稱也已遮蔽。畫面的 Cloud Calls = 0 是程式對這條離線路徑的宣告,沒有網路觀測器替它計數;這張圖不能證明已取得 token 或成功連上 Foundry。
JSON 裡的 cloud_calls=0 是程式對這條離線路徑的宣告,並非網路儀表量到的次數。閱讀證據時要分開看。
今天用 Agent Framework 的 FoundryChatClient 連到 Project endpoint,先回答「目前登入者能否取得模型文字」。這個 Agent 的指令與工具清單放在 Python 程式裡,沒有在 Agent Service 建立持久化 Agent,也沒有測試後面那些會改動帳本的工具。
雲端核心放在 examples/cloud/foundry_quickstart.py。下面節錄呼叫部分,環境驗證、錯誤處理與 JSON 輸出也放在同一個檔案:
import asyncio
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
async def invoke_once() -> object:
with AzureCliCredential(process_timeout=10) as credential:
client = FoundryChatClient(
project_endpoint=config.project_endpoint,
model=config.model,
credential=credential,
)
async with client.project_client, client.client:
client.client.max_retries = 0
client.client.timeout = 30
agent = Agent(
client=client,
instructions="你是大魔術熊貓工程司的連線測試助理。本次不提供任何工具,請直接簡短回答使用者問題。",
tools=[],
default_options={"store": False, "max_tokens": 1024},
)
return await asyncio.wait_for(
agent.run("我有一張台北工作坊的高鐵票,報帳前要準備哪些資料?請簡短回答。"),
timeout=45,
)
result = asyncio.run(invoke_once())
AzureCliCredential 提供目前 Azure CLI 登入身分;FoundryChatClient 把 Agent 的請求送往 Project Responses API。這段刻意指定 tools=[]。雖然程式建立了 Agent(...),它仍是本機程式掌握指令與工具的 Agent,不是已發布到 Foundry Agent Service 的資源。
這次使用者問的是高鐵票報帳前要準備哪些資料。程式沒有要求模型複誦指定的成功句子,驗收也不比對完整答案,只檢查 Agent.run() 的結果是否有非空文字。store=False 不把這筆 response 留作可延續的 Responses 對話;底層 client 關閉自動重試,並設定 request timeout 與輸出上限。
| 放置位置 | 內容 |
|---|---|
Agent.run() 的輸入 |
高鐵票報帳問題 |
| Python 的驗收判斷 | 是否能取得非空文字,沒有註冊工具 |
驗收條件留在程式裡,不傳給模型。None、只有空白的字串、沒有可用文字欄位的物件,都不能拿來算成功。離開區塊時,底層 client、Project client 與 credential 都會關閉。
真正執行這條雲端路徑時,加上 --cloud-smoke:
uv run python -m scripts.check_foundry_env --cloud-smoke
省略這個參數,程式只做前面的離線設定檢查。成功呼叫時,輸出格式如下:
{
"api_surface": "FoundryChatClient + Agent.run (Project Responses API)",
"cloud_calls": 1,
"endpoint_redacted": true,
"http_attempts_observed": null,
"logical_agent_calls": 1,
"model_redacted": true,
"nonempty_text_received": true,
"status": "cloud_smoke_completed"
}
其中 nonempty_text_received=true 表示收到非空文字;endpoint、deployment 與模型原文不會寫進這份公開結果。這足以檢查最小推論路徑是否走通,尚未評估回答品質或安全控制。
cloud_calls=1 與 logical_agent_calls=1 對應程式的一次 Agent.run()。底層 client 的自動重試已關閉;這支 quickstart 仍沒有 HTTP transport observer,因此 http_attempts_observed 保留 null,不把一次邏輯呼叫寫成量測到的一次 HTTP request。
Day 02 的 foundry extra 固定 agent-framework-foundry==1.13.1,lock 解析為 core 1.19.0、OpenAI provider 1.14.4、azure-ai-projects==2.6.1 與 OpenAI Python SDK 3.19.0。可以先檢查版本及傳遞依賴,不需要 Azure credential:
uv run --frozen --extra foundry python -c \
"from importlib.metadata import version; from agent_framework.foundry import FoundryChatClient; print(version('agent-framework-foundry'), version('agent-framework-core'), version('azure-ai-projects'), FoundryChatClient.__name__)"
uv tree --locked --extra foundry
這組 provider 要求 azure-ai-projects>=2.2,<2.7,因此不能搭配原先 V3 選用的 2.7.0。安裝時也會間接帶入 azure-ai-inference==1.0.0b9;Microsoft 已公告這個 beta SDK 於 2026-08-26 退役。
這次跑通的是上述版本組合的 Project Responses 呼叫。它不能說明已退役的套件恢復支援,也沒有驗證該套件的 embedding 功能。
當天的離線測試可以這樣執行:
uv run pytest tests/stages/day02/test_acceptance.py -q
它會檢查合法設定、缺少或錯誤 endpoint、非空的禁用 key、例外訊息是否收斂,以及回應文字的判斷。空字串的 key 不會被誤報成已取得秘密;opaque object 或全空白文字也不會被當成模型回覆。CLI 入口本身同樣包含在測試裡。
這些測試不會替你登入 Azure。要確認雲端連線,仍需另外執行 --cloud-smoke。遇到問題時,可以依錯誤縮小範圍:
| 症狀 | 先檢查 |
|---|---|
CredentialUnavailableError |
是否已執行 az login,Python 是否在同一個使用者環境 |
| 401 | 登入 tenant、CLI session 與 token audience |
| 403 | 目前身分的角色、指派範圍,以及角色是否已傳播 |
| 404 | Project 名稱、endpoint 與 API 路徑 |
| 400/Responses 不受支援 | deployment 是否支援 Responses API;改用 Chat Completions 時應另記為不同路徑 |
| deployment not found | FOUNDRY_MODEL 是否誤填 catalog 名稱 |
| 429/quota/capacity error | region、模型 quota、部署容量與重試節奏 |
| 500 | 先記下部署與 Project 入口,再用已知可用的部署對照;不因管理頁顯示 Succeeded 就認定資料平面可用 |
範例將 credential、401、403、404、429 等錯誤轉成固定 code,其餘收斂為 FOUNDRY_CLOUD_SMOKE_FAILED。公開結果不輸出 SDK exception body,避免一起帶出服務資訊;除錯時依 code 回頭檢查 Portal、RBAC 與 CLI 登入狀態。
2026-09-25,我們用上面的 FoundryChatClient + Agent.run() 程式,在既有 lab 的 gpt-5.6-luna 基準部署完成一次 Project endpoint 呼叫。去識別化紀錄的時間是 2026-09-25T15:16:06Z,可以看到它使用 AzureCliCredential,完成一次呼叫(logical_agent_calls=1)、沒有註冊工具(tools_registered=0),也收到非空文字(nonempty_text_received=true)。
新紀錄保存在主專案的 cloud-validation/keyless/evidence/foundry-project-keyless-smoke-2026-09-25.json。它保存的是這次呼叫的結果,沒有為了產生紀錄再送第二次請求,也沒有留下模型文字、endpoint、部署名稱或 credential。
同一輪診斷也已測過本系列選用的 gpt-6-luna 部署:從 resource Responses 入口能取得非空文字,直接走 Project 入口則回 HTTP 500。也就是說,GPT-6 部署已有成功推論的紀錄;當時尚未跑通的是這個部署的 Project 路徑。
因此,上面的設定範例使用 gpt-6-luna,下面保存的 Project 成功 receipt 則來自 gpt-5.6-luna 基準部署。沿用同一份 Agent Framework 程式,不代表兩個部署的呼叫結果相同。這些紀錄還無法指出 Azure 內部哪個元件出了問題,也不能判斷先前的 Project 錯誤如今是否已解除。
另外,這份正式 receipt 沒有 HTTP observer,http_attempts_observed 仍是 null。診斷程式另外觀察到的 HTTP 次數,沒有寫進這份 receipt。
Repository 也保留一筆 2026-08-04 的 AzureCliCredential 無工具呼叫紀錄,當時使用較早的 Agent Framework Foundry provider。兩筆紀錄各有自己的 SDK 版本與執行時間;下面改用 09-25 的新紀錄,舊檔仍保留供回查。
以下畫面讀取的是已保存、通過 schema 與遮蔽檢查的 2026-09-25 紀錄,不會重新呼叫雲端。

這是 2026-09-25 的 receipt,既有基準部署已完成一次無工具的 keyless Project 呼叫;當時 gpt-6-luna 部署的 Project 對照未通過,resource Responses 入口則已成功。http_attempts_observed=null 表示沒有觀測底層 HTTP 次數。本畫面只讀取既有紀錄,沒有重新呼叫模型。
紀錄保留當時的 SDK 版本與遮蔽狀態,沒有保留模型原文、完整 endpoint、deployment name、credential、response ID 或 token。它是操作者執行腳本後保存的資料,並非 Azure 簽發的執行證明;公開檔案也缺少 response/correlation ID、Activity Log reference 或簽章,無法只靠它獨立驗證當時的身分與服務端執行。
舊紀錄只涵蓋當時的一次最小推論呼叫;新紀錄也只涵蓋既有基準部署的一次無工具呼叫。其他使用者會不會被拒絕、工具能不能越權,都沒有在這裡測到。
依官方 readiness table,Foundry Project、Models core 與 Agents core 列為 GA,Instant Access Models 與 managed compute deployment type 則是 Preview。使用前還要確認所選模型、工具、區域與 evaluator 的狀態。
模型推論費用會隨模型、部署方式與輸入輸出量而變,建立前先確認定價、quota 與組織預算。完成實驗後,可以先列出 resource group 裡的資源:
# 先確認這個 resource group 只包含可刪除的 lab 資源,再執行。
az resource list --resource-group '<resource-group>' --output table
如果整組資源只服務這個 lab,再由 owner 確認後清理。若還有其他人共用的資源,就需要分別處理。
最後,把 keyless 的範圍說清楚:不用 API key,仍然有 token、角色與登入 session。登入錯 tenant、共用開發機或角色開得過大,都不會因為改用 AzureCliCredential 自動解決。
NIST SP 800-207 提醒我們,不應只因網路位置或資產歸屬就給予信任;ISO/IEC 27001:2022 也可用來對照身分與存取管理的責任。今天先把 credential 來源、Project 範圍與操作結果拆開確認,距離完整的 Zero Trust 或合規仍有其他工作。
接下來 Day 03,我們會把有漏洞的 Agent 放進 localhost 與合成資料環境,開始重現攻擊。今天取得的 endpoint 會留給後續雲端整合使用。