@fortytwoservices/ai-universe-setup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/assets/app.bicep +145 -0
  2. package/assets/docker-compose.yml +50 -0
  3. package/assets/main.bicep +233 -0
  4. package/assets/modules/auditSecret.bicep +28 -0
  5. package/assets/modules/backendApp.bicep +107 -0
  6. package/assets/modules/containerApp.bicep +99 -0
  7. package/assets/modules/containerAppsEnv.bicep +43 -0
  8. package/assets/modules/controlGalaxyApp.bicep +129 -0
  9. package/assets/modules/entraClientSecret.bicep +31 -0
  10. package/assets/modules/innovationJob.bicep +91 -0
  11. package/assets/modules/insights.bicep +29 -0
  12. package/assets/modules/keyVault.bicep +42 -0
  13. package/assets/modules/platformIdentity.bicep +69 -0
  14. package/assets/modules/postgres.bicep +116 -0
  15. package/assets/modules/registry.bicep +38 -0
  16. package/assets/modules/storage.bicep +30 -0
  17. package/bin/setup.mjs +7 -0
  18. package/dist/azure/bicep.d.ts +9 -0
  19. package/dist/azure/bicep.d.ts.map +1 -0
  20. package/dist/azure/bicep.js +57 -0
  21. package/dist/azure/bicep.js.map +1 -0
  22. package/dist/azure/entra.d.ts +7 -0
  23. package/dist/azure/entra.d.ts.map +1 -0
  24. package/dist/azure/entra.js +45 -0
  25. package/dist/azure/entra.js.map +1 -0
  26. package/dist/azure/github.d.ts +5 -0
  27. package/dist/azure/github.d.ts.map +1 -0
  28. package/dist/azure/github.js +28 -0
  29. package/dist/azure/github.js.map +1 -0
  30. package/dist/azure/images.d.ts +15 -0
  31. package/dist/azure/images.d.ts.map +1 -0
  32. package/dist/azure/images.js +53 -0
  33. package/dist/azure/images.js.map +1 -0
  34. package/dist/azure/prerequisites.d.ts +8 -0
  35. package/dist/azure/prerequisites.d.ts.map +1 -0
  36. package/dist/azure/prerequisites.js +56 -0
  37. package/dist/azure/prerequisites.js.map +1 -0
  38. package/dist/azure/prompts.d.ts +10 -0
  39. package/dist/azure/prompts.d.ts.map +1 -0
  40. package/dist/azure/prompts.js +88 -0
  41. package/dist/azure/prompts.js.map +1 -0
  42. package/dist/azure/resource-group.d.ts +2 -0
  43. package/dist/azure/resource-group.d.ts.map +1 -0
  44. package/dist/azure/resource-group.js +8 -0
  45. package/dist/azure/resource-group.js.map +1 -0
  46. package/dist/azure/run.d.ts +2 -0
  47. package/dist/azure/run.d.ts.map +1 -0
  48. package/dist/azure/run.js +87 -0
  49. package/dist/azure/run.js.map +1 -0
  50. package/dist/azure/summary.d.ts +3 -0
  51. package/dist/azure/summary.d.ts.map +1 -0
  52. package/dist/azure/summary.js +13 -0
  53. package/dist/azure/summary.js.map +1 -0
  54. package/dist/constants.d.ts +13 -0
  55. package/dist/constants.d.ts.map +1 -0
  56. package/dist/constants.js +21 -0
  57. package/dist/constants.js.map +1 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +17 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/local/compose.d.ts +5 -0
  63. package/dist/local/compose.d.ts.map +1 -0
  64. package/dist/local/compose.js +40 -0
  65. package/dist/local/compose.js.map +1 -0
  66. package/dist/local/prerequisites.d.ts +3 -0
  67. package/dist/local/prerequisites.d.ts.map +1 -0
  68. package/dist/local/prerequisites.js +60 -0
  69. package/dist/local/prerequisites.js.map +1 -0
  70. package/dist/local/prompts.d.ts +6 -0
  71. package/dist/local/prompts.d.ts.map +1 -0
  72. package/dist/local/prompts.js +23 -0
  73. package/dist/local/prompts.js.map +1 -0
  74. package/dist/local/run.d.ts +2 -0
  75. package/dist/local/run.d.ts.map +1 -0
  76. package/dist/local/run.js +50 -0
  77. package/dist/local/run.js.map +1 -0
  78. package/dist/local/summary.d.ts +2 -0
  79. package/dist/local/summary.d.ts.map +1 -0
  80. package/dist/local/summary.js +16 -0
  81. package/dist/local/summary.js.map +1 -0
  82. package/dist/logo.d.ts +2 -0
  83. package/dist/logo.d.ts.map +1 -0
  84. package/dist/logo.js +8 -0
  85. package/dist/logo.js.map +1 -0
  86. package/dist/ui.d.ts +7 -0
  87. package/dist/ui.d.ts.map +1 -0
  88. package/dist/ui.js +16 -0
  89. package/dist/ui.js.map +1 -0
  90. package/dist/utils.d.ts +10 -0
  91. package/dist/utils.d.ts.map +1 -0
  92. package/dist/utils.js +45 -0
  93. package/dist/utils.js.map +1 -0
  94. package/package.json +49 -0
@@ -0,0 +1,145 @@
1
+ // Patch template — the deployment Fortytwo runs to ship a new kernel image to a
2
+ // customer WITHOUT touching their data or config.
3
+ //
4
+ // This template's deployment graph contains ONLY the container apps and the
5
+ // innovation job. Postgres (all customer data AND all config — providers,
6
+ // models, policies, wisdom, galaxies, connectors, branding, users), the Key
7
+ // Vault secrets, the storage account, and the identity role assignments are
8
+ // referenced as EXISTING resources, never declared here, so a patch can neither
9
+ // recreate nor overwrite them. Customer state survives every image bump.
10
+ //
11
+ // Onboarding (the one-time full provision) uses main.bicep. Patches use this.
12
+ //
13
+ // Deploy:
14
+ // az deployment group create \
15
+ // --resource-group <customer>-rg \
16
+ // --template-file app.bicep \
17
+ // --parameters portalName=<customer> \
18
+ // backendImage=<acr>/ai-universe-backend:<new-tag> \
19
+ // controlGalaxyImage=<acr>/ai-universe-control-galaxy:<new-tag> \
20
+ // entraTenantId=<...> entraAdminClientId=<...> portalOrgDid=<...>
21
+ //
22
+ // ACA rolls a new revision per app; existing revisions drain. No secret values
23
+ // are re-supplied because the secrets already live in the customer Key Vault.
24
+
25
+ targetScope = 'resourceGroup'
26
+
27
+ @description('Short, kebab-case portal name. Must match the original deployment.')
28
+ @minLength(2)
29
+ @maxLength(32)
30
+ param portalName string
31
+
32
+ @description('Azure region. Defaults to the resource group location.')
33
+ param location string = resourceGroup().location
34
+
35
+ @description('New backend image reference (login server + repo + tag).')
36
+ param backendImage string
37
+
38
+ @description('New control-galaxy image reference (login server + repo + tag).')
39
+ param controlGalaxyImage string
40
+
41
+ @description('Entra tenant id (unchanged from onboarding).')
42
+ param entraTenantId string
43
+
44
+ @description('Admin/config-plane Entra app registration client id (unchanged).')
45
+ param entraAdminClientId string
46
+
47
+ @description('Portal organization DID (unchanged).')
48
+ param portalOrgDid string
49
+
50
+ @description('Portal organization display name (unchanged).')
51
+ param portalOrgName string = portalName
52
+
53
+ // Conventional secret names written by main.bicep at onboarding.
54
+ var backendConnectionSecretName = 'postgres-connection-string-backend'
55
+ var controlConnectionSecretName = 'postgres-connection-string-control'
56
+ var auditSecretName = 'audit-signing-key'
57
+ var entraClientSecretName = 'entra-client-secret'
58
+
59
+ // --- Existing resources (read-only; never mutated by this template) --------
60
+
61
+ resource environment 'Microsoft.App/managedEnvironments@2024-03-01' existing = {
62
+ name: '${portalName}-cae'
63
+ }
64
+
65
+ resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
66
+ name: '${portalName}-platform-id'
67
+ }
68
+
69
+ resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
70
+ name: take('${replace(portalName, '-', '')}acr', 50)
71
+ }
72
+
73
+ resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
74
+ // Must match keyVault.bicep's vaultName expression EXACTLY. uniqueString is
75
+ // deterministic per resource group, so this reconstructs the onboarded name.
76
+ name: '${take(portalName, 9)}-kv-${take(uniqueString(resourceGroup().id), 6)}'
77
+ }
78
+
79
+ resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
80
+ name: '${portalName}-insights'
81
+ }
82
+
83
+ // --- Container apps (the only things this template deploys) -----------------
84
+
85
+ module backend 'modules/backendApp.bicep' = {
86
+ name: 'backendApp'
87
+ params: {
88
+ portalName: portalName
89
+ location: location
90
+ environmentId: environment.id
91
+ identityId: identity.id
92
+ acrLoginServer: registry.properties.loginServer
93
+ backendImage: backendImage
94
+ keyVaultUri: keyVault.properties.vaultUri
95
+ databaseSecretName: backendConnectionSecretName
96
+ auditSecretName: auditSecretName
97
+ insightsConnectionString: appInsights.properties.ConnectionString
98
+ entraTenantId: entraTenantId
99
+ entraAdminClientId: entraAdminClientId
100
+ portalOrgDid: portalOrgDid
101
+ }
102
+ }
103
+
104
+ module controlGalaxy 'modules/controlGalaxyApp.bicep' = {
105
+ name: 'controlGalaxyApp'
106
+ params: {
107
+ portalName: portalName
108
+ location: location
109
+ environmentId: environment.id
110
+ identityId: identity.id
111
+ acrLoginServer: registry.properties.loginServer
112
+ controlGalaxyImage: controlGalaxyImage
113
+ backendBaseUrl: 'https://${backend.outputs.fqdn}'
114
+ keyVaultUri: keyVault.properties.vaultUri
115
+ databaseSecretName: controlConnectionSecretName
116
+ auditSecretName: auditSecretName
117
+ entraClientSecretName: entraClientSecretName
118
+ insightsConnectionString: appInsights.properties.ConnectionString
119
+ entraTenantId: entraTenantId
120
+ entraAdminClientId: entraAdminClientId
121
+ portalOrgDid: portalOrgDid
122
+ portalOrgName: portalOrgName
123
+ }
124
+ }
125
+
126
+ // The implementer job is stateless (state lives in the proposal record + the
127
+ // PR it opens), so a patch can safely roll a new implementer image alongside
128
+ // the apps. It references the existing shared environment + identity + vault by
129
+ // name, never recreating them. Keeping the image param default in sync with
130
+ // main.bicep is intentional so a patch and an onboarding produce the same job.
131
+ module innovationJob 'modules/innovationJob.bicep' = {
132
+ name: 'innovationJob'
133
+ params: {
134
+ portalName: portalName
135
+ location: location
136
+ environmentId: environment.id
137
+ identityId: identity.id
138
+ identityClientId: identity.properties.clientId
139
+ keyVaultName: keyVault.name
140
+ }
141
+ }
142
+
143
+ output backendUrl string = 'https://${backend.outputs.fqdn}'
144
+ output controlGalaxyUrl string = 'https://${controlGalaxy.outputs.fqdn}'
145
+ output implementerJobName string = innovationJob.outputs.jobName
@@ -0,0 +1,50 @@
1
+ # Fortytwo AI Universe — local evaluation stack
2
+ # Generated by @fortytwoservices/ai-universe-setup --local
3
+ version: '3.9'
4
+ services:
5
+ backend:
6
+ image: fortytwoacr.azurecr.io/ai-universe-backend:1.0.0
7
+ container_name: '${COMPOSE_PROJECT_NAME:-portal}-backend'
8
+ ports:
9
+ - '6101:6101'
10
+ environment:
11
+ NODE_ENV: development
12
+ DATABASE_URL: 'postgresql://postgres:postgres@postgres:5432/universe'
13
+ DEMO_MOCK_CHAT: 'true'
14
+ AUDIT_SIGNING_KEY: 'local-dev-key-not-for-production'
15
+ depends_on:
16
+ postgres:
17
+ condition: service_healthy
18
+ restart: unless-stopped
19
+
20
+ control-galaxy:
21
+ image: fortytwoacr.azurecr.io/ai-universe-control-galaxy:1.0.0
22
+ container_name: '${COMPOSE_PROJECT_NAME:-portal}-control-galaxy'
23
+ ports:
24
+ - '5174:5174'
25
+ environment:
26
+ BACKEND_BASE_URL: 'http://backend:6101'
27
+ depends_on:
28
+ - backend
29
+ restart: unless-stopped
30
+
31
+ postgres:
32
+ image: postgres:16-alpine
33
+ container_name: '${COMPOSE_PROJECT_NAME:-portal}-postgres'
34
+ ports:
35
+ - '5435:5432'
36
+ environment:
37
+ POSTGRES_USER: postgres
38
+ POSTGRES_PASSWORD: postgres
39
+ POSTGRES_DB: universe
40
+ volumes:
41
+ - postgres_data:/var/lib/postgresql/data
42
+ healthcheck:
43
+ test: ['CMD-SHELL', 'pg_isready -U postgres']
44
+ interval: 5s
45
+ timeout: 5s
46
+ retries: 10
47
+ restart: unless-stopped
48
+
49
+ volumes:
50
+ postgres_data:
@@ -0,0 +1,233 @@
1
+ // Fortytwo AI Universe — Azure provisioning orchestrator (galaxy platform).
2
+ //
3
+ // One deployment = one customer, self-hosted in the customer's tenant
4
+ // (Fortytwo runs the deploy). Provisions, in the customer's resource group:
5
+ // - Container Registry (images imported from the Fortytwo central ACR)
6
+ // - Shared user-assigned identity (AcrPull + Key Vault Secrets User)
7
+ // - Key Vault, Postgres (two databases), App Insights, Storage
8
+ // - Container Apps environment
9
+ // - Backend kernel container app (gateway /v1, MCP /mcp, control-plane /admin/api)
10
+ // - Control galaxy container app (admin SPA + BFF over the backend config API)
11
+ // - Innovation Implementer job (short-lived, on the shared environment)
12
+ //
13
+ // NOT created here (manual one-time setup):
14
+ // - Entra ID app registrations. The admin/config plane uses ONE shared app
15
+ // registration (backend + control galaxy share its audience); the customer's
16
+ // IT admin creates it and supplies the client id + tenant id as parameters.
17
+ // Per-galaxy product registrations are minted by the backend provisioner at
18
+ // "create Galaxy" time, not here.
19
+ // - Image push/import. Fortytwo CI pushes images to the central Fortytwo ACR;
20
+ // the deploy step imports them into this customer ACR (see README).
21
+
22
+ targetScope = 'resourceGroup'
23
+
24
+ @description('Short, kebab-case name for the customer deployment (e.g. "acme-portal"). Used as a prefix for every resource.')
25
+ @minLength(2)
26
+ @maxLength(32)
27
+ param portalName string
28
+
29
+ @description('Azure region. Defaults to the resource group location.')
30
+ param location string = resourceGroup().location
31
+
32
+ @description('Postgres administrator login.')
33
+ param postgresAdmin string = 'portal_admin'
34
+
35
+ @secure()
36
+ @description('Postgres administrator password. Pass via parameter file env var; never commit.')
37
+ param postgresAdminPassword string
38
+
39
+ @secure()
40
+ @description('Audit HMAC signing key shared by the backend and control galaxy. Pass via env var; never commit.')
41
+ param auditSigningKey string
42
+
43
+ @secure()
44
+ @description('Entra app-registration client secret for connector OBO (used by the control galaxy). Pass via env var; never commit.')
45
+ param entraClientSecret string
46
+
47
+ @description('Postgres Flexible Server SKU.')
48
+ @allowed([
49
+ 'Standard_B1ms'
50
+ 'Standard_B2s'
51
+ 'Standard_D2s_v3'
52
+ ])
53
+ param postgresSku string = 'Standard_B1ms'
54
+
55
+ @description('Postgres storage size in GB.')
56
+ param postgresStorageGb int = 32
57
+
58
+ @description('Container Registry SKU.')
59
+ @allowed([
60
+ 'Standard'
61
+ 'Premium'
62
+ ])
63
+ param registrySku string = 'Standard'
64
+
65
+ @description('Fully qualified backend image reference (login server + repo + tag).')
66
+ param backendImage string
67
+
68
+ @description('Fully qualified control-galaxy image reference (login server + repo + tag).')
69
+ param controlGalaxyImage string
70
+
71
+ @description('Entra ID tenant id.')
72
+ param entraTenantId string
73
+
74
+ @description('Admin/config-plane Entra app registration client id (shared audience: backend + control galaxy).')
75
+ param entraAdminClientId string
76
+
77
+ @description('Portal organization DID (identity namespace for agents and audit).')
78
+ param portalOrgDid string
79
+
80
+ @description('Portal organization display name.')
81
+ param portalOrgName string = portalName
82
+
83
+ // --- Foundational resources ------------------------------------------------
84
+
85
+ module insights 'modules/insights.bicep' = {
86
+ name: 'insights'
87
+ params: {
88
+ portalName: portalName
89
+ location: location
90
+ }
91
+ }
92
+
93
+ module storage 'modules/storage.bicep' = {
94
+ name: 'storage'
95
+ params: {
96
+ portalName: portalName
97
+ location: location
98
+ }
99
+ }
100
+
101
+ module keyVault 'modules/keyVault.bicep' = {
102
+ name: 'keyVault'
103
+ params: {
104
+ portalName: portalName
105
+ location: location
106
+ tenantId: entraTenantId
107
+ }
108
+ }
109
+
110
+ module registry 'modules/registry.bicep' = {
111
+ name: 'registry'
112
+ params: {
113
+ portalName: portalName
114
+ location: location
115
+ sku: registrySku
116
+ }
117
+ }
118
+
119
+ // Shared identity must exist (with AcrPull + KV Secrets User) before any
120
+ // container app references it for image pull and secret resolution.
121
+ module identity 'modules/platformIdentity.bicep' = {
122
+ name: 'platformIdentity'
123
+ params: {
124
+ portalName: portalName
125
+ location: location
126
+ registryName: registry.outputs.registryName
127
+ keyVaultName: keyVault.outputs.vaultName
128
+ }
129
+ }
130
+
131
+ module postgres 'modules/postgres.bicep' = {
132
+ name: 'postgres'
133
+ params: {
134
+ portalName: portalName
135
+ location: location
136
+ administratorLogin: postgresAdmin
137
+ administratorPassword: postgresAdminPassword
138
+ sku: postgresSku
139
+ storageGb: postgresStorageGb
140
+ keyVaultName: keyVault.outputs.vaultName
141
+ }
142
+ }
143
+
144
+ // Audit HMAC signing key, shared by backend + control galaxy.
145
+ module auditSecret 'modules/auditSecret.bicep' = {
146
+ name: 'auditSecret'
147
+ params: {
148
+ keyVaultName: keyVault.outputs.vaultName
149
+ auditSigningKey: auditSigningKey
150
+ }
151
+ }
152
+
153
+ // Entra app-registration client secret for connector OBO (control galaxy).
154
+ module entraClientSecretModule 'modules/entraClientSecret.bicep' = {
155
+ name: 'entraClientSecret'
156
+ params: {
157
+ keyVaultName: keyVault.outputs.vaultName
158
+ entraClientSecret: entraClientSecret
159
+ }
160
+ }
161
+
162
+ // --- Compute ---------------------------------------------------------------
163
+
164
+ module caeEnv 'modules/containerAppsEnv.bicep' = {
165
+ name: 'containerAppsEnv'
166
+ params: {
167
+ portalName: portalName
168
+ location: location
169
+ }
170
+ }
171
+
172
+ module backend 'modules/backendApp.bicep' = {
173
+ name: 'backendApp'
174
+ params: {
175
+ portalName: portalName
176
+ location: location
177
+ environmentId: caeEnv.outputs.environmentId
178
+ identityId: identity.outputs.identityId
179
+ acrLoginServer: registry.outputs.loginServer
180
+ backendImage: backendImage
181
+ keyVaultUri: keyVault.outputs.vaultUri
182
+ databaseSecretName: postgres.outputs.backendConnectionSecretName
183
+ auditSecretName: auditSecret.outputs.secretName
184
+ insightsConnectionString: insights.outputs.connectionString
185
+ entraTenantId: entraTenantId
186
+ entraAdminClientId: entraAdminClientId
187
+ portalOrgDid: portalOrgDid
188
+ }
189
+ }
190
+
191
+ module controlGalaxy 'modules/controlGalaxyApp.bicep' = {
192
+ name: 'controlGalaxyApp'
193
+ params: {
194
+ portalName: portalName
195
+ location: location
196
+ environmentId: caeEnv.outputs.environmentId
197
+ identityId: identity.outputs.identityId
198
+ acrLoginServer: registry.outputs.loginServer
199
+ controlGalaxyImage: controlGalaxyImage
200
+ backendBaseUrl: 'https://${backend.outputs.fqdn}'
201
+ keyVaultUri: keyVault.outputs.vaultUri
202
+ databaseSecretName: postgres.outputs.controlConnectionSecretName
203
+ auditSecretName: auditSecret.outputs.secretName
204
+ entraClientSecretName: entraClientSecretModule.outputs.secretName
205
+ insightsConnectionString: insights.outputs.connectionString
206
+ entraTenantId: entraTenantId
207
+ entraAdminClientId: entraAdminClientId
208
+ portalOrgDid: portalOrgDid
209
+ portalOrgName: portalOrgName
210
+ }
211
+ }
212
+
213
+ module innovationJob 'modules/innovationJob.bicep' = {
214
+ name: 'innovationJob'
215
+ params: {
216
+ portalName: portalName
217
+ location: location
218
+ environmentId: caeEnv.outputs.environmentId
219
+ identityId: identity.outputs.identityId
220
+ identityClientId: identity.outputs.clientId
221
+ keyVaultName: keyVault.outputs.vaultName
222
+ }
223
+ }
224
+
225
+ // --- Outputs ---------------------------------------------------------------
226
+
227
+ output backendUrl string = 'https://${backend.outputs.fqdn}'
228
+ output controlGalaxyUrl string = 'https://${controlGalaxy.outputs.fqdn}'
229
+ output registryLoginServer string = registry.outputs.loginServer
230
+ output postgresFqdn string = postgres.outputs.fqdn
231
+ output keyVaultName string = keyVault.outputs.vaultName
232
+ output storageAccountName string = storage.outputs.accountName
233
+ output implementerJobName string = innovationJob.outputs.jobName
@@ -0,0 +1,28 @@
1
+ // Audit HMAC signing key, shared by the backend and the control galaxy.
2
+ // Written here (not in keyVault.bicep) so the @secure() value stays inside a
3
+ // module that references the vault by name param, keeping it out of the
4
+ // orchestration deployment graph as an output.
5
+
6
+ @description('Name of the existing Key Vault to write the secret into.')
7
+ param keyVaultName string
8
+
9
+ @secure()
10
+ @description('Audit HMAC signing key value.')
11
+ param auditSigningKey string
12
+
13
+ @description('Secret name.')
14
+ param secretName string = 'audit-signing-key'
15
+
16
+ resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
17
+ name: keyVaultName
18
+ }
19
+
20
+ resource secret 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
21
+ parent: vault
22
+ name: secretName
23
+ properties: {
24
+ value: auditSigningKey
25
+ }
26
+ }
27
+
28
+ output secretName string = secret.name
@@ -0,0 +1,107 @@
1
+ // Backend kernel as a Container App. This is the headless control plane:
2
+ // it serves the AI gateway (/v1), the MCP hub (/mcp), the galaxy registry +
3
+ // provisioner, the durable audit, identity verification, and the control-plane
4
+ // config API (/admin/api/*). External ingress on 6101 because galaxies reach
5
+ // /v1 with a per-galaxy key, developer tools reach /mcp, and the control galaxy
6
+ // reaches /admin/api with the forwarded operator token, all across the network
7
+ // boundary. minReplicas is 1: the gateway is on the runtime hot path (ADR-0003).
8
+ //
9
+ // Secrets (DATABASE_URL, AUDIT_SIGNING_KEY) resolve from Key Vault over the
10
+ // shared user-assigned identity. Images pull from the customer ACR over the
11
+ // same identity (AcrPull granted in platformIdentity.bicep). No stored secret.
12
+
13
+ @description('Short, kebab-case portal name.')
14
+ param portalName string
15
+
16
+ @description('Azure region.')
17
+ param location string
18
+
19
+ @description('Container Apps managed environment id (containerAppsEnv.bicep).')
20
+ param environmentId string
21
+
22
+ @description('User-assigned managed identity resource id (platformIdentity.bicep).')
23
+ param identityId string
24
+
25
+ @description('ACR login server (e.g. acmeacr.azurecr.io).')
26
+ param acrLoginServer string
27
+
28
+ @description('Fully qualified backend image reference, including tag.')
29
+ param backendImage string
30
+
31
+ @description('Key Vault URI (https://<vault>.vault.azure.net/).')
32
+ param keyVaultUri string
33
+
34
+ @description('Key Vault secret name holding the backend database connection string.')
35
+ param databaseSecretName string
36
+
37
+ @description('Key Vault secret name holding the audit HMAC signing key.')
38
+ param auditSecretName string
39
+
40
+ @description('Application Insights connection string.')
41
+ param insightsConnectionString string
42
+
43
+ @description('Entra tenant id for token validation.')
44
+ param entraTenantId string
45
+
46
+ @description('Admin/config-plane Entra app registration client id (shared audience with the control galaxy).')
47
+ param entraAdminClientId string
48
+
49
+ @description('Portal organization DID (identity namespace for agents and audit).')
50
+ param portalOrgDid string
51
+
52
+ @description('CPU cores.')
53
+ param cpuCores string = '1.0'
54
+
55
+ @description('Memory.')
56
+ param memorySize string = '2Gi'
57
+
58
+ // Domain-specific secrets[] and env[]. These literal arrays stay here (rather
59
+ // than in containerApp.bicep) so the infra contract test can parse the env
60
+ // contract from this module. The shared containerApps scaffolding lives in
61
+ // containerApp.bicep.
62
+ var backendSecrets = [
63
+ {
64
+ name: 'database-url'
65
+ keyVaultUrl: '${keyVaultUri}secrets/${databaseSecretName}'
66
+ identity: identityId
67
+ }
68
+ {
69
+ name: 'audit-signing-key'
70
+ keyVaultUrl: '${keyVaultUri}secrets/${auditSecretName}'
71
+ identity: identityId
72
+ }
73
+ ]
74
+
75
+ var backendEnv = [
76
+ { name: 'NODE_ENV', value: 'production' }
77
+ { name: 'HOST', value: '0.0.0.0' }
78
+ { name: 'PORT', value: '6101' }
79
+ { name: 'DATABASE_URL', secretRef: 'database-url' }
80
+ { name: 'AUDIT_SIGNING_KEY', secretRef: 'audit-signing-key' }
81
+ { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: insightsConnectionString }
82
+ { name: 'ENTRA_TENANT_ID', value: entraTenantId }
83
+ { name: 'BACKEND_API_AUDIENCE', value: 'api://${entraAdminClientId}' }
84
+ { name: 'PORTAL_ORG_DID', value: portalOrgDid }
85
+ ]
86
+
87
+ module backend 'containerApp.bicep' = {
88
+ name: 'backendContainerApp'
89
+ params: {
90
+ portalName: portalName
91
+ appSuffix: 'backend'
92
+ location: location
93
+ environmentId: environmentId
94
+ identityId: identityId
95
+ acrLoginServer: acrLoginServer
96
+ image: backendImage
97
+ port: 6101
98
+ maxReplicas: 3
99
+ cpuCores: cpuCores
100
+ memorySize: memorySize
101
+ secrets: backendSecrets
102
+ env: backendEnv
103
+ }
104
+ }
105
+
106
+ output appName string = backend.outputs.appName
107
+ output fqdn string = backend.outputs.fqdn
@@ -0,0 +1,99 @@
1
+ // Shared Container App scaffolding for the platform's HTTP surfaces (backend
2
+ // kernel, control galaxy). Both wrap this module: they assemble their own
3
+ // domain-specific secrets[] and env[] arrays and pass them in here, so the
4
+ // identity, ACR registry binding, Key-Vault secret wiring, external ingress,
5
+ // and single-revision scale shape live in exactly one place.
6
+ //
7
+ // What stays in the wrapper modules (backendApp.bicep, controlGalaxyApp.bicep):
8
+ // the literal env[] and secrets[] arrays. The infra contract test parses those
9
+ // arrays from the wrapper files, so keeping them there preserves both the
10
+ // env-contract guard and the per-app readability of the config.
11
+
12
+ @description('Short, kebab-case portal name.')
13
+ param portalName string
14
+
15
+ @description('Suffix appended to portalName for the app + container name (e.g. backend, control-galaxy).')
16
+ param appSuffix string
17
+
18
+ @description('Azure region.')
19
+ param location string
20
+
21
+ @description('Container Apps managed environment id (containerAppsEnv.bicep).')
22
+ param environmentId string
23
+
24
+ @description('User-assigned managed identity resource id (platformIdentity.bicep).')
25
+ param identityId string
26
+
27
+ @description('ACR login server (e.g. acmeacr.azurecr.io).')
28
+ param acrLoginServer string
29
+
30
+ @description('Fully qualified container image reference, including tag.')
31
+ param image string
32
+
33
+ @description('Ingress target port.')
34
+ param port int
35
+
36
+ @description('Maximum replica count. minReplicas is fixed at 1 (runtime hot path, ADR-0003).')
37
+ param maxReplicas int
38
+
39
+ @description('CPU cores.')
40
+ param cpuCores string
41
+
42
+ @description('Memory.')
43
+ param memorySize string
44
+
45
+ @description('Key-Vault-backed container secrets ({ name, keyVaultUrl, identity }).')
46
+ param secrets array
47
+
48
+ @description('Container env entries ({ name, value } or { name, secretRef }).')
49
+ param env array
50
+
51
+ resource app 'Microsoft.App/containerApps@2024-03-01' = {
52
+ name: '${portalName}-${appSuffix}'
53
+ location: location
54
+ identity: {
55
+ type: 'UserAssigned'
56
+ userAssignedIdentities: {
57
+ '${identityId}': {}
58
+ }
59
+ }
60
+ properties: {
61
+ managedEnvironmentId: environmentId
62
+ configuration: {
63
+ activeRevisionsMode: 'Single'
64
+ ingress: {
65
+ external: true
66
+ targetPort: port
67
+ transport: 'auto'
68
+ allowInsecure: false
69
+ }
70
+ registries: [
71
+ {
72
+ server: acrLoginServer
73
+ identity: identityId
74
+ }
75
+ ]
76
+ secrets: secrets
77
+ }
78
+ template: {
79
+ containers: [
80
+ {
81
+ name: appSuffix
82
+ image: image
83
+ resources: {
84
+ cpu: json(cpuCores)
85
+ memory: memorySize
86
+ }
87
+ env: env
88
+ }
89
+ ]
90
+ scale: {
91
+ minReplicas: 1
92
+ maxReplicas: maxReplicas
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ output appName string = app.name
99
+ output fqdn string = app.properties.configuration.ingress.fqdn