Skip to content

Commit f7ae5c6

Browse files
OgeonX-Airoot
andauthored
Add identity-first Flex ingress and loop telemetry (#5)
* feat(cloud): add identity-first Flex ingress boundary * test(telemetry): preserve upstream coverage after rebase * fix(ci): update Docker action revisions --------- Co-authored-by: root <root@Kimi.localdomain>
1 parent 667ace5 commit f7ae5c6

18 files changed

Lines changed: 396 additions & 15 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ jobs:
3131
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
3232

3333
- name: Set up Docker Buildx
34-
uses: docker/setup-buildx-action@b5730b14e8b0bc39f62ade545785cb7e6f44b97c # v3
34+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
3535

3636
- name: Build image
37-
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b4b5 # v6
37+
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
3838
with:
3939
context: .
4040
platforms: linux/amd64
@@ -70,7 +70,7 @@ jobs:
7070

7171
- name: Push to GHCR
7272
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
73-
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b4b5 # v6
73+
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
7474
with:
7575
context: .
7676
platforms: linux/amd64

docs/architecture.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,10 @@ and principal IDs remain deployment-orchestration outputs and are not applicatio
3737
- CAS correlation IDs are attached to workflow spans and canonical events preserve W3C trace context.
3838
- Broad Azure SDK and outbound HTTP auto-instrumentation is disabled to avoid capturing prompt or
3939
output content. The application records only explicit boundary spans and safe identifiers.
40+
# Loop execution boundaries
41+
42+
The public HTTP ingress is an Azure Functions Flex Consumption Linux app. It validates the canonical prompt envelope and returns `202` only after handing the message to Azure Queue Storage. It never invokes Foundry, executes tools, or runs sandbox work in the request process. A separately scaled worker owns those long-running operations.
43+
44+
The ingress uses a system-assigned managed identity. Bicep grants Storage Queue Data Contributor on the workload storage account and Azure AI User on the specific Foundry project. No account keys, API keys, or connection-string credentials are materialized. Local development continues to use `DefaultAzureCredential`; Azure environments use `ManagedIdentityCredential`.
45+
46+
Loop spans use W3C trace context and the fixed stages `control_plane`, `worker`, `tool`, `verifier`, and `foundry`. Attributes are limited to correlation, goal, and work-item identifiers; prompts and model output are not telemetry attributes.

docs/operations.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ Build a Linux AMD64 image and pass its immutable image reference to the `contain
3434
The Docker build context excludes local `.env` files and development artifacts. The application does
3535
not consume platform resource IDs; deployment orchestration retains those outputs for RBAC and
3636
operations workflows.
37+
# Flex ingress validation
38+
39+
Build the infrastructure locally before any reviewed deployment:
40+
41+
```powershell
42+
az bicep build --file infra/main.bicep
43+
az deployment group what-if --resource-group <resource-group> --template-file infra/main.bicep --parameters foundryProjectResourceId=<resource-id>
44+
```
45+
46+
Deployment is intentionally not performed by repository validation. A rollout must publish the function package separately, verify the system-assigned principal's project-scoped and storage-scoped role assignments, submit one synthetic envelope, and confirm a queue message and correlated Application Insights spans. Rollback disables ingress or restores the prior function package; storage and identity resources are retained to avoid destructive replacement.

functions/ingress/function_app.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import azure.functions as func
2+
3+
from cas_reference_product.ingress import InvalidIngressRequest, create_worker_message
4+
5+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
6+
7+
8+
@app.route(route="workflows", methods=["POST"])
9+
@app.queue_output(
10+
arg_name="worker_message",
11+
queue_name="%WORK_QUEUE_NAME%",
12+
connection="WORK_QUEUE_STORAGE",
13+
)
14+
def submit_workflow(
15+
request: func.HttpRequest,
16+
worker_message: func.Out[str],
17+
) -> func.HttpResponse:
18+
try:
19+
message = create_worker_message(request.get_body())
20+
except InvalidIngressRequest:
21+
return func.HttpResponse("Invalid request.", status_code=400)
22+
23+
worker_message.set(message)
24+
return func.HttpResponse(status_code=202)

functions/ingress/host.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": "2.0",
3+
"logging": {
4+
"applicationInsights": {
5+
"samplingSettings": {
6+
"isEnabled": true,
7+
"excludedTypes": "Request"
8+
}
9+
}
10+
}
11+
}

functions/ingress/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
azure-functions>=1.23.0
2+
cas-reference-product

infra/main.bicep

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
targetScope = 'resourceGroup'
2+
3+
@description('Short lowercase application name used in resource names.')
4+
param applicationName string = 'casloop'
5+
6+
@description('Deployment environment name.')
7+
@allowed(['dev', 'test', 'staging', 'prod'])
8+
param environment string = 'dev'
9+
10+
@description('Azure region for all regional resources.')
11+
param location string = resourceGroup().location
12+
13+
@description('Foundry project resource ID receiving the agent caller role assignment.')
14+
param foundryProjectResourceId string
15+
16+
var suffix = uniqueString(resourceGroup().id, applicationName, environment)
17+
var foundrySegments = split(foundryProjectResourceId, '/')
18+
var tags = {
19+
application: applicationName
20+
environment: environment
21+
managedBy: 'bicep'
22+
dataClassification: 'internal'
23+
}
24+
25+
module observability 'modules/observability.bicep' = {
26+
name: 'observability'
27+
params: {
28+
name: '${applicationName}-${environment}'
29+
location: location
30+
tags: tags
31+
}
32+
}
33+
34+
module storage 'modules/storage.bicep' = {
35+
name: 'storage'
36+
params: {
37+
name: take('st${applicationName}${environment}${suffix}', 24)
38+
location: location
39+
tags: tags
40+
}
41+
}
42+
43+
module ingress 'modules/function-ingress.bicep' = {
44+
name: 'function-ingress'
45+
params: {
46+
name: '${applicationName}-${environment}-${suffix}'
47+
location: location
48+
storageAccountName: storage.outputs.name
49+
storageAccountId: storage.outputs.id
50+
queueName: storage.outputs.queueName
51+
applicationInsightsConnectionString: observability.outputs.connectionString
52+
tags: tags
53+
}
54+
}
55+
56+
module foundryRbac 'modules/foundry-rbac.bicep' = {
57+
name: 'foundry-rbac'
58+
scope: resourceGroup(foundrySegments[2], foundrySegments[4])
59+
params: {
60+
accountName: foundrySegments[8]
61+
projectName: foundrySegments[10]
62+
principalId: ingress.outputs.principalId
63+
}
64+
}
65+
66+
@description('System-assigned principal used by ingress for queue and Foundry access.')
67+
output ingressPrincipalId string = ingress.outputs.principalId
68+
69+
@description('Ingress function resource ID.')
70+
output ingressResourceId string = ingress.outputs.id

infra/modules/foundry-rbac.bicep

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
targetScope = 'resourceGroup'
2+
3+
param accountName string
4+
param projectName string
5+
param principalId string
6+
7+
resource foundryAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = {
8+
name: accountName
9+
}
10+
11+
resource foundryProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' existing = {
12+
parent: foundryAccount
13+
name: projectName
14+
}
15+
16+
resource foundryUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
17+
name: guid(foundryProject.id, principalId, 'foundry-project-user')
18+
scope: foundryProject
19+
properties: {
20+
principalId: principalId
21+
principalType: 'ServicePrincipal'
22+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '53ca6127-db72-4b80-b1b0-d745d6d5456d')
23+
}
24+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
param name string
2+
param location string
3+
param storageAccountName string
4+
param storageAccountId string
5+
param queueName string
6+
param applicationInsightsConnectionString string
7+
param tags object
8+
9+
resource plan 'Microsoft.Web/serverfarms@2024-04-01' = {
10+
name: 'plan-${name}'
11+
location: location
12+
tags: tags
13+
kind: 'functionapp'
14+
sku: { name: 'FC1', tier: 'FlexConsumption' }
15+
properties: { reserved: true }
16+
}
17+
18+
resource functionApp 'Microsoft.Web/sites@2024-04-01' = {
19+
name: 'func-${name}'
20+
location: location
21+
tags: tags
22+
kind: 'functionapp,linux'
23+
identity: { type: 'SystemAssigned' }
24+
properties: {
25+
serverFarmId: plan.id
26+
httpsOnly: true
27+
publicNetworkAccess: 'Enabled'
28+
siteConfig: {
29+
linuxFxVersion: 'Python|3.12'
30+
minTlsVersion: '1.2'
31+
ftpsState: 'Disabled'
32+
appSettings: [
33+
{ name: 'FUNCTIONS_WORKER_RUNTIME', value: 'python' }
34+
{ name: 'WORK_QUEUE_NAME', value: queueName }
35+
{ name: 'WORK_QUEUE_STORAGE__queueServiceUri', value: 'https://${storageAccountName}.queue.${environment().suffixes.storage}' }
36+
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: applicationInsightsConnectionString }
37+
]
38+
}
39+
}
40+
}
41+
42+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
43+
name: storageAccountName
44+
}
45+
46+
resource queueContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
47+
name: guid(storageAccountId, functionApp.id, 'queue-contributor')
48+
scope: storageAccount
49+
properties: {
50+
principalId: functionApp.identity.principalId
51+
principalType: 'ServicePrincipal'
52+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
53+
}
54+
}
55+
56+
output id string = functionApp.id
57+
output principalId string = functionApp.identity.principalId

infra/modules/observability.bicep

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
param name string
2+
param location string
3+
param tags object
4+
5+
resource workspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
6+
name: 'log-${name}'
7+
location: location
8+
tags: tags
9+
properties: {
10+
retentionInDays: 30
11+
features: { enableLogAccessUsingOnlyResourcePermissions: true }
12+
}
13+
}
14+
15+
resource insights 'Microsoft.Insights/components@2020-02-02' = {
16+
name: 'appi-${name}'
17+
location: location
18+
tags: tags
19+
kind: 'web'
20+
properties: {
21+
Application_Type: 'web'
22+
WorkspaceResourceId: workspace.id
23+
IngestionMode: 'LogAnalytics'
24+
}
25+
}
26+
27+
output connectionString string = insights.properties.ConnectionString

0 commit comments

Comments
 (0)