Install
$ agentstack add skill-aiappsgbb-awesome-gbb-citadel-spoke-onboarding ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Citadel Spoke Onboarding — Reference Guide
How to connect a GenAI application or Microsoft Foundry project to an existing AI Citadel Governance Hub so that all AI traffic is governed, observable, and compliant.
> Threadlight integration: This skill is the opt-in Phase 7 of > threadlight-deploy. It runs ONLY when SPEC § 11b sets > governance_hub.required: yes (the SPEC field is generic; the AI > Citadel hub is one reference implementation). The base agent deploy > (Phase 5 + 6) lands in the customer's tenant first; this skill > onboards it as a hub spoke afterwards as an additive step. Read > SPEC § 11b for the per-process governance posture (hub endpoint, > access contracts, JWT requirements, secret wiring). > > Threadlight pilots MUST use Option B (Foundry Connection) — see > Consuming the Gateway from Your App below. Option A (Key Vault > secret pull) violates the keyless-by-mandate posture: it requires > the agent to hold an APIM subscription key and read it from KV at > runtime. Option B threads the call through a Foundry APIM connection > so the agent's UAMI is the only credential, and APIM enforces JWT > validation on the project's MI token. If a customer insists on > Option A for a non-threadlight reason, document the deviation in > SPEC § 11b explicitly.
> Source repo: Azure-Samples/ai-hub-gateway-solution-accelerator (branch citadel-v1) > Quick link:
Key Concepts
| Term | Meaning | |------|---------| | Citadel Governance Hub | Central control plane with Azure API Management (APIM) acting as the unified AI gateway. Already deployed — not your concern here. | | Spoke | An isolated workload environment (Foundry project, Container App, Function, etc.) that consumes AI services through the hub gateway. | | Access Contract | A Bicep parameter file (.bicepparam) + optional policy XML declaring what AI services a spoke needs, with what policies. Deployed as IaC. | | Foundry Connection | An APIM-type connection inside an Azure AI Foundry project that routes model calls through the Citadel gateway. | | Service Code | Short acronym mapping a category of AI services to APIM API IDs (e.g. LLM, DOC, SRCH, OAIRT). |
What Gets Created Per Access Contract
| Resource | Naming Pattern | Description | |----------|----------------|-------------| | APIM Product | {code}-{BU}-{UseCase}-{ENV} | One per service code, with attached APIs and policies | | APIM Subscription | {product}-SUB-01 | Subscription with API key | | Key Vault Secrets (optional) | {secretName} | Endpoint URL + API key stored in KV | | Foundry Connection (optional) | {prefix}-{code} | APIM connection for Foundry agents |
Prerequisites (Spoke Side)
| Requirement | Details | |-------------|---------| | Running Citadel Hub | APIM deployed with published APIs matching your apiNameMapping | | Azure CLI + Bicep | Latest version with az deployment sub create support | | Permissions | API Management Service Contributor on APIM RG, Key Vault Secrets Officer on target KV (if used), Contributor on Foundry RG (if using Foundry connections) | | Foundry Project | Must exist if you want APIM connections inside Foundry |
Step-by-Step: Create an Access Contract
1. Scaffold the Contract Folder
Clone or init the accelerator, then follow the pattern contracts///:
# Option A: clone the accelerator
git clone -b citadel-v1 https://github.com/Azure-Samples/ai-hub-gateway-solution-accelerator.git
cd ai-hub-gateway-solution-accelerator/bicep/infra/citadel-access-contracts
# Option B: if using azd
azd init --template Azure-Samples/ai-hub-gateway-solution-accelerator -e my-citadel --branch citadel-v1
# Create contract folder
mkdir -p contracts/myteam-myagent/dev
cd contracts/myteam-myagent/dev
# Copy templates
cp ../../../main.bicepparam main.bicepparam
cp ../../../policies/default-ai-product-policy.xml ai-product-policy.xml
> 📂 Full contract folder structure and module reference: > citadel-access-contracts/ > > ⚠️ Sample contracts were removed from the repo. Use main.bicepparam as your template base.
2. Configure the Parameter File
Edit main.bicepparam:
using '../../../main.bicep'
// ── Hub coordinates (get these from your platform team) ──
param apim = {
subscriptionId: ''
resourceGroupName: ''
name: ''
}
// ── Secret storage ──
param useTargetAzureKeyVault = true // false → credentials in deployment output
param keyVault = {
subscriptionId: ''
resourceGroupName: ''
name: ''
}
// ── Use-case identity ──
param useCase = {
businessUnit: 'MyTeam'
useCaseName: 'MyAgent'
environment: 'DEV' // DEV | TEST | PROD
}
// ── Map service codes → APIM API IDs ──
// ⚠️ Order matters: endpoint secret stores the gateway URL for the FIRST API.
// Put the API matching your SDK first (e.g. azure-openai-api for AzureOpenAI SDK).
param apiNameMapping = {
LLM: ['azure-openai-api', 'universal-llm-api', 'unified-ai-api']
}
// ── Services to onboard ──
param services = [
{
code: 'LLM'
endpointSecretName: 'MYAGENT-LLM-ENDPOINT'
apiKeySecretName: 'MYAGENT-LLM-KEY'
policyXml: loadTextContent('ai-product-policy.xml') // '' → use default
}
]
// ── Foundry integration (optional) ──
param useTargetFoundry = true // false if not using Foundry agents
param foundry = {
subscriptionId: ''
resourceGroupName: ''
accountName: ''
projectName: ''
}
param foundryConfig = {
connectionNamePrefix: '' // empty → auto from useCase naming
deploymentInPath: 'false' // model name in request body
isSharedToAll: false
inferenceAPIVersion: '' // empty → APIM defaults
deploymentAPIVersion: ''
staticModels: []
listModelsEndpoint: ''
getModelEndpoint: ''
deploymentProvider: ''
customHeaders: {}
authConfig: {}
}
3. Customise the Product Policy (Optional)
The default policy includes model restrictions, token limits, and content safety. For custom policies, edit ai-product-policy.xml. Full policy reference: citadel-access-contracts-policy.md
Recommended policy ordering in ``:
Per-model capacity limits (instead of flat subscription-level):
Throttling alerts (in `` section):
("requestedModel", "DefaultModel"))" />
("appId", context.Subscription?.Id ?? "Portal-Admin-Sub"))" />
4. Validate and Deploy
# Preview (what-if)
az deployment sub what-if `
--location `
--template-file ../../../main.bicep `
--parameters main.bicepparam
# Deploy
az deployment sub create `
--name myteam-myagent-dev `
--location `
--template-file ../../../main.bicep `
--parameters main.bicepparam
5. Verify
# Check APIM product
az apim product list `
--resource-group `
--service-name `
--query "[?contains(name, 'MyTeam')].{Name:name, State:state}"
# Check Key Vault secrets (if using KV)
az keyvault secret list `
--vault-name `
--query "[?contains(name, 'MYAGENT')].name"
Consuming the Gateway from Your App
Option A: Key Vault (Traditional Apps — NOT for threadlight pilots)
> Threadlight pilots: do NOT use Option A. Pulling an APIM subscription > key from Key Vault means the agent holds a long-lived secret at > runtime, which violates the keyless-by-mandate posture. Use Option B > (Foundry Connection) below — APIM still authorizes via the project > MI token, and the agent never sees a key. Option A remains documented > for traditional non-Foundry apps that don't have a project-level > connection surface.
> Secret name normalization: The Bicep module lowercases names and replaces > underscores with hyphens. E.g. MYAGENT-LLM-ENDPOINT → myagent-llm-endpoint. > Use the normalized name when retrieving secrets.
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
kv = SecretClient(vault_url="https://.vault.azure.net/", credential=credential)
endpoint = kv.get_secret("myagent-llm-endpoint").value # normalized name
api_key = kv.get_secret("myagent-llm-key").value
# Use with Azure OpenAI SDK (requires azure-openai-api FIRST in apiNameMapping)
from openai import AzureOpenAI
client = AzureOpenAI(azure_endpoint=endpoint, api_key=api_key, api_version="2024-12-01-preview")
response = client.chat.completions.create(model="gpt-5.4-mini", messages=[{"role":"user","content":"Hello"}])
Option B: Foundry Connection (Foundry Agents)
The connectionName/modelName pattern routes LLM calls through the APIM gateway. This works at the agent level — not via raw oai.chat.completions.create().
Hosted Agents (FoundryChatClient):
Set MODEL_DEPLOYMENT_NAME in agent.yaml to connectionName/modelName:
# agent.yaml
environment_variables:
- name: MODEL_DEPLOYMENT_NAME
value: Hub-MyTeam-MyAgent-DEV-LLM/gpt-5.4
The FoundryChatClient in container.py resolves the connection automatically:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["MODEL_DEPLOYMENT_NAME"], # "connectionName/gpt-5.4"
credential=DefaultAzureCredential(),
)
Prompt Agents (PromptAgentDefinition):
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition
from azure.identity import DefaultAzureCredential
client = AIProjectClient(
credential=DefaultAzureCredential(),
endpoint="https://.services.ai.azure.com/api/projects/",
allow_preview=True,
)
# Connection name from access contract output
model_deployment = "Hub-MyTeam-MyAgent-DEV-LLM/gpt-5.4"
agent = client.agents.create_version(
agent_name="my-agent",
definition=PromptAgentDefinition(
model=model_deployment,
instructions="You are a helpful assistant.",
),
)
# Chat via get_openai_client(agent_name=...) + responses.create()
oai = client.get_openai_client(agent_name="my-agent")
response = oai.responses.create(input="Hello", stream=False)
> ⚠️ CRITICAL: connectionName/model does NOT work with raw OpenAI API calls. > Calling oai.chat.completions.create(model="connName/gpt-5.4") returns > 404 DeploymentNotFound. The routing only works through: > - FoundryChatClient(model="connName/model") (hosted agents) > - PromptAgentDefinition(model="connName/model") (prompt agents) > - NOT via oai.chat.completions.create() or oai.responses.create() directly
> isSharedToAll quirk: The REST API ignores isSharedToAll=true on PUT/PATCH > — it always stays false. This does NOT block hosted agent routing (the agent > identity resolves the connection via FoundryChatClient). It may affect prompt > agents depending on how the caller authenticates.
Option C: Direct Output (CI/CD Pipelines)
When not using Key Vault, set useTargetAzureKeyVault = false but still provide a placeholder keyVault object (Bicep validation requires it):
param useTargetAzureKeyVault = false
param keyVault = {
subscriptionId: '00000000-0000-0000-0000-000000000000'
resourceGroupName: 'placeholder'
name: 'placeholder'
}
Retrieve credentials from deployment output:
$output = az deployment sub show `
--name myteam-myagent-dev `
--query properties.outputs.endpoints.value -o json | ConvertFrom-Json
$creds = $output | Where-Object { $_.code -eq 'LLM' }
# $creds.endpoint and $creds.apiKey are available (handle as secrets!)
JWT Authentication (Optional Layer)
When the hub is deployed with entraAuth=true, you can require JWT on top of the API key.
Enable in Product Policy
Add to your ai-product-policy.xml:
Authentication Matrix
| Scenario | Headers Required | Result | |----------|-----------------|--------| | API Key only (JWT disabled) | api-key: {key} | ✅ | | API Key + JWT (JWT enabled) | api-key: {key} + Authorization: Bearer {token} | ✅ | | API Key only (JWT enabled) | api-key: {key} | ❌ 401 | | JWT only (no API Key) | Authorization: Bearer {token} | ❌ 401 |
Acquiring the JWT
Two distinct identities are involved:
- Gateway audience (``): The Entra app registration configured in the hub's APIM. The hub team provides this.
- Spoke client identity: Your app's own service principal or managed identity, which must be granted access to the gateway app role.
Service principal client:
from azure.identity import ClientSecretCredential
credential = ClientSecretCredential(
tenant_id="",
client_id="", # your app's identity
client_secret="" # your app's secret
)
token = credential.get_token("api:///.default").token
# Pass as: Authorization: Bearer {token}
Managed identity client (recommended on Azure):
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
token = credential.get_token("api:///.default").token
> ⚠️ Your spoke identity must be granted the required app role (e.g. Models.Read) > on the gateway app registration. Ask the platform team to assign this via Entra ID. > > Guides: > - JWT Authentication Guide > - JWT Client Identity & Permissions
Custom Identity Provider Override
Access contracts can override gateway JWT defaults per product:
| Variable | Falls Back To (APIM Named Value) | |----------|----------------------------------| | jwtAudience | JWT-AppRegistrationId | | jwtIssuer | JWT-Issuer | | jwtOpenIdConfigUrl | JWT-OpenIdConfigUrl |
App Role Authorization
Require specific Entra app roles (enforced after JWT validation, OR logic):
Available gateway app roles: Task.ReadWrite, Models.Read, MCP.Read, Agent.Read.
Foundry APIM Connection (Standalone)
If you only need to wire a Foundry project to the APIM gateway without a full Access Contract (e.g. the product/subscription already exists), use the foundry-integration/main.bicep template:
cd bicep/infra/foundry-integration
cp main.bicepparam my-connection.bicepparam
# Edit my-connection.bicepparam with your values
az account set --subscription
az deployment group create \
--name foundry-apim-conn \
--resource-group \
--template-file main.bicep \
--parameters my-connection.bicepparam
Key parameters (foundry-integration/main.bicepparam):
| Parameter | Description | |-----------|-------------| | aiFoundryAccountName | Name of the AI Foundry account | | aiFoundryProjectName | Name of the AI Foundry project | | connectionName | Name for the connection (e.g. citadel-hub-connection) | |
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: aiappsgbb
- Source: aiappsgbb/awesome-gbb
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.