Skip to content

Add MicroHack module 06_AI_Governance (Agentic AI Governance with APIM) - #458

Merged
Marco Weber (qxsch) merged 6 commits into
microsoft:mainfrom
ajakupov-microsoft:main
Aug 28, 2026
Merged

Add MicroHack module 06_AI_Governance (Agentic AI Governance with APIM)#458
Marco Weber (qxsch) merged 6 commits into
microsoft:mainfrom
ajakupov-microsoft:main

Conversation

@ajakupov-microsoft

Copy link
Copy Markdown
Contributor

Adds a new AI lab module: 06_AI_Governance, adapting the Citadel / Agentic
Governance workshop to the MicroHack format.

Participants deploy an Azure API Management AI gateway in front of Azure AI
Foundry and progressively apply governance controls — model routing, access
contracts with model-level RBAC, token capacity limits, PII handling and
agent-framework integration.

  • 9 challenges (1–6 core, 7–9 optional/instructor-led) with walkthroughs
  • labautomation/ deploys hub + spoke into the single platform-provided
    resource group, resource-group scoped
  • 6 Foundry models sized to fit regional quota for a full subscription of labs
  • Notebooks read config from .env or azd, so no azd up is required

ajakupov-microsoft and others added 6 commits August 27, 2026 16:17
Three defects found by running deploy-lab.ps1 against a live subscription.
Each one alone left the lab unusable for participants.

1. ARM output names are not case-preserving, so Get-OutVal read nothing.
   A template output declared APIM_GATEWAY_URL is returned by Azure as
   'apiM_GATEWAY_URL' (the leading run of capitals is lowercased except the
   last character), and APPLICATIONINSIGHTS_CONNECTION_STRING comes back as
   'applicationinsightS_CONNECTION_STRING'. The outputs collection is a
   case-sensitive dictionary, so the literal ContainsKey() lookup missed all
   52 requested keys. The deployment "succeeded" while every participant RBAC
   grant was skipped and all 42 dashboard credentials were emitted empty.
   Get-OutVal now falls back to a case-insensitive match.

2. Key Vault Secrets Officer had a wrong role definition GUID
   (...eccb2c155090 instead of ...eccb2c155cd7), so both Key Vault grants
   failed with RoleDefinitionDoesNotExist and failed the whole provision.
   All seven role GUIDs were re-verified against the live role definitions.

3. Deployment required the standalone Bicep CLI on the host. Az PowerShell
   shells out to it for -TemplateFile *.bicep and cannot see the az-bundled
   binary in ~/.azure/bin, so deployment died before creating anything. The
   committed infra/resources.json exists precisely to avoid this dependency
   but was never used; it is now preferred, with the .bicep source as
   fallback. Verified byte-identical to a fresh compile of the Bicep.

Also: run-local.ps1 now shims Test-MhhDeploymentFailureRetryable, which the
platform injects but a local run did not have. Without it every failure
looked retryable and a host-local error was retried across all three regions.
Noted that Azure renamed 'Azure AI User' to 'Foundry User' and 'Azure AI
Project Manager' to 'Foundry Project Manager'; matching is by GUID, but the
portal shows the new names when verifying access.

Verified against a live deployment in swedencentral: 15/15 RBAC grants,
42/42 credentials populated, 0 errors, 0 warnings, and re-runs are
idempotent. Gateway smoke test passed - all 6 models listed, chat and
embeddings 200, missing model defaults to gpt-4.1, unsupported model 400,
missing key 401. All 10 notebook azd keys resolve from challenges/workshop.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58
…ai-governance-lab-automation

fix(06_AI_Governance): make lab deployment work end to end
…s expect (#3)

Challenge 1 failed on its very first cell because LLM_BACKEND_CONFIG was
both encoded and shaped wrong.

* Encoding: the output was wrapped in base64(), but notebooks 1 and 8 call
  json.loads() on it directly, raising JSONDecodeError immediately.
  Now emitted as raw JSON via string().

* Schema: the output was a flat model catalogue of
  { name, publisher, endpoint, models } - one entry per model. Notebook 1
  cell 15 requires { backendId, backendType, endpoint, supportedModels },
  so it raised KeyError: 'backendId'. Replaced with the contract documented
  in challenges/bicep/infra/llm-backend-onboarding/main.bicep (lines 30-68):
  a single ai-foundry backend whose supportedModels carries the per-model
  metadata (sku, capacity, modelFormat, modelVersion, retirementDate)
  derived from the aiFoundryModels variable, so the config can no longer
  drift from what is actually deployed.

* Added A2A_FOUNDRY_ACCOUNT_NAME output (spoke account) so notebook 8 has a
  supported override for its "-0" backendId convention, which our
  aif-hub-/aif-spoke- naming does not satisfy.

* Updated the LlmBackendConfig credential note and setup-notebook-env.ps1
  help, both of which still said "base64".

Regenerated infra/resources.json, since deploy-lab.ps1 deploys the compiled
ARM template rather than the .bicep source.

Verified against rg-agentic-gov-dryrun-ajk01 (swedencentral): notebook 1
cells 2 and 15 now run unmodified and list 1 backend / 6 models; gateway
regression clean (models/chat/embeddings 200, no-key 401, bad model 400).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58
)

* fix(06_AI_Governance): emit LLM_BACKEND_CONFIG in the schema notebooks expect

Challenge 1 failed on its very first cell because LLM_BACKEND_CONFIG was
both encoded and shaped wrong.

* Encoding: the output was wrapped in base64(), but notebooks 1 and 8 call
  json.loads() on it directly, raising JSONDecodeError immediately.
  Now emitted as raw JSON via string().

* Schema: the output was a flat model catalogue of
  { name, publisher, endpoint, models } - one entry per model. Notebook 1
  cell 15 requires { backendId, backendType, endpoint, supportedModels },
  so it raised KeyError: 'backendId'. Replaced with the contract documented
  in challenges/bicep/infra/llm-backend-onboarding/main.bicep (lines 30-68):
  a single ai-foundry backend whose supportedModels carries the per-model
  metadata (sku, capacity, modelFormat, modelVersion, retirementDate)
  derived from the aiFoundryModels variable, so the config can no longer
  drift from what is actually deployed.

* Added A2A_FOUNDRY_ACCOUNT_NAME output (spoke account) so notebook 8 has a
  supported override for its "-0" backendId convention, which our
  aif-hub-/aif-spoke- naming does not satisfy.

* Updated the LlmBackendConfig credential note and setup-notebook-env.ps1
  help, both of which still said "base64".

Regenerated infra/resources.json, since deploy-lab.ps1 deploys the compiled
ARM template rather than the .bicep source.

Verified against rg-agentic-gov-dryrun-ajk01 (swedencentral): notebook 1
cells 2 and 15 now run unmodified and list 1 backend / 6 models; gateway
regression clean (models/chat/embeddings 200, no-key 401, bad model 400).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58

* feat(06_AI_Governance): make azd optional for notebook configuration

Participants get RG access and a credentials dashboard, not a provisioned
hackbox - so every tool the notebooks need is a prerequisite we impose on
their own laptop. Requiring azd (plus pwsh, to run setup-notebook-env.ps1)
just to read config was the largest avoidable failure surface in the lab.

Notebooks 1-7 now resolve configuration from os.environ first and fall back
to `azd env get-value`, so both routes work and the existing azd bridge is
unaffected. This follows the workshop's own direction rather than diverging
from it: notebooks 8 and 9 - the newest ones upstream - already use the
os.environ-first / tolerate-missing-azd pattern. Notebooks 1-7 were simply
the older, brittle variant that hard-raised on a missing azd.

The .env loader is deliberately dependency-free (a ~15 line parser walking
up from cwd) rather than python-dotenv. An early version used dotenv and
failed silently when the package was absent, producing a confusing missing
-key error; the error now also reports whether a .env was found at all.

Notably this fixes challenge 8, which was quietly broken: it resolves the
agent-hosting account by looking for a backendId ending in "-0", which our
aif-hub-*/aif-spoke-* naming never produces. Its documented override reads
os.environ only, so the azd bridge could not supply it. Adds the
A2aFoundryAccountName dashboard credential and documents the override.

Manual entry from the Azure Portal was considered and rejected:
LLM_BACKEND_CONFIG is a synthesized JSON document that appears nowhere in
the portal, so it cannot be copied from there - and a hand-written .env
would still have required azd to read it.

- .env.template documents all 10 notebook keys against their exact dashboard
  credential names (verified: no key the notebooks read is missing)
- README, challenge-01 Part A, per-challenge troubleshooting rows, the nine
  walkthrough solutions and labautomation/README updated; "unmodified
  notebooks" claims corrected

Verified against rg-agentic-gov-dryrun-ajk01 on system Python 3.9 with no
third-party packages: config resolves with azd absent from PATH (.env only)
and with .env absent (azd only). .env stays gitignored.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58
…cy (#5)

The lab deployed every chat/embedding model at capacity 100 (100K TPM). With
labsPerSubscription: 8 that costs 800 units per model per region against a
typical 1000-unit GlobalStandard quota. Measured against the live subscription
in swedencentral, three models could not have fitted 8 labs:

  text-embedding-3-large   300 free vs 800 needed
  gpt-5.4-mini             700 free vs 800 needed
  Mistral-Large-3          700 free vs 800 needed

and gpt-4.1/gpt-5.2 had exactly zero margin. Labs would have deployed fine
until roughly the fourth attendee and then failed mid-event.

Introduce a modelCapacity parameter (default 20) instead of the hardcoded 100,
so a full subscription costs 160 units per model. Phi-4 stays at 1. Verified by
redeploying the live dry-run RG: scale-down succeeded, all six deployments now
report capacity 20, and the gateway smoke test still passes (models 200, chat
200 across gpt-4.1/gpt-5.4-mini/Mistral-Large-3, default-model routing 200,
embeddings 200, unknown model 400, missing key 401). Tightest model now has
380 free vs 160 needed.

Also document that challenges 2/3/5/6 require challenge 1 to have been *run*,
not merely for the .env to be populated. Their generated product policies
include APIM policy fragments (set-llm-requested-model, validate-model-access,
...) that are created by notebook 1's llm-policy-fragments.bicep. The hub
template deliberately ships only the minimal get-available-models fragment, so
starting at challenge 3 fails with "Policy fragment not found". The existing
prerequisite links pointed only at Challenge 1 Part A (environment setup).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58
The retirementDate values in aiFoundryModels were hardcoded and stale. They are
surfaced to participants through LLM_BACKEND_CONFIG.supportedModels, so they
were actively misleading: gpt-5.4-mini claimed 2026-09-30, i.e. 33 days from
now, which would look like the lab was about to break mid-event. Azure actually
reports 2027-09-21 for that model.

Corrected all six against live `az cognitiveservices model list` data for the
exact pinned versions, and verified every value matches:

  gpt-4.1                 2026-10-14 -> 2027-04-14
  gpt-5.4-mini            2026-09-30 -> 2027-09-21
  gpt-5.2                 2027-02-05 -> 2027-06-08
  text-embedding-3-large  2027-04-14 -> 2028-02-09
  Mistral-Large-3         2099-12-30 -> 2099-12-31
  Phi-4                   2099-10-14 -> 2099-12-31

The nearest real retirement is now gpt-4.1 in April 2027, so no model retires
anywhere near the event.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 05a828d3-c050-4705-9340-6c65646dfb58
@ajakupov-microsoft

Copy link
Copy Markdown
Contributor Author

please accept: @microsoft-github-policy-service agree company="Microsoft"

@qxsch
Marco Weber (qxsch) merged commit 4f8417d into microsoft:main Aug 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants