@aifabrix/builder 2.57.0 → 2.59.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 (106) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/init.js +4 -17
  3. package/lib/agent-kit/session-report.js +15 -10
  4. package/lib/agent-kit/setup-auth.js +131 -0
  5. package/lib/agent-kit/setup-context.js +118 -0
  6. package/lib/agent-kit/setup-github.js +70 -0
  7. package/lib/agent-kit/setup-managed.js +79 -0
  8. package/lib/agent-kit/setup-process.js +42 -0
  9. package/lib/agent-kit/setup-session.js +53 -0
  10. package/lib/agent-kit/setup-state.js +52 -0
  11. package/lib/agent-kit/setup-workspace.js +113 -0
  12. package/lib/agent-kit/setup.js +84 -0
  13. package/lib/agent-kit/start.js +4 -3
  14. package/lib/agent-kit/tmux-sessions.js +20 -3
  15. package/lib/api/dev.api.js +1 -1
  16. package/lib/api/miso-health.api.js +26 -0
  17. package/lib/api/types/dev.types.js +3 -3
  18. package/lib/app/deploy.js +0 -102
  19. package/lib/app/push.js +98 -41
  20. package/lib/build/standard-docker-build.js +13 -11
  21. package/lib/cli/index.js +4 -0
  22. package/lib/cli/setup-app.js +16 -5
  23. package/lib/cli/setup-dev-path-commands.js +53 -2
  24. package/lib/cli/setup-dev.js +7 -4
  25. package/lib/cli/setup-environment.js +17 -4
  26. package/lib/cli/setup-help.js +86 -0
  27. package/lib/cli/setup-onboarding.js +37 -0
  28. package/lib/cli/setup-utility-repair.js +7 -5
  29. package/lib/commands/agent-kit-help.js +21 -7
  30. package/lib/commands/agent-kit-sessions.js +126 -0
  31. package/lib/commands/agent-kit-setup.js +44 -0
  32. package/lib/commands/agent-kit.js +5 -79
  33. package/lib/commands/dev-init.js +21 -23
  34. package/lib/commands/dev-refresh.js +2 -2
  35. package/lib/commands/dev-registry.js +155 -0
  36. package/lib/commands/dev-show-display.js +10 -2
  37. package/lib/commands/governance-verify-external.js +12 -5
  38. package/lib/commands/repair-datasource.js +9 -9
  39. package/lib/commands/repair.js +45 -8
  40. package/lib/commands/setup-image-refresh.js +26 -0
  41. package/lib/commands/upload-qualification-client.js +56 -11
  42. package/lib/commands/upload-qualification-poll.js +51 -22
  43. package/lib/commands/wizard-dataplane.js +16 -52
  44. package/lib/core/config-attach-extensions.js +4 -1
  45. package/lib/datasource/datasource-validate-summary.js +8 -5
  46. package/lib/datasource/sync-defaults.js +53 -0
  47. package/lib/deployment/installation/azure-infra-stage.js +50 -9
  48. package/lib/deployment/installation/azure-preflight.js +63 -7
  49. package/lib/deployment/installation/azure-readiness.js +59 -7
  50. package/lib/deployment/installation/environment-auth.js +60 -20
  51. package/lib/deployment/installation/environment-stage.js +9 -5
  52. package/lib/deployment/installation/infra-catalog.js +57 -5
  53. package/lib/deployment/installation/infra-manifest.js +22 -2
  54. package/lib/deployment/installation/infra-projection.js +14 -3
  55. package/lib/deployment/installation/registry-auth-mode.js +121 -0
  56. package/lib/deployment/installation/stage-input.js +21 -6
  57. package/lib/deployment/marketplace-environment.js +26 -1
  58. package/lib/generator/builders.js +8 -9
  59. package/lib/generator/index.js +23 -12
  60. package/lib/integration-definition/apply.js +2 -2
  61. package/lib/internal/fs-real-sync.js +4 -1
  62. package/lib/programmatic/bearer-auth.js +3 -62
  63. package/lib/programmatic/builder-api-startup.js +51 -0
  64. package/lib/programmatic/builder-miso-runtime.js +167 -0
  65. package/lib/programmatic/openapi-descriptions.js +3 -3
  66. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  67. package/lib/programmatic/openapi-spec.js +2 -1
  68. package/lib/programmatic/route-handler-map.js +3 -2
  69. package/lib/schema/environment-deploy-request.schema.json +1 -1
  70. package/lib/schema/external-datasource.schema.json +20 -5
  71. package/lib/schema/infra-parameter.schema.json +1 -1
  72. package/lib/schema/infra.parameter.yaml +2 -1
  73. package/lib/schema/infrastructure-schema.json +200 -3
  74. package/lib/schema/installation-environment.schema.json +1 -1
  75. package/lib/schema/wizard-config.schema.json +1 -1
  76. package/lib/utils/app-register-config.js +2 -4
  77. package/lib/utils/build-resolve-image.js +2 -21
  78. package/lib/utils/cli-utils.js +8 -1
  79. package/lib/utils/config-paths.js +24 -29
  80. package/lib/utils/config-registry-preference.js +113 -0
  81. package/lib/utils/dev-init-ssh-merge.js +5 -4
  82. package/lib/utils/dev-user-groups.js +2 -1
  83. package/lib/utils/docker-build.js +53 -19
  84. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  85. package/lib/utils/env-map.js +23 -6
  86. package/lib/utils/error-formatters/validation-errors.js +25 -5
  87. package/lib/utils/help-builder.js +4 -3
  88. package/lib/utils/image-tags.js +88 -0
  89. package/lib/utils/promote-dev-docker-image.js +6 -0
  90. package/lib/utils/registry-credentials.js +236 -0
  91. package/lib/utils/resolve-docker-image-ref.js +78 -9
  92. package/lib/utils/secrets-helpers.js +9 -1
  93. package/lib/utils/token-manager.js +29 -36
  94. package/lib/utils/variable-transformer.js +2 -4
  95. package/package.json +4 -2
  96. package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
  97. package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
  98. package/templates/agent-kit/skills/shared/hosts.md +23 -5
  99. package/templates/agent-kit/workspace/README.md +14 -1
  100. package/templates/applications/builder-api/application.yaml +1 -1
  101. package/templates/applications/dataplane/application.yaml +1 -1
  102. package/templates/applications/dataplane/env.template +1 -1
  103. package/templates/applications/miso-controller/application.yaml +1 -1
  104. package/templates/applications/miso-controller/env.template +1 -1
  105. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  106. package/templates/marketplace/main.json +440 -82
@@ -20,6 +20,9 @@ const LICENSE_EDITIONS = Object.freeze(['COMMUNITY', 'STANDARD', 'ENTERPRISE']);
20
20
  const DEFAULT_LICENSE_EDITION = 'COMMUNITY';
21
21
  const ONBOARDING_MODES = Object.freeze(['saasDirect', 'customerOwned']);
22
22
  const DEFAULT_ONBOARDING_MODE = 'saasDirect';
23
+ const ACR_AUTH_MODES = Object.freeze(['auto', 'mi', 'credentials']);
24
+ const DEFAULT_ACR_AUTH = 'auto';
25
+ const AZURE_LOCATION = /^[a-z0-9]{2,40}$/;
23
26
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
27
 
25
28
  /**
@@ -36,6 +39,45 @@ function normalizeLicenseEdition(value) {
36
39
  throw new Error(`License edition must be one of ${LICENSE_EDITIONS.join(', ')} or an Azure Marketplace subscription ID`);
37
40
  }
38
41
 
42
+ /**
43
+ * Normalizes requested ACR image-pull auth: auto (default), mi, or credentials.
44
+ * `admin` (and its spellings) is a deprecated alias for `credentials`.
45
+ * @param {string} value - Raw value
46
+ * @returns {string} Canonical mode
47
+ */
48
+ function normalizeAcrAuth(value) {
49
+ const raw = String(value ?? '').trim();
50
+ const aliases = {
51
+ auto: 'auto',
52
+ mi: 'mi',
53
+ 'managed-identity': 'mi',
54
+ managedidentity: 'mi',
55
+ credentials: 'credentials',
56
+ admin: 'credentials',
57
+ adminuser: 'credentials',
58
+ 'admin-user': 'credentials',
59
+ password: 'credentials'
60
+ };
61
+ const mode = aliases[raw.toLowerCase()];
62
+ if (!mode) {
63
+ throw new Error(`ACR auth must be one of ${ACR_AUTH_MODES.join(', ')} (deprecated alias: admin = credentials)`);
64
+ }
65
+ return mode;
66
+ }
67
+
68
+ /**
69
+ * Validates an Azure region name (for example swedencentral).
70
+ * @param {string} value - Raw value
71
+ * @returns {string} Lower-case region
72
+ */
73
+ function normalizeAzureLocation(value) {
74
+ const location = String(value ?? '').trim().toLowerCase();
75
+ if (!AZURE_LOCATION.test(location)) {
76
+ throw new Error(`--location must be an Azure region name such as swedencentral (letters and digits only); got "${value}"`);
77
+ }
78
+ return location;
79
+ }
80
+
39
81
  const PARAMETER_CATALOG = Object.freeze({
40
82
  infrastructureNameOverride: { kind: 'file', source: 'user', description: 'Optional infrastructure-name override; empty uses generated naming.' },
41
83
  location: { kind: 'file', source: 'user', required: true, description: 'Azure region for shared infrastructure.' },
@@ -50,11 +92,14 @@ const PARAMETER_CATALOG = Object.freeze({
50
92
  keycloakVerifyAudience: { kind: 'file', source: 'templateDefault', description: 'Enable Keycloak audience verification.' },
51
93
  encryptionKey: { kind: 'secretEnv', env: 'ENCRYPTION_KEY', required: true, description: 'Application/data encryption key.' },
52
94
  acrLoginServer: { kind: 'acrLookup', field: 'loginServer', description: 'Resolved from acrName; not entered separately.' },
53
- acrAdminUsername: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Unused: Azure web apps pull images with their managed identities.' },
54
- acrAdminPassword: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Unused: Azure image pull never uses a registry password.' },
55
- acrResourceId: { kind: 'acrLookup', field: 'id', description: 'Resolved from acrName; both web app identities receive AcrPull on it and the deployment fails if the grant fails.' },
56
- deployRoleAssignments: { kind: 'file', source: 'templateDefault', description: 'Deploy required Azure role assignments.' },
95
+ acrAdminUsername: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Empty in the workspace; credential mode fills it only in the temporary ARM parameters file (never committed).' },
96
+ acrAdminPassword: { kind: 'fixed', value: '', source: 'managedIdentityMode', description: 'Empty in the workspace; credential mode fills it only in the temporary ARM parameters file (never committed).' },
97
+ acrResourceId: { kind: 'acrLookup', field: 'id', description: 'Resolved from acrName; mi mode grants AcrPull to both web app identities.' },
98
+ deployRoleAssignments: { kind: 'file', source: 'templateDefault', description: 'Deploy Contributor role assignments in the installation resource group. Needed in every mode: without it the controller cannot create the environment resources its second stage deploys.' },
99
+ deployRegistryRoleAssignment: { kind: 'parameterDefault', defaultFrom: 'deployRoleAssignments', source: 'templateDefault', description: 'Deploy the AcrPull grant on the container registry. False while credential image pull is in effect, because that grant lives in the registry resource group, which a customer installation cannot reach; restored for managed identity.' },
57
100
  moriBaseUrl: { kind: 'file', source: 'registration', required: true, description: 'Mori service base URL.' },
101
+ moriLicensePublicKey: { kind: 'file', source: 'registration', required: false, description: 'Mori licence issuer public key (SPKI PEM); empty leaves the installation unpinned and it then refuses every licence.' },
102
+ moriLicenseIssuer: { kind: 'file', source: 'registration', required: false, description: 'Expected iss claim on a Mori licence.' },
58
103
  moriCallbackApiKey: { kind: 'secretEnv', env: 'MORI_CALLBACK_API_KEY', required: true, description: 'Mori callback authentication key.' },
59
104
  entraCallbackStateSecret: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
60
105
  entraIdClientId: { kind: 'deferred', description: 'Not collected or sent by infra; issued/configured during later Entra provider onboarding.' },
@@ -62,6 +107,7 @@ const PARAMETER_CATALOG = Object.freeze({
62
107
  entraIdTenantId: { kind: 'deferred', description: 'Customer Entra provider tenant; distinct from the Azure subscription directory tenant and configured later.' },
63
108
  licenseEdition: { kind: 'license', source: 'cliOrFile', description: 'License requested from Mori during tenant activation: COMMUNITY, STANDARD or ENTERPRISE, or an Azure Marketplace subscription ID. Standard/Enterprise by name is a 30-day evaluation.' },
64
109
  onboardingMode: { kind: 'onboarding', source: 'workspacePolicy', description: 'Miso onboarding mode (ONBOARDING_MODE); Builder installations use saasDirect, Marketplace customer installations use customerOwned.' },
110
+ capacityProfile: { kind: 'capacity', source: 'workspacePreset', description: 'Infrastructure sizing profile (s, m, l, xl) selecting compute and PostgreSQL SKUs. Derived from the workspace preset so the two cannot drift; set it with --preset.' },
65
111
  misoImageTag: { kind: 'file', source: 'templateDefault', description: 'Miso container image tag.' },
66
112
  keycloakImageTag: { kind: 'file', source: 'templateDefault', description: 'Keycloak container image tag.' }
67
113
  });
@@ -85,7 +131,9 @@ const OUTPUT_CATALOG = Object.freeze({
85
131
  keycloakWebAppUrl: 'Keycloak web application URL.',
86
132
  keycloakRealmUrl: 'Keycloak realm URL.',
87
133
  keycloakAdminUser: 'Keycloak administrator user name.',
88
- portalUrl: 'Azure portal link for the installation resource group.'
134
+ portalUrl: 'Azure portal link for the installation resource group.',
135
+ azureOpenAiCreated: 'Whether Phase-1 created the Azure OpenAI account in the installation region.',
136
+ azureOpenAiSkipReason: 'Why Azure OpenAI was not created; empty when it was. An unsupported region skips the optional account instead of relocating it.'
89
137
  });
90
138
 
91
139
  /** Mori service key name written by DOCKER registration. */
@@ -159,7 +207,11 @@ module.exports = {
159
207
  DEFAULT_LICENSE_EDITION,
160
208
  ONBOARDING_MODES,
161
209
  DEFAULT_ONBOARDING_MODE,
210
+ ACR_AUTH_MODES,
211
+ DEFAULT_ACR_AUTH,
162
212
  normalizeLicenseEdition,
213
+ normalizeAcrAuth,
214
+ normalizeAzureLocation,
163
215
  buildEnvTemplate,
164
216
  assertTemplateCoverage
165
217
  };
@@ -8,9 +8,10 @@
8
8
 
9
9
  const crypto = require('crypto');
10
10
  const {
11
- PARAMETER_CATALOG, OUTPUT_CATALOG, DEFAULT_LICENSE_EDITION, DEFAULT_ONBOARDING_MODE
11
+ PARAMETER_CATALOG, OUTPUT_CATALOG, DEFAULT_LICENSE_EDITION, DEFAULT_ONBOARDING_MODE, DEFAULT_ACR_AUTH
12
12
  } = require('./infra-catalog');
13
13
 
14
+ const ACR_AUTH_DESCRIPTION = 'Image pull auth: auto (default; local dev set-registry pair for the ACR login server, else managed identity), mi (AcrPull), or credentials (local pair, then authorized ACR admin lookup). Credentials are never committed.';
14
15
  const INFRA_SCHEMA_VERSION = '2.0.0';
15
16
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
17
 
@@ -41,17 +42,34 @@ function parameterEntry(name, template) {
41
42
  return entry(DEFAULT_LICENSE_EDITION, spec.source, spec.description);
42
43
  case 'onboarding':
43
44
  return entry(DEFAULT_ONBOARDING_MODE, spec.source, spec.description);
45
+ case 'capacity':
46
+ // Mirrors the workspace preset; azure-infra-stage keeps them in step on every run.
47
+ return entry('s', spec.source, spec.description);
48
+ case 'parameterDefault':
49
+ return entry(templateDefault(template, spec.defaultFrom), spec.source, spec.description);
44
50
  default:
45
51
  return entry(templateDefault(template, name), spec.source, spec.description, spec.required ? { required: true } : {});
46
52
  }
47
53
  }
48
54
 
55
+ /** Sources written by Builder workflows that must survive regeneration. */
56
+ const STICKY_SOURCES = Object.freeze(['cli', 'acrAuthCredentials', 'acrAuthAdmin']);
57
+
58
+ function preservedState(previous) {
59
+ const kept = { value: previous.value };
60
+ if (STICKY_SOURCES.includes(previous.source)) kept.source = previous.source;
61
+ for (const key of ['restoreValue', 'restoreSource']) {
62
+ if (previous[key] !== undefined) kept[key] = previous[key];
63
+ }
64
+ return kept;
65
+ }
66
+
49
67
  function buildParameters(template, existing = {}) {
50
68
  const parameters = {};
51
69
  for (const name of Object.keys(PARAMETER_CATALOG)) {
52
70
  const fresh = parameterEntry(name, template);
53
71
  const workflowOwned = ['fixed', 'deferred', 'secretEnv'].includes(PARAMETER_CATALOG[name].kind);
54
- parameters[name] = existing[name] && !workflowOwned ? { ...fresh, value: existing[name].value } : fresh;
72
+ parameters[name] = existing[name] && !workflowOwned ? { ...fresh, ...preservedState(existing[name]) } : fresh;
55
73
  }
56
74
  return parameters;
57
75
  }
@@ -64,6 +82,7 @@ function buildOutputs(existing = {}) {
64
82
  function azureSections(template, existing) {
65
83
  return {
66
84
  acrName: existing.acrName || entry('', 'user', 'Required private ACR name. Builder resolves and persists its login server and resource ID using the authenticated Azure account.', { required: true }),
85
+ acrAuth: existing.acrAuth || entry(DEFAULT_ACR_AUTH, 'cliOrFile', ACR_AUTH_DESCRIPTION),
67
86
  allowMyIp: existing.allowMyIp || entry(false, 'cliOrFile', 'Default-off internal test access. When enabled, Builder resolves and audits one public IPv4 /32.', { resolvedCidr: null, resolvedAt: null, resolver: null }),
68
87
  preset: existing.preset || entry('s', 'cliOrFile', 'Shared infrastructure sizing preset.'),
69
88
  parameters: buildParameters(template, existing.parameters),
@@ -156,6 +175,7 @@ function missingRequiredFields(doc) {
156
175
 
157
176
  module.exports = {
158
177
  INFRA_SCHEMA_VERSION,
178
+ ACR_AUTH_DESCRIPTION,
159
179
  buildInfraDocument,
160
180
  missingRequiredFields
161
181
  };
@@ -3,12 +3,15 @@
3
3
  /**
4
4
  * @fileoverview Projects `environment-infra.json` plus resolved workspace secrets into standard
5
5
  * ARM deploymentParameters, and applies the default-off current-IP access option.
6
- * Deferred tenant-activation parameters are never projected.
6
+ * Deferred tenant-activation parameters are never projected. ACR admin credentials (password mode)
7
+ * are injected only into the temporary parameters file and never enter the input hash.
7
8
  */
8
9
 
9
10
  const { PARAMETER_CATALOG } = require('./infra-catalog');
10
11
  const { sha256 } = require('./workspace-files');
11
12
 
13
+ const ARM_CREDENTIAL_PARAMS = Object.freeze(new Set(['acrAdminUsername', 'acrAdminPassword']));
14
+
12
15
  function projectParameter(name, doc, context) {
13
16
  const spec = PARAMETER_CATALOG[name];
14
17
  switch (spec.kind) {
@@ -27,6 +30,12 @@ function projectParameter(name, doc, context) {
27
30
  }
28
31
  return context.acr[spec.field];
29
32
  case 'fixed':
33
+ if (name === 'acrAdminUsername' && context.registryCredentials) {
34
+ return context.registryCredentials.username;
35
+ }
36
+ if (name === 'acrAdminPassword' && context.registryCredentials) {
37
+ return context.registryCredentials.password;
38
+ }
30
39
  return spec.value;
31
40
  default:
32
41
  return doc.parameters[name].value;
@@ -39,6 +48,7 @@ function projectParameter(name, doc, context) {
39
48
  * @param {Object} context - Resolution context
40
49
  * @param {Object.<string,string>} context.env - Resolved workspace .env values
41
50
  * @param {{ id: string, loginServer: string }} context.acr - Resolved registry
51
+ * @param {{ username: string, password: string }|null|undefined} context.registryCredentials - Credential-mode pull creds (never hashed)
42
52
  * @param {string} context.templateHash - SHA-256 of the deployed template
43
53
  * @returns {{ document: Object, inputHash: string }}
44
54
  */
@@ -49,9 +59,10 @@ function buildArmParameters(doc, context) {
49
59
  const value = projectParameter(name, doc, context);
50
60
  if (value === undefined) continue;
51
61
  parameters[name] = { value };
52
- // Secrets never enter the hash.
53
- if (PARAMETER_CATALOG[name].kind !== 'secretEnv') nonSecret[name] = value;
62
+ const catalog = PARAMETER_CATALOG[name];
63
+ if (catalog.kind !== 'secretEnv' && !ARM_CREDENTIAL_PARAMS.has(name)) nonSecret[name] = value;
54
64
  }
65
+ nonSecret.acrAuth = doc.acrAuth?.effective || doc.acrAuth?.value || 'auto';
55
66
  return {
56
67
  document: {
57
68
  $schema: 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#',
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Image-pull authentication for CLI-managed AZURE infra. The requested mode
5
+ * (`auto|mi|credentials`, deprecated `admin` = credentials) resolves to an effective `mi` or
6
+ * `credentials` mode. Credential values come from the primary user's `dev set-registry` pair for the
7
+ * resolved ACR login server, or (explicit credentials only) from an authorized ACR admin lookup.
8
+ * They live only in memory and in the temporary ARM parameters file.
9
+ */
10
+
11
+ const { normalizeAcrAuth } = require('./infra-catalog');
12
+ const { ACR_AUTH_DESCRIPTION } = require('./infra-manifest');
13
+
14
+ const FORCED_ROLE_SOURCES = Object.freeze(['acrAuthCredentials', 'acrAuthAdmin']);
15
+
16
+ function restoreForcedRole(roles) {
17
+ const { restoreValue, restoreSource, ...rest } = roles;
18
+ return {
19
+ ...rest,
20
+ value: restoreValue === undefined ? true : restoreValue,
21
+ source: restoreSource || 'templateDefault'
22
+ };
23
+ }
24
+
25
+ function restoreLegacyContributorRole(doc) {
26
+ const roles = doc.parameters.deployRoleAssignments;
27
+ if (roles && FORCED_ROLE_SOURCES.includes(roles.source)) {
28
+ doc.parameters.deployRoleAssignments = restoreForcedRole(roles);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Normalizes and records the requested mode (CLI wins over the workspace value).
34
+ * @param {Object} doc - Infra manifest (mutated)
35
+ * @param {Object} input - Stage input (`acrAuth` from CLI)
36
+ * @returns {string} Requested mode
37
+ */
38
+ function applyRequestedAcrAuth(doc, input) {
39
+ const fromCli = input.acrAuth !== undefined;
40
+ const mode = normalizeAcrAuth(fromCli ? input.acrAuth : (doc.acrAuth?.value || 'auto'));
41
+ doc.acrAuth = {
42
+ ...(doc.acrAuth || {}),
43
+ value: mode,
44
+ source: fromCli ? 'cli' : (doc.acrAuth?.source || 'cliOrFile'),
45
+ description: ACR_AUTH_DESCRIPTION
46
+ };
47
+ return mode;
48
+ }
49
+
50
+ function localSetupHint(host) {
51
+ return `Run "aifabrix dev set-registry ${host} --username <username> --password-stdin", or use --acr-auth mi`;
52
+ }
53
+
54
+ async function resolveAzureFallback(doc, acr, az, fetchAdmin) {
55
+ try {
56
+ return fetchAdmin(doc, acr, az);
57
+ } catch (error) {
58
+ throw new Error(`No registry credentials are available for ${acr.loginServer}: ${error.message}. ${localSetupHint(acr.loginServer)}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Resolves the effective pull mode and ephemeral credentials. A partial local pair always fails.
64
+ * @param {Object} doc - Infra manifest with a normalized `acrAuth`
65
+ * @param {{ loginServer: string, adminUserEnabled?: boolean }} acr - Resolved registry
66
+ * @param {Function} az - Azure CLI runner
67
+ * @param {Object} deps - `loadRegistryCredentials(host)` and `resolveAcrAdminCredentials(doc, acr, az)`
68
+ * @returns {Promise<{ mode: 'mi'|'credentials', credentials: ({username: string, password: string}|null), source: string }>}
69
+ */
70
+ async function resolveRegistryAuth(doc, acr, az, deps) {
71
+ const requested = normalizeAcrAuth(doc.acrAuth?.value || 'auto');
72
+ if (requested === 'mi') return { mode: 'mi', credentials: null, source: 'managedIdentity' };
73
+ const local = await deps.loadRegistryCredentials(acr.loginServer);
74
+ if (local) return { mode: 'credentials', credentials: local, source: 'localRegistryCredentials' };
75
+ if (requested === 'auto') return { mode: 'mi', credentials: null, source: 'managedIdentity' };
76
+ const azure = await resolveAzureFallback(doc, acr, az, deps.resolveAcrAdminCredentials);
77
+ return { mode: 'credentials', credentials: azure, source: 'acrAdminLookup' };
78
+ }
79
+
80
+ /**
81
+ * Records the effective mode and keeps the registry role assignment consistent with it.
82
+ *
83
+ * Credential mode turns off only `deployRegistryRoleAssignment` -- the AcrPull grant, which lives
84
+ * in the registry's own resource group and, for a customer installation, in the vendor's tenant
85
+ * where Azure RBAC cannot reach. It deliberately leaves `deployRoleAssignments` alone: those are
86
+ * Contributor grants in the resource group the installation is creating, which the deployer owns.
87
+ *
88
+ * Turning both off is what ce-12 and ce-13 did, and it broke the installation in a way that took
89
+ * two attempts to see. Without Contributor on its own resource group the controller cannot create
90
+ * anything, so its second stage failed every module, no resource bindings were recorded, and both
91
+ * application deployments refused with "Environment 'dev' has no recorded placement".
92
+ *
93
+ * @param {Object} doc - Infra manifest (mutated)
94
+ * @param {'mi'|'credentials'} mode - Effective mode
95
+ */
96
+ function applyEffectiveAcrAuth(doc, mode) {
97
+ doc.acrAuth = { ...doc.acrAuth, effective: mode };
98
+ restoreLegacyContributorRole(doc);
99
+ const roles = doc.parameters.deployRegistryRoleAssignment
100
+ || doc.parameters.deployRoleAssignments;
101
+ const forced = FORCED_ROLE_SOURCES.includes(roles.source);
102
+ if (mode === 'credentials') {
103
+ if (forced) return;
104
+ doc.parameters.deployRegistryRoleAssignment = {
105
+ ...roles, value: false, source: 'acrAuthCredentials', restoreValue: roles.value, restoreSource: roles.source
106
+ };
107
+ return;
108
+ }
109
+ if (!forced) return;
110
+ doc.parameters.deployRegistryRoleAssignment = restoreForcedRole(roles);
111
+ }
112
+
113
+ /**
114
+ * @param {Object} doc - Infra manifest
115
+ * @returns {boolean} True when the last resolved pull mode uses registry credentials
116
+ */
117
+ function usesRegistryCredentials(doc) {
118
+ return doc.acrAuth?.effective === 'credentials';
119
+ }
120
+
121
+ module.exports = { applyRequestedAcrAuth, resolveRegistryAuth, applyEffectiveAcrAuth, usesRegistryCredentials };
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const path = require('path');
10
- const { normalizeLicenseEdition } = require('./infra-catalog');
10
+ const { normalizeLicenseEdition, normalizeAcrAuth, normalizeAzureLocation } = require('./infra-catalog');
11
11
 
12
12
  const WORKSPACE_STAGES = Object.freeze(['register', 'infra', 'dev', 'tst', 'pro']);
13
13
  const ENVIRONMENT_STAGES = Object.freeze(['dev', 'tst', 'pro']);
@@ -102,11 +102,24 @@ function assertInfraOnlyOptions(stage, options) {
102
102
  if (options.allowMyIp && stage !== 'infra') {
103
103
  throw new Error('--allow-my-ip is supported only with --stage infra for AZURE installations');
104
104
  }
105
- if (options.licenseEdition === undefined) return;
106
- if (stage !== 'infra') {
107
- throw new Error('--license-edition is supported only with --stage infra for AZURE installations');
105
+ if (options.licenseEdition !== undefined) {
106
+ if (stage !== 'infra') {
107
+ throw new Error('--license-edition is supported only with --stage infra for AZURE installations');
108
+ }
109
+ normalizeLicenseEdition(options.licenseEdition);
110
+ }
111
+ if (options.acrAuth !== undefined) {
112
+ if (stage !== 'infra') {
113
+ throw new Error('--acr-auth is supported only with --stage infra for AZURE installations');
114
+ }
115
+ normalizeAcrAuth(options.acrAuth);
116
+ }
117
+ if (options.location !== undefined) {
118
+ if (stage !== 'infra') {
119
+ throw new Error('--location is supported only with --stage infra (later environments inherit the saved region)');
120
+ }
121
+ normalizeAzureLocation(options.location);
108
122
  }
109
- normalizeLicenseEdition(options.licenseEdition);
110
123
  }
111
124
 
112
125
  function assertStageScopedOptions(stage, options) {
@@ -137,7 +150,7 @@ function assertStageScopedOptions(stage, options) {
137
150
  * @param {Object} options - Commander options
138
151
  * @param {string|undefined} persistedVendor - Vendor recorded in an existing workspace
139
152
  * @returns {{ resourceGroup: string, stage: string, vendor: string, allowMyIp: boolean, licenseEdition: string|undefined,
140
- * preset: string|undefined, controllerUrl: string|undefined, misoApiKeyEnv: string|undefined }}
153
+ * acrAuth: string|undefined, location: string|undefined, preset: string|undefined, controllerUrl: string|undefined, misoApiKeyEnv: string|undefined }}
141
154
  */
142
155
  function resolveStageInput(resourceGroupKey, options, persistedVendor) {
143
156
  const resourceGroup = validateResourceGroupKey(resourceGroupKey);
@@ -150,6 +163,8 @@ function resolveStageInput(resourceGroupKey, options, persistedVendor) {
150
163
  vendor,
151
164
  allowMyIp: options.allowMyIp === true,
152
165
  licenseEdition: options.licenseEdition,
166
+ acrAuth: options.acrAuth,
167
+ location: options.location,
153
168
  preset: options.preset,
154
169
  controllerUrl: options.controllerUrl,
155
170
  misoApiKeyEnv: options.misoApiKeyEnv
@@ -9,6 +9,7 @@ const crypto = require('crypto');
9
9
  const { spawnSync } = require('child_process');
10
10
  const config = require('../core/config');
11
11
  const { getRuntimeConfigDir } = require('../core/config-runtime-paths');
12
+ const logger = require('../utils/logger');
12
13
 
13
14
  const SHIPPED_TEMPLATE = path.resolve(__dirname, '../../templates/marketplace/main.json');
14
15
  // Resolved per call (not a module-load constant) so it honors AIFABRIX_HOME/AIFABRIX_CONFIG
@@ -273,8 +274,28 @@ async function deployMarketplaceEnvironment(envKey, options, dependencies = {})
273
274
  return deployPlatform({ ...input, controllerUrl: options.controllerUrl }, dependencies);
274
275
  }
275
276
 
277
+ /**
278
+ * Attempts allowed for the controller, at 10-second intervals.
279
+ *
280
+ * The controller runs first-time onboarding inside startup, and on Azure that onboarding is
281
+ * normally interrupted once — the template applies post-Key-Vault app settings and restarts the
282
+ * site — so the retry runs too. A clean installation was measured taking 720 seconds to pass its
283
+ * warm-up probe. The old 30 attempts gave 5 minutes and reported `readiness did not pass` for
284
+ * installations that were healthy, which is worse than a slow wait: it leaves `controllerUrl`
285
+ * unrecorded, so `--stage dev` then refuses with "Shared infrastructure is not deployed".
286
+ *
287
+ * 180 attempts is 30 minutes, matching WEBSITES_CONTAINER_START_TIME_LIMIT. Waiting longer than
288
+ * App Service itself would is pointless; waiting less than it declares is what caused this.
289
+ */
290
+ const CONTROLLER_READINESS_ATTEMPTS = 180;
291
+
276
292
  async function probeController(controllerUrl) {
277
- return probeHttps(controllerUrl, '/api/v1/health/readiness', 'Controller');
293
+ return probeHttps(
294
+ controllerUrl,
295
+ '/api/v1/health/readiness',
296
+ 'Controller',
297
+ CONTROLLER_READINESS_ATTEMPTS
298
+ );
278
299
  }
279
300
 
280
301
  /**
@@ -297,6 +318,10 @@ async function probeHttps(baseUrl, pathname, label, attempts = 30) {
297
318
  } catch (_) {
298
319
  // App Service startup can lag successful ARM provisioning.
299
320
  }
321
+ // A wait measured in minutes should say so, or it is indistinguishable from a hang.
322
+ if (attempt > 0 && attempt % 6 === 0) {
323
+ logger.log(` waiting for ${label} readiness… ${attempt * 10}s elapsed`);
324
+ }
300
325
  await new Promise(resolve => setTimeout(resolve, 10000));
301
326
  }
302
327
  throw new Error(`${label} readiness did not pass at ${url.origin}; inspect App Service before resuming`);
@@ -41,16 +41,15 @@ function filterConfigurationByRegistryMode(configuration, _registryMode) {
41
41
  return Array.isArray(configuration) ? configuration : [];
42
42
  }
43
43
 
44
+ /**
45
+ * Deployment image reference using the shared registry precedence
46
+ * (installation `registry` in config.yaml, then manifest `image.registry`).
47
+ * @param {Object} variables - Application variables
48
+ * @returns {string}
49
+ */
44
50
  function buildImageReference(variables) {
45
- const imageName = variables.image?.name || variables.app?.key || 'app';
46
- const registry = variables.image?.registry;
47
- const tag = variables.image?.tag || 'latest';
48
-
49
- if (registry) {
50
- return `${registry}/${imageName}:${tag}`;
51
- }
52
-
53
- return `${imageName}:${tag}`;
51
+ const { buildManifestImageReference } = require('../utils/resolve-docker-image-ref');
52
+ return buildManifestImageReference(variables, 'app');
54
53
  }
55
54
 
56
55
  /**
@@ -117,35 +117,46 @@ function substitutePortInDeployment(obj, portNumber) {
117
117
  /** Regex to find ${VAR} placeholders for env substitution */
118
118
  const ENV_VAR_PLACEHOLDER_REGEX = /\$\{([^}]+)\}/g;
119
119
 
120
+ function replaceDeploymentEnvString(value, envVarMap, fieldPath) {
121
+ if (fieldPath === 'frontDoorRouting.host') return value;
122
+ return value.replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
123
+ Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
124
+ }
125
+
126
+ function substituteEnvVarsInArray(items, envVarMap, fieldPath) {
127
+ for (let i = 0; i < items.length; i++) {
128
+ const itemPath = `${fieldPath}[${i}]`;
129
+ if (typeof items[i] === 'string') {
130
+ items[i] = replaceDeploymentEnvString(items[i], envVarMap, itemPath);
131
+ } else if (typeof items[i] === 'object' && items[i] !== null) {
132
+ substituteEnvVarsInDeployment(items[i], envVarMap, itemPath);
133
+ }
134
+ }
135
+ }
136
+
120
137
  /**
121
138
  * Recursively replaces ${VAR} with envVarMap[VAR] in all string values of obj (in-place).
122
139
  * Only substitutes when VAR is a key in envVarMap (from env-config.yaml / config).
123
140
  *
124
141
  * @param {Object} obj - Deployment manifest or any nested object
125
142
  * @param {Object} envVarMap - Flat map of variable names to values (e.g. from buildEnvVarMap('docker'))
143
+ * @param {string} [fieldPath=''] - Internal traversal path used to preserve the portable front door host template
126
144
  */
127
- function substituteEnvVarsInDeployment(obj, envVarMap) {
145
+ function substituteEnvVarsInDeployment(obj, envVarMap, fieldPath = '') {
128
146
  if (!envVarMap || typeof envVarMap !== 'object') return;
129
147
  if (obj === null || obj === undefined) return;
130
148
  if (Array.isArray(obj)) {
131
- for (let i = 0; i < obj.length; i++) {
132
- if (typeof obj[i] === 'string') {
133
- obj[i] = obj[i].replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
134
- Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
135
- } else if (typeof obj[i] === 'object' && obj[i] !== null) {
136
- substituteEnvVarsInDeployment(obj[i], envVarMap);
137
- }
138
- }
149
+ substituteEnvVarsInArray(obj, envVarMap, fieldPath);
139
150
  return;
140
151
  }
141
152
  if (typeof obj === 'object') {
142
153
  for (const key of Object.keys(obj)) {
143
154
  const value = obj[key];
155
+ const childPath = fieldPath ? `${fieldPath}.${key}` : key;
144
156
  if (typeof value === 'string') {
145
- obj[key] = value.replace(ENV_VAR_PLACEHOLDER_REGEX, (match, varName) =>
146
- Object.prototype.hasOwnProperty.call(envVarMap, varName) ? String(envVarMap[varName]) : match);
157
+ obj[key] = replaceDeploymentEnvString(value, envVarMap, childPath);
147
158
  } else if (typeof value === 'object' && value !== null) {
148
- substituteEnvVarsInDeployment(value, envVarMap);
159
+ substituteEnvVarsInDeployment(value, envVarMap, childPath);
149
160
  }
150
161
  }
151
162
  }
@@ -18,6 +18,7 @@ const { applyStaticAndEnterprise } = require('./apply-static-enterprise');
18
18
  const { applyViewpointDefaults } = require('./apply-viewpoint-defaults');
19
19
  const { stampCredentialTestEndpoint } = require('./apply-test-endpoint');
20
20
  const { buildSystemFile } = require('./apply-system-file');
21
+ const { resolveAuthoredDatasourceSync } = require('../datasource/sync-defaults');
21
22
 
22
23
  const STAMP_FILE = '.aifabrix-integration-definition.json';
23
24
 
@@ -160,8 +161,7 @@ function buildDatasourceFile(ds, systemKey, capabilities, sync, enrichOptions =
160
161
  ...(ds.storageType ? { storageType: ds.storageType } : {}),
161
162
  ...(ds.dimensions && typeof ds.dimensions === 'object' ? { dimensions: ds.dimensions } : {}),
162
163
  capabilities: buildDatasourceCapabilities(caps, key),
163
- // Schema sync is mode/schedule/batchSize only; BID sync.mode=incremental is operational intent.
164
- sync: { mode: 'pull', schedule: /** @type {any} */ (sync).schedule || '0 */6 * * *', batchSize: 100 },
164
+ sync: resolveAuthoredDatasourceSync(ds, sync),
165
165
  primaryKey: ['id'],
166
166
  labelKey: [labelField],
167
167
  ...buildDatasourceSchema(labelField)
@@ -178,5 +178,8 @@ module.exports = {
178
178
  rmSync,
179
179
  unlinkSync,
180
180
  copyFileSync,
181
- appendFileSync
181
+ appendFileSync,
182
+ openSync: (...args) => delegate('openSync', args),
183
+ closeSync: (...args) => delegate('closeSync', args),
184
+ realpathSync: (...args) => delegate('realpathSync', args)
182
185
  };
@@ -8,8 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- /** @type {import('@aifabrix/miso-client').MisoClient | null} */
12
- let misoClientSingleton = null;
11
+ const { getBuilderMisoRuntime } = require('./builder-miso-runtime');
13
12
 
14
13
  /**
15
14
  * @param {import('http').IncomingMessage} req
@@ -36,65 +35,10 @@ function assertNoForbiddenAuthHeaders(req) {
36
35
  }
37
36
 
38
37
  /**
39
- * @returns {string}
40
- */
41
- function resolveControllerUrlFromEnv() {
42
- const raw =
43
- process.env.MISO_CONTROLLER_URL ||
44
- process.env.CONTROLLER_URL ||
45
- process.env.MISO_WEB_SERVER_URL ||
46
- '';
47
- const url = String(raw).trim().replace(/\/$/, '');
48
- if (!url) {
49
- throw new Error('MISO_CONTROLLER_URL (or CONTROLLER_URL) is required for token validation');
50
- }
51
- return url;
52
- }
53
-
54
- /**
55
- * @returns {import('@aifabrix/miso-client/dist/types/config.types').MisoClientConfig}
56
- */
57
- function buildMisoClientConfigFromEnv() {
58
- const clientId = (process.env.MISO_CLIENTID || '').trim();
59
- const clientSecret = (process.env.MISO_CLIENTSECRET || '').trim();
60
- if (!clientId || !clientSecret) {
61
- throw new Error('MISO_CLIENTID and MISO_CLIENTSECRET are required for builder-api auth');
62
- }
63
- return {
64
- clientId,
65
- clientSecret,
66
- controllerUrl: resolveControllerUrlFromEnv(),
67
- logLevel: (process.env.LOG_LEVEL || 'info').toLowerCase()
68
- };
69
- }
70
-
71
- /**
72
- * Test hook — reset cached MisoClient between Jest cases.
73
- */
74
- function resetMisoClientForTests() {
75
- misoClientSingleton = null;
76
- }
77
-
78
- /**
79
- * @returns {Promise<import('@aifabrix/miso-client').MisoClient>}
38
+ * @returns {Promise<import('@aifabrix/miso-client').MisoClient>} Initialized shared client
80
39
  */
81
40
  async function getMisoClientForAuth() {
82
- let MisoClient;
83
- try {
84
- ({ MisoClient } = require('@aifabrix/miso-client'));
85
- } catch {
86
- const err = new Error('@aifabrix/miso-client is required for builder-api auth');
87
- err.statusCode = 503;
88
- throw err;
89
- }
90
-
91
- if (!misoClientSingleton) {
92
- misoClientSingleton = new MisoClient(buildMisoClientConfigFromEnv());
93
- }
94
- if (!misoClientSingleton.isInitialized()) {
95
- await misoClientSingleton.initialize();
96
- }
97
- return misoClientSingleton;
41
+ return getBuilderMisoRuntime().getClient();
98
42
  }
99
43
 
100
44
  /**
@@ -167,9 +111,6 @@ async function authenticateRequest(req, opts = {}) {
167
111
  module.exports = {
168
112
  extractBearerToken,
169
113
  assertNoForbiddenAuthHeaders,
170
- resolveControllerUrlFromEnv,
171
- buildMisoClientConfigFromEnv,
172
- resetMisoClientForTests,
173
114
  getMisoClientForAuth,
174
115
  validateBearerToken,
175
116
  authenticateRequest