@aifabrix/builder 2.61.2 → 2.63.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 (90) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/api/onboarding-status.api.js +117 -0
  4. package/lib/app/deploy.js +14 -5
  5. package/lib/app/register.js +24 -2
  6. package/lib/app/run-env-compose.js +44 -8
  7. package/lib/app/run-env-recovery.js +1 -0
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/app/run.js +6 -4
  10. package/lib/build/standard-docker-build.js +4 -0
  11. package/lib/channels/channel-artifact-generator.js +13 -3
  12. package/lib/cli/infra-guided-footers.js +10 -8
  13. package/lib/cli/infra-guided.js +60 -12
  14. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  15. package/lib/cli/setup-platform.js +20 -3
  16. package/lib/commands/auth-config.js +2 -0
  17. package/lib/commands/login.js +4 -0
  18. package/lib/commands/repair-datasource-apply.js +3 -1
  19. package/lib/commands/repair-datasource-legacy.js +94 -0
  20. package/lib/commands/setup-image-refresh.js +26 -0
  21. package/lib/commands/setup-modes.js +9 -2
  22. package/lib/commands/setup-onboarding-readiness.js +83 -0
  23. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  24. package/lib/commands/setup.js +35 -10
  25. package/lib/commands/up-builder-api.js +24 -8
  26. package/lib/commands/up-common.js +16 -1
  27. package/lib/commands/up-dataplane-bootstrap.js +149 -16
  28. package/lib/commands/up-dataplane-credentials.js +21 -0
  29. package/lib/commands/up-dataplane.js +26 -34
  30. package/lib/commands/up-integration-server.js +112 -0
  31. package/lib/commands/wizard-config-normalizer.js +5 -1
  32. package/lib/core/admin-secrets-env-overlay.js +8 -3
  33. package/lib/core/environment-access-policy.js +121 -0
  34. package/lib/core/environment-mode-policy.js +177 -0
  35. package/lib/core/local-env-overrides.js +149 -0
  36. package/lib/core/secrets-ensure.js +4 -2
  37. package/lib/core/secrets-env-content.js +12 -9
  38. package/lib/core/secrets-env-write.js +1 -1
  39. package/lib/datasource/capability/capability-manifest.js +1 -0
  40. package/lib/deployment/installation/index.js +14 -1
  41. package/lib/deployment/installation/infra-catalog.js +3 -0
  42. package/lib/deployment/installation/local-environment-stage.js +285 -0
  43. package/lib/deployment/installation/local-installation.js +76 -0
  44. package/lib/deployment/installation/stage-input.js +5 -1
  45. package/lib/generator/builders.js +3 -1
  46. package/lib/generator/index.js +32 -9
  47. package/lib/generator/split-variables.js +2 -2
  48. package/lib/parameters/database-adoption.js +125 -0
  49. package/lib/parameters/physical-database-naming.js +136 -0
  50. package/lib/parameters/physical-secret-name.js +194 -0
  51. package/lib/parameters/rsa-secret-values.js +165 -0
  52. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  53. package/lib/programmatic/run-channel-package.js +3 -1
  54. package/lib/programmatic/workspace-context.js +37 -2
  55. package/lib/schema/infra.parameter.yaml +61 -27
  56. package/lib/schema/infrastructure-schema.json +213 -120
  57. package/lib/utils/compose-generate-docker-compose.js +2 -2
  58. package/lib/utils/compose-generator.js +4 -7
  59. package/lib/utils/compose-miso-env.js +2 -2
  60. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  61. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  62. package/lib/utils/declarative-url-ports.js +3 -0
  63. package/lib/utils/env-environment-file-paths.js +105 -0
  64. package/lib/utils/environment-scoped-resources.js +48 -14
  65. package/lib/utils/log-redaction.js +4 -1
  66. package/lib/utils/paths-system-builder-keys.js +7 -1
  67. package/lib/utils/paths.js +7 -2
  68. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  69. package/lib/utils/redis-env-scope.js +21 -1
  70. package/lib/utils/registry-auth-sources.js +154 -0
  71. package/lib/utils/registry-credentials.js +131 -14
  72. package/lib/utils/secrets-generator.js +30 -21
  73. package/lib/utils/secrets-helpers.js +5 -7
  74. package/lib/utils/secrets-kv-scope.js +127 -28
  75. package/lib/utils/secrets-materialize-local.js +9 -0
  76. package/lib/utils/secrets-missing-error.js +35 -1
  77. package/lib/utils/token-manager.js +14 -8
  78. package/lib/utils/url-public-path-prefix.js +6 -10
  79. package/package.json +4 -2
  80. package/templates/README.md +6 -0
  81. package/templates/applications/builder-api/application.yaml +1 -1
  82. package/templates/applications/dataplane/application.yaml +2 -2
  83. package/templates/applications/dataplane/env.template +1 -1
  84. package/templates/applications/integration-server/application.yaml +91 -0
  85. package/templates/applications/integration-server/env.template +215 -0
  86. package/templates/applications/mirrored-templates.json +52 -0
  87. package/templates/applications/miso-controller/application.yaml +43 -1
  88. package/templates/applications/miso-controller/env.template +45 -39
  89. package/templates/marketplace/createUiDefinition.json +52 -4
  90. package/templates/marketplace/main.json +85 -7
@@ -14,6 +14,10 @@ const { execWithDockerEnv } = require('../utils/docker-exec');
14
14
  const { getLogLevel, passesLevelFilter } = require('../commands/app-logs');
15
15
  const { handleLogin } = require('../commands/login');
16
16
  const { handleUpBuilderApi } = require('../commands/up-builder-api');
17
+ const {
18
+ handleUpIntegrationServer,
19
+ isIntegrationServerSelected
20
+ } = require('../commands/up-integration-server');
17
21
  const {
18
22
  resolveGuidedAuthControllerUrl,
19
23
  runGuidedAuthStep,
@@ -45,6 +49,34 @@ const {
45
49
  emitSystemBuilderAppManifestLineIfTTY
46
50
  } = require('../utils/manifest-source-emit');
47
51
  const { flushDeferredPublicUrlWarnings } = require('./infra-guided-public-url-warn');
52
+ const { fetchOnboardingStatus } = require('../api/onboarding-status.api');
53
+ const {
54
+ waitForRequiredBaselineReadiness
55
+ } = require('../commands/setup-onboarding-readiness');
56
+
57
+ async function runRequiredBaselineReadinessIfNeeded(options, authOpts) {
58
+ if (!options || options.requireBaselineReadiness !== true) return;
59
+ const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
60
+ const spin = startSpinner('Waiting for required Miso baseline onboarding...');
61
+ const onProgress = createSpinnerTextUpdater(spin);
62
+ try {
63
+ await waitForRequiredBaselineReadiness({
64
+ request: requestOptions => fetchOnboardingStatus(controllerUrl, requestOptions),
65
+ onProgress
66
+ });
67
+ } catch (error) {
68
+ if (spin && typeof spin.fail === 'function') spin.fail('Miso baseline onboarding not ready');
69
+ throw error;
70
+ }
71
+ stopSpinnerSuccess(spin, 'Miso baseline onboarding ready');
72
+ logger.log('');
73
+ }
74
+
75
+ async function runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts) {
76
+ await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
77
+ await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
78
+ await runRequiredBaselineReadinessIfNeeded(options, authOpts);
79
+ }
48
80
 
49
81
  /**
50
82
  * Guided Keycloak → Miso Controller start with one spinner per app (no second health wait).
@@ -295,8 +327,25 @@ async function runGuidedDataplaneInstallPhase(options, handleUpDataplane, handle
295
327
  await refreshDataplaneVersionAfterPlatformInstall();
296
328
  }
297
329
 
330
+ async function runGuidedIntegrationServerPhase(options, selected) {
331
+ if (!selected) return;
332
+ const spin = startSpinner('Starting Integration Server...');
333
+ await withMutedLogger(() => handleUpIntegrationServer({ ...options, platformInstall: true }));
334
+ stopSpinnerSuccess(spin, 'Integration Server ready');
335
+ }
336
+
337
+ async function runGuidedKeycloakRealmPhase(options) {
338
+ if (!options || options.requireKeycloakRealm !== true) return;
339
+ const { waitForKeycloakRealmReady } = require('../utils/keycloak-realm-ready');
340
+ const spin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
341
+ await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
342
+ stopSpinnerSuccess(spin, 'Keycloak realm ready');
343
+ logger.log('');
344
+ }
345
+
298
346
  async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, handleLogin, forceCleanSummary = null) {
299
- logGuidedPlatformSetupHeader();
347
+ const integrationServerSelected = await isIntegrationServerSelected();
348
+ logGuidedPlatformSetupHeader({ integrationServer: integrationServerSelected });
300
349
  if (forceCleanSummary && forceCleanSummary.forceSummary) {
301
350
  logUpPlatformForceCleanSummary(forceCleanSummary.forceSummary, forceCleanSummary.cleanedApps);
302
351
  }
@@ -306,23 +355,21 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
306
355
  skipLoginIfAuthenticated: options && options.skipLoginIfAuthenticated === true
307
356
  };
308
357
 
309
- await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
310
- await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
311
- if (options && options.requireKeycloakRealm === true) {
312
- const { waitForKeycloakRealmReady } = require('../utils/keycloak-realm-ready');
313
- const realmSpin = startSpinner('Waiting for Keycloak realm (aifabrix)...');
314
- await waitForKeycloakRealmReady({ timeoutSeconds: 180 });
315
- stopSpinnerSuccess(realmSpin, 'Keycloak realm ready');
316
- logger.log('');
317
- }
358
+ await runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts);
359
+ await runGuidedKeycloakRealmPhase(options);
318
360
  await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
319
361
  const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
320
362
  await runGuidedDataplaneInstallPhase(options, handleUpDataplane, handleLogin, controllerUrl);
321
363
  const baSpin = startSpinner('Starting Builder API...');
322
364
  await withMutedLogger(() => handleUpBuilderApi({ ...options, platformInstall: true }));
323
365
  stopSpinnerSuccess(baSpin, 'Builder API ready');
324
- await validateAppErrorLogs(['miso-controller', 'dataplane', 'builder-api']);
325
- await logPlatformReadyFooter();
366
+ await runGuidedIntegrationServerPhase(options, integrationServerSelected);
367
+ const appNames = [
368
+ 'miso-controller', 'dataplane', 'builder-api',
369
+ ...(integrationServerSelected ? ['integration-server'] : [])
370
+ ];
371
+ await validateAppErrorLogs(appNames);
372
+ await logPlatformReadyFooter({ integrationServer: integrationServerSelected });
326
373
  }
327
374
 
328
375
  async function runGuidedDownInfra(appName, options, infraMod, appLib) {
@@ -368,6 +415,7 @@ module.exports = {
368
415
  runGuidedUpMiso,
369
416
  runGuidedUpDataplane,
370
417
  runGuidedUpPlatform,
418
+ runRequiredBaselineReadinessIfNeeded,
371
419
  runGuidedDownInfra,
372
420
  stripHealthPrefix
373
421
  };
@@ -17,6 +17,10 @@ const { handleLogin } = require('../commands/login');
17
17
  const { handleUpMiso } = require('../commands/up-miso');
18
18
  const { handleUpDataplane } = require('../commands/up-dataplane');
19
19
  const { handleUpBuilderApi } = require('../commands/up-builder-api');
20
+ const {
21
+ handleUpIntegrationServer,
22
+ isIntegrationServerSelected
23
+ } = require('../commands/up-integration-server');
20
24
  const {
21
25
  applyUpPlatformForceConfig,
22
26
  cleanBuilderAppDirs,
@@ -26,7 +30,13 @@ const {
26
30
  const { runGuidedUpPlatform } = require('./infra-guided');
27
31
  const { recordInfraInstallationCommand } = require('./installation-log-command');
28
32
 
29
- const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane', 'builder-api'];
33
+ const PLATFORM_APPS = [
34
+ 'keycloak',
35
+ 'miso-controller',
36
+ 'dataplane',
37
+ 'builder-api',
38
+ 'integration-server'
39
+ ];
30
40
 
31
41
  /**
32
42
  * @param {Object} options
@@ -59,6 +69,9 @@ async function runUpPlatformWork(options) {
59
69
  await handleUpMiso(options);
60
70
  await handleUpDataplane(options);
61
71
  await handleUpBuilderApi({ ...options, platformInstall: true });
72
+ if (await isIntegrationServerSelected()) {
73
+ await handleUpIntegrationServer({ ...options, platformInstall: true });
74
+ }
62
75
  }
63
76
  return cleanedAppKeys;
64
77
  }
@@ -4,7 +4,7 @@
4
4
  * `setup` is the one-shot platform installer. When no infrastructure is
5
5
  * detected it runs a small wizard (admin email/password, AI tool keys) and
6
6
  * then `up-infra` + `up-platform`. When infra is already up, it offers a
7
- * mode menu (re-install, wipe data, clean install files, update images).
7
+ * mode menu (re-install, wipe data, update images).
8
8
  *
9
9
  * `teardown` is the symmetrical inverse of `setup`: `down-infra -v` plus a
10
10
  * full clean of the AI Fabrix system directory (everything except
@@ -20,6 +20,7 @@
20
20
  const { handleCommandError } = require('../utils/cli-utils');
21
21
  const { handleSetup } = require('../commands/setup');
22
22
  const { handleTeardown } = require('../commands/teardown');
23
+ const { parseDimensionAbacOption } = require('../utils/dataplane-dimension-abac-setting');
23
24
 
24
25
  const SETUP_HELP_AFTER = `
25
26
  What this command does:
@@ -27,16 +28,25 @@ What this command does:
27
28
  - With infra running: shows a mode menu:
28
29
  1) Re-install — stop infra and remove all volumes, then up-infra + up-platform --force.
29
30
  2) Wipe data — drop every database and DB user, then up-infra + up-platform --force.
30
- 3) Clean installation files — remove user-local secrets, then up-infra + up-platform --force.
31
- 4) Update images — pull the latest infra and platform images, then up-infra + up-platform.
31
+ 3) Update images — pull the latest infra and platform images, then up-infra + up-platform.
32
32
  - With --mode fresh: first-time install when no infra is running (non-interactive; use with --admin-email and --admin-password).
33
33
  - With --mode update-images: refresh images when infra is already running (non-interactive).
34
34
 
35
35
  AI tool keys are read from the user-local file (~/.aifabrix/secrets.local.yaml) merged with the shared aifabrix-secrets file. If either source already provides keys, the AI tool prompt is skipped.
36
36
 
37
+ Dimension ABAC defaults to off. Use --dimension-abac on|off to persist an explicit Dataplane selection. The saved selection is reused by later setup and redeploy flows; changing it takes effect after the normal Dataplane restart/redeploy boundary.
38
+
37
39
  Use 'aifabrix teardown' to fully remove the local installation.
38
40
  `;
39
41
 
42
+ function parseRepeatedDimensionAbacOption(raw, previous) {
43
+ const value = parseDimensionAbacOption(raw);
44
+ if (previous !== undefined && previous !== value) {
45
+ throw new Error('Conflicting --dimension-abac values. Supply only on or only off.');
46
+ }
47
+ return value;
48
+ }
49
+
40
50
  const TEARDOWN_HELP_AFTER = `
41
51
  What this command does:
42
52
  - Runs down-infra -v (stops all infra + apps, removes every Docker volume).
@@ -61,7 +71,14 @@ function registerSetup(program) {
61
71
  .option('--pro-password-mode <mode>', 'Pro profile only: autogen or manual')
62
72
  .option('--admin-email <email>', 'Admin email (non-interactive fresh install)')
63
73
  .option('--admin-password <password>', 'Admin password for dev or pro manual-single (non-interactive)')
74
+ .option('--integration-server', 'Install the integration server (runs once in the miso environment)')
75
+ .option('--no-integration-server', 'Do not install the integration server (default)')
64
76
  .option('--mode <mode>', 'Non-interactive mode: fresh (no infra) or update-images (infra running)')
77
+ .option(
78
+ '--dimension-abac <off|on>',
79
+ 'Persist Dataplane Dimension ABAC selection (default: off)',
80
+ parseRepeatedDimensionAbacOption
81
+ )
65
82
  .option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
66
83
  .addHelpText('after', SETUP_HELP_AFTER)
67
84
  .action(async(options) => {
@@ -23,6 +23,7 @@ const {
23
23
  checkUserLoggedIn
24
24
  } = require('../utils/auth-config-validator');
25
25
  const { hasStoredDeviceTokenForController } = require('../utils/controller-url');
26
+ const { assertEnvironmentAllowedForCurrentMode } = require('../core/environment-mode-policy');
26
27
  const logger = require('../utils/logger');
27
28
 
28
29
  /**
@@ -166,6 +167,7 @@ async function handleSetEnvironment(environment) {
166
167
  try {
167
168
  // Validate environment format
168
169
  validateEnvironment(environment);
170
+ await assertEnvironmentAllowedForCurrentMode(environment);
169
171
 
170
172
  // Get current controller from config
171
173
  const controllerUrl = await getControllerUrl();
@@ -121,6 +121,10 @@ async function normalizeControllerUrl(options) {
121
121
  async function handleEnvironmentConfig(options) {
122
122
  if (options.environment) {
123
123
  const environment = options.environment.trim();
124
+ // `login -e tst` sets the environment without going through `auth set-environment`,
125
+ // so the mode rule has to be applied here too or it is trivially bypassed.
126
+ const { assertEnvironmentAllowedForCurrentMode } = require('../core/environment-mode-policy');
127
+ await assertEnvironmentAllowedForCurrentMode(environment);
124
128
  await setCurrentEnvironment(environment);
125
129
  if (!options.compact) {
126
130
  logger.log(chalk.gray(`Environment: ${environment}`));
@@ -8,6 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { repairLegacyDatasource } = require('./repair-datasource-legacy');
11
12
  const { repairKeyedDocument } = require('./repair-datasource-keyed-document');
12
13
  const { repairOpenapiSection } = require('./repair-datasource-openapi');
13
14
  const { repairSyncOperationsFromCapabilities } = require('./repair-datasource-sync-operations');
@@ -122,7 +123,8 @@ function applyOpenapiDatasourceRepairs(parsed, changes) {
122
123
  */
123
124
  function runRepairDatasourceFile(parsed, options, changes, helpers) {
124
125
  const out = Array.isArray(changes) ? changes : [];
125
- let updated = helpers.stripDeprecatedServiceCategory(parsed, out);
126
+ let updated = repairLegacyDatasource(parsed, out);
127
+ updated = helpers.stripDeprecatedServiceCategory(parsed, out) || updated;
126
128
 
127
129
  if (shouldApplyServiceContractRepair(parsed, options)) {
128
130
  updated = repairNoneLlmToServiceContract(parsed, out) || updated;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview Migrate legacy datasource storage and mapping shapes.
3
+ * @author AI Fabrix Team
4
+ */
5
+ 'use strict';
6
+
7
+ const ENTITY_TYPES = {
8
+ 'record-storage': 'recordStorage',
9
+ 'document-storage': 'documentStorage',
10
+ 'vector-store': 'vectorStore',
11
+ 'message-service': 'messageService'
12
+ };
13
+
14
+ /**
15
+ * Preserve legacy attribute types in metadataSchema and qualify raw input paths.
16
+ * @param {Object} parsed
17
+ * @param {string[]} changes
18
+ * @returns {boolean}
19
+ */
20
+ function repairLegacyAttributes(parsed, changes) {
21
+ let updated = false;
22
+ for (const [key, attr] of Object.entries(parsed.fieldMappings?.attributes || {})) {
23
+ if (!attr || typeof attr !== 'object') continue;
24
+ if (typeof attr.expression === 'string' && /^\{\{\s*metadata\.[\w.[\]]+\s*\}\}$/.test(attr.expression)) {
25
+ attr.expression = attr.expression.replace(/\{\{\s*(metadata\.[\w.[\]]+)\s*\}\}/, '{{raw.$1}}');
26
+ changes.push(`Qualified legacy attribute '${key}' with raw input root`);
27
+ updated = true;
28
+ }
29
+ if (['string', 'number', 'integer', 'boolean', 'object', 'array', 'null'].includes(attr.type)) {
30
+ parsed.metadataSchema ||= { type: 'object', additionalProperties: true };
31
+ parsed.metadataSchema.properties ||= {};
32
+ parsed.metadataSchema.properties[key] ||= { type: attr.type };
33
+ delete attr.type;
34
+ changes.push(`Moved legacy attribute '${key}' type to metadataSchema`);
35
+ updated = true;
36
+ }
37
+ }
38
+ return updated;
39
+ }
40
+
41
+ /**
42
+ * Move only unambiguous local bindings; preserve unsupported/conflicting input.
43
+ * @param {Object} parsed
44
+ * @param {string[]} changes
45
+ * @returns {boolean}
46
+ */
47
+ function repairLegacyDimensions(parsed, changes) {
48
+ const legacy = parsed.fieldMappings?.dimensions;
49
+ if (!legacy || typeof legacy !== 'object' || Array.isArray(legacy)) return false;
50
+ let updated = false;
51
+ for (const [key, value] of Object.entries(legacy)) {
52
+ const match = typeof value === 'string' && /^metadata\.([a-z][a-zA-Z0-9]*)$/.exec(value);
53
+ if (!match || !Object.hasOwn(parsed.fieldMappings.attributes || {}, match[1])) continue;
54
+ const binding = { type: 'local', field: match[1] };
55
+ const existing = parsed.dimensions?.[key];
56
+ if (existing && (existing.type !== 'local' || existing.field !== match[1])) continue;
57
+ parsed.dimensions ||= {};
58
+ parsed.dimensions[key] ||= binding;
59
+ delete legacy[key];
60
+ changes.push(`Moved legacy dimension '${key}' to root dimensions`);
61
+ updated = true;
62
+ }
63
+ if (Object.keys(legacy).length === 0) {
64
+ delete parsed.fieldMappings.dimensions;
65
+ updated = true;
66
+ }
67
+ return updated;
68
+ }
69
+
70
+ /**
71
+ * @param {Object} parsed
72
+ * @param {string[]} changes
73
+ * @returns {boolean}
74
+ */
75
+ function repairLegacyDatasource(parsed, changes = []) {
76
+ if (!parsed || typeof parsed !== 'object') return false;
77
+ let updated = false;
78
+ if (Object.hasOwn(ENTITY_TYPES, parsed.entityType)) {
79
+ parsed.entityType = ENTITY_TYPES[parsed.entityType];
80
+ changes.push(`Normalized legacy entityType to ${parsed.entityType}`);
81
+ updated = true;
82
+ }
83
+ if (['recordStorage', 'documentStorage'].includes(parsed.entityType) && !parsed.labelKey
84
+ && parsed.primaryKey?.length === 1
85
+ && Object.hasOwn(parsed.fieldMappings?.attributes || {}, parsed.primaryKey[0])) {
86
+ parsed.labelKey = [...parsed.primaryKey];
87
+ changes.push('Defaulted missing labelKey to the sole primary key attribute');
88
+ updated = true;
89
+ }
90
+ updated = repairLegacyAttributes(parsed, changes) || updated;
91
+ return repairLegacyDimensions(parsed, changes) || updated;
92
+ }
93
+
94
+ module.exports = { repairLegacyDatasource };
@@ -105,6 +105,26 @@ function readPlatformDeployImageRef(appName) {
105
105
  * @param {{ quiet?: boolean }} [opts]
106
106
  * @returns {Promise<boolean>} True when a pull was attempted
107
107
  */
108
+ /**
109
+ * True when Docker already holds the image.
110
+ *
111
+ * An installation with no registry configured deploys the `application.yaml` image as it
112
+ * stands, which is built locally and never pushed. Authenticating first would prompt for
113
+ * credentials the install does not need, and pulling would fail against a registry that
114
+ * has never seen the tag, so the local image is the answer when it is already there.
115
+ *
116
+ * @param {string} imageRef - Image reference
117
+ * @returns {Promise<boolean>}
118
+ */
119
+ async function imageExistsLocally(imageRef) {
120
+ try {
121
+ await dockerExec.execWithDockerEnv(`docker image inspect ${imageRef}`);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
108
128
  async function pullPlatformAppImage(appName, opts = {}) {
109
129
  const quiet = Boolean(opts.quiet);
110
130
  const imageRef = readPlatformDeployImageRef(appName);
@@ -114,6 +134,12 @@ async function pullPlatformAppImage(appName, opts = {}) {
114
134
  }
115
135
  return false;
116
136
  }
137
+ if (await imageExistsLocally(imageRef)) {
138
+ if (!quiet) {
139
+ logger.log(formatDatasourceListRow('ok', appName, 'image already local'));
140
+ }
141
+ return true;
142
+ }
117
143
  // A partial stored credential pair fails closed here instead of degrading to a warning.
118
144
  await ensureLoginForImageRefs([imageRef]);
119
145
  try {
@@ -58,7 +58,13 @@ const {
58
58
  } = require('../utils/infra-optional-service-flags');
59
59
 
60
60
  /** Builder app keys touched by `up-platform --force`. */
61
- const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane', 'builder-api'];
61
+ const PLATFORM_APPS = [
62
+ 'keycloak',
63
+ 'miso-controller',
64
+ 'dataplane',
65
+ 'builder-api',
66
+ 'integration-server'
67
+ ];
62
68
 
63
69
  function formatBackupSuffix() {
64
70
  const d = new Date();
@@ -290,7 +296,8 @@ async function runUpPlatform(opts = {}) {
290
296
  base: true,
291
297
  platformControllerUrl: authCtx.platformControllerUrl,
292
298
  skipLoginIfAuthenticated: authCtx.skipLoginIfAuthenticated,
293
- requireKeycloakRealm: opts.requireKeycloakRealm === true
299
+ requireKeycloakRealm: opts.requireKeycloakRealm === true,
300
+ requireBaselineReadiness: true
294
301
  },
295
302
  upMiso.handleUpMiso,
296
303
  upDataplane.handleUpDataplane,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Bounded observer for required Miso baseline onboarding.
3
+ *
4
+ * @fileoverview Setup-only Miso baseline readiness observer
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { performance } = require('node:perf_hooks');
12
+
13
+ const DEFAULT_INTERVAL_MS = 2000;
14
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5000;
15
+ const DEFAULT_DEADLINE_MS = 90000;
16
+
17
+ function monotonicNow() {
18
+ return performance.now();
19
+ }
20
+
21
+ function readinessError(message, code) {
22
+ const error = new Error(message);
23
+ error.code = code;
24
+ return error;
25
+ }
26
+
27
+ function classifyReadiness(snapshot) {
28
+ const dimensions = snapshot.baselineDimensions;
29
+ const roles = snapshot.baselineRoles;
30
+ if (dimensions.state === 'incomplete') {
31
+ throw readinessError(
32
+ `Required baseline Dimensions are incomplete (${dimensions.code}). Retry setup after reconciliation completes.`,
33
+ dimensions.code
34
+ );
35
+ }
36
+ if (roles.state === 'incomplete') {
37
+ throw readinessError(
38
+ `Required baseline Business Roles are incomplete (${roles.code}). Retry setup after reconciliation completes.`,
39
+ roles.code
40
+ );
41
+ }
42
+ if (dimensions.state === 'complete' && roles.state === 'complete') return 'complete';
43
+ return dimensions.state === 'pending' ? 'dimensions' : 'roles';
44
+ }
45
+
46
+ function timeoutError() {
47
+ return readinessError(
48
+ 'Timed out waiting for required Miso baseline onboarding. Retry setup after reconciliation completes.',
49
+ 'MISO_BASELINE_READINESS_TIMEOUT'
50
+ );
51
+ }
52
+
53
+ async function waitForRequiredBaselineReadiness(options) {
54
+ const request = options.request;
55
+ const now = options.now || monotonicNow;
56
+ const sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
57
+ const onProgress = options.onProgress || (() => {});
58
+ const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
59
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
60
+ const deadlineMs = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
61
+ const startedAt = now();
62
+ while (now() - startedAt < deadlineMs) {
63
+ const remaining = deadlineMs - (now() - startedAt);
64
+ if (remaining <= 0) throw timeoutError();
65
+ const snapshot = await request({ timeoutMs: Math.min(requestTimeoutMs, remaining) });
66
+ const state = classifyReadiness(snapshot);
67
+ if (state === 'complete') return snapshot;
68
+ onProgress(state === 'dimensions'
69
+ ? 'Waiting for required baseline Dimensions...'
70
+ : 'Waiting for required baseline Business Roles...');
71
+ const afterRequest = deadlineMs - (now() - startedAt);
72
+ if (afterRequest <= 0) throw timeoutError();
73
+ await sleep(Math.min(intervalMs, afterRequest));
74
+ }
75
+ throw timeoutError();
76
+ }
77
+
78
+ module.exports = {
79
+ DEFAULT_INTERVAL_MS,
80
+ DEFAULT_REQUEST_TIMEOUT_MS,
81
+ DEFAULT_DEADLINE_MS,
82
+ waitForRequiredBaselineReadiness
83
+ };
@@ -73,8 +73,42 @@ async function promptSetupPlatformMode(options = {}) {
73
73
  return normalizeSetupPlatformMode(mode);
74
74
  }
75
75
 
76
+ /**
77
+ * Whether this installation carries the integration server.
78
+ *
79
+ * It runs once per stack in the `miso` environment and most installations do not need it,
80
+ * so it is opt-in rather than something every install acquires by default. The answer is
81
+ * persisted, so it is asked once rather than on every run.
82
+ *
83
+ * `--integration-server` and `--no-integration-server` answer it non-interactively; a
84
+ * non-TTY run without either defaults to not installing it, because acquiring an extra
85
+ * application by silence is the wrong default.
86
+ *
87
+ * @async
88
+ * @param {Object} [options] - CLI options
89
+ * @returns {Promise<boolean>}
90
+ */
91
+ async function promptIntegrationServer(options = {}) {
92
+ if (options.integrationServer === true || options.integrationServer === false) {
93
+ return options.integrationServer;
94
+ }
95
+ if (!process.stdin.isTTY || options.yes === true) {
96
+ return false;
97
+ }
98
+ const { integrationServer } = await inquirer.prompt([
99
+ {
100
+ type: 'confirm',
101
+ name: 'integrationServer',
102
+ message: 'Install the integration server (runs once in the miso environment)?',
103
+ default: false
104
+ }
105
+ ]);
106
+ return Boolean(integrationServer);
107
+ }
108
+
76
109
  module.exports = {
77
110
  resolvePlatformModeFromOptions,
78
111
  promptSetupPlatformMode,
112
+ promptIntegrationServer,
79
113
  getSetupPlatformModePromptChoices
80
114
  };
@@ -36,6 +36,11 @@ const setupModes = require('./setup-modes');
36
36
  const { syncPlatformControllerUrlsInConfig } = require('./setup-platform-auth');
37
37
  const installationLog = require('../utils/installation-log');
38
38
  const { beginSetupCommandFlow, endSetupCommandFlow } = require('./setup-run-context');
39
+ const {
40
+ parseDimensionAbacOption,
41
+ resolveDimensionAbacSetting,
42
+ persistExplicitDimensionAbacSetting
43
+ } = require('../utils/dataplane-dimension-abac-setting');
39
44
 
40
45
  const MODE = setupPrompts.MODE;
41
46
 
@@ -87,7 +92,20 @@ function normalizeCliMode(options) {
87
92
  * @param {'fresh'|'update-images'} cliMode
88
93
  * @returns {Promise<{ outcome: string, setupMode: string }>}
89
94
  */
90
- async function runSetupWithCliMode(options, cliMode) {
95
+ async function persistDimensionAbacSelection(settingContext) {
96
+ await persistExplicitDimensionAbacSetting(config, settingContext.explicitValue);
97
+ }
98
+
99
+ async function prepareDimensionAbacSetting(options) {
100
+ const explicitValue = parseDimensionAbacOption(options.dimensionAbac);
101
+ const userConfig = await config.getConfig();
102
+ const resolved = resolveDimensionAbacSetting(userConfig, explicitValue);
103
+ const displayValue = resolved.value ? 'on' : 'off';
104
+ logger.log(infoLine(`Dimension ABAC: ${displayValue} (${resolved.source}).`));
105
+ return { explicitValue, ...resolved };
106
+ }
107
+
108
+ async function runSetupWithCliMode(options, cliMode, settingContext) {
91
109
  logSetupHeader();
92
110
  logger.log(formatProgress('Detecting current installation state...'));
93
111
  const running = await isInfraRunning();
@@ -97,12 +115,13 @@ async function runSetupWithCliMode(options, cliMode) {
97
115
  'Infrastructure is already running; use --mode update-images or run teardown first.'
98
116
  );
99
117
  }
100
- await runFreshInstallFlow(options);
118
+ await runFreshInstallFlow(options, settingContext);
101
119
  return { outcome: 'success', setupMode: CLI_SETUP_MODES.FRESH };
102
120
  }
103
121
  if (!running) {
104
122
  throw new Error('No infrastructure running; use --mode fresh for first-time install.');
105
123
  }
124
+ await persistDimensionAbacSelection(settingContext);
106
125
  await dispatchMode(MODE.UPDATE_IMAGES);
107
126
  logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${cliMode}).`));
108
127
  return { outcome: 'success', setupMode: cliMode };
@@ -110,19 +129,23 @@ async function runSetupWithCliMode(options, cliMode) {
110
129
 
111
130
  async function applySetupPlatformMode(options) {
112
131
  const mode = await setupPlatformModePrompts.promptSetupPlatformMode(options);
132
+ const integrationServer = await setupPlatformModePrompts.promptIntegrationServer(options);
113
133
  const cfg = await config.getConfig();
114
134
  applyPlatformTopologyFlagsToConfig(cfg, mode);
135
+ const { onboardingAppsForIntegrationServer } = require('../core/local-env-overrides');
136
+ cfg.onboardingApps = onboardingAppsForIntegrationServer(integrationServer);
115
137
  await config.saveConfig(cfg);
116
138
  await syncPlatformControllerUrlsInConfig();
117
139
  return mode;
118
140
  }
119
141
 
120
- async function runFreshInstallFlow(options) {
142
+ async function runFreshInstallFlow(options, settingContext) {
121
143
  await maybePinDeveloperIdForFreshInstall(options.developer);
122
144
  logger.log(infoLine('No infrastructure detected; starting fresh install.'));
123
145
  await applySetupPlatformMode(options);
124
146
  const adminCreds = await setupPrompts.promptAdminCredentials(options);
125
147
  await config.setAdminEmail(adminCreds.adminEmail);
148
+ await persistDimensionAbacSelection(settingContext);
126
149
  await setupModes.runFreshInstall(adminCreds);
127
150
  logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
128
151
  }
@@ -131,7 +154,7 @@ async function runFreshInstallFlow(options) {
131
154
  * @param {boolean} assumeYes
132
155
  * @returns {Promise<{ aborted: boolean, mode: string|null }>}
133
156
  */
134
- async function runExistingInfraFlow(assumeYes) {
157
+ async function runExistingInfraFlow(assumeYes, settingContext) {
135
158
  const mode = await setupPrompts.promptModeSelection();
136
159
  const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
137
160
  if (!proceed) {
@@ -142,6 +165,7 @@ async function runExistingInfraFlow(assumeYes) {
142
165
  if (mode === MODE.REINSTALL || mode === MODE.WIPE_DATA) {
143
166
  await applySetupPlatformMode({});
144
167
  }
168
+ await persistDimensionAbacSelection(settingContext);
145
169
  await dispatchMode(mode);
146
170
  logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
147
171
  return { aborted: false, mode };
@@ -194,20 +218,19 @@ async function dispatchMode(mode) {
194
218
  * @param {boolean} assumeYes
195
219
  * @returns {Promise<{ outcome: string, setupMode: string|null }>}
196
220
  */
197
- async function runSetupMainPath(options, assumeYes) {
198
- const cliMode = normalizeCliMode(options);
221
+ async function runSetupMainPath(options, assumeYes, cliMode, settingContext) {
199
222
  if (cliMode) {
200
- return runSetupWithCliMode(options, cliMode);
223
+ return runSetupWithCliMode(options, cliMode, settingContext);
201
224
  }
202
225
 
203
226
  logSetupHeader();
204
227
  logger.log(formatProgress('Detecting current installation state...'));
205
228
  const running = await isInfraRunning();
206
229
  if (!running) {
207
- await runFreshInstallFlow(options);
230
+ await runFreshInstallFlow(options, settingContext);
208
231
  return { outcome: 'success', setupMode: 'fresh' };
209
232
  }
210
- const r = await runExistingInfraFlow(assumeYes);
233
+ const r = await runExistingInfraFlow(assumeYes, settingContext);
211
234
  const outcome = r.aborted ? 'aborted' : 'success';
212
235
  return { outcome, setupMode: r.mode };
213
236
  }
@@ -297,7 +320,9 @@ async function handleSetup(options = {}) {
297
320
 
298
321
  beginSetupCommandFlow();
299
322
  try {
300
- const main = await runSetupMainPath(options, assumeYes);
323
+ const cliMode = normalizeCliMode(options);
324
+ const settingContext = await prepareDimensionAbacSetting(options);
325
+ const main = await runSetupMainPath(options, assumeYes, cliMode, settingContext);
301
326
  outcome = main.outcome;
302
327
  setupMode = main.setupMode;
303
328
  } catch (e) {