AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Citadel Spoke Onboarding

skill-aiappsgbb-awesome-gbb-citadel-spoke-onboarding · by aiappsgbb

>

No reviews yet
0 installs
13 views
0.0% view→install

Install

$ agentstack add skill-aiappsgbb-awesome-gbb-citadel-spoke-onboarding

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-aiappsgbb-awesome-gbb-citadel-spoke-onboarding)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Citadel Spoke Onboarding? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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-ENDPOINTmyagent-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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.