@aifabrix/builder 2.62.0 → 2.63.1

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 (34) hide show
  1. package/lib/api/onboarding-status.api.js +117 -0
  2. package/lib/app/register.js +24 -2
  3. package/lib/app/run-env-compose.js +39 -2
  4. package/lib/app/run-env-recovery.js +1 -0
  5. package/lib/app/run-helpers.js +1 -1
  6. package/lib/cli/infra-guided.js +30 -2
  7. package/lib/cli/setup-platform.js +18 -3
  8. package/lib/commands/setup-modes.js +2 -1
  9. package/lib/commands/setup-onboarding-readiness.js +83 -0
  10. package/lib/commands/setup.js +32 -10
  11. package/lib/commands/up-dataplane-bootstrap.js +115 -38
  12. package/lib/commands/up-dataplane.js +4 -31
  13. package/lib/core/local-env-overrides.js +23 -2
  14. package/lib/deployment/installation/index.js +4 -1
  15. package/lib/deployment/installation/infra-catalog.js +0 -2
  16. package/lib/deployment/installation/local-environment-authorize.js +221 -0
  17. package/lib/deployment/installation/local-environment-snapshot.js +124 -0
  18. package/lib/deployment/installation/local-environment-stage.js +43 -66
  19. package/lib/generator/index.js +32 -9
  20. package/lib/parameters/rsa-secret-values.js +14 -31
  21. package/lib/schema/infra.parameter.yaml +2 -45
  22. package/lib/schema/infrastructure-schema.json +0 -58
  23. package/lib/utils/dataplane-dimension-abac-setting.js +120 -0
  24. package/lib/utils/log-redaction.js +4 -1
  25. package/package.json +2 -1
  26. package/templates/README.md +6 -0
  27. package/templates/applications/builder-api/application.yaml +1 -1
  28. package/templates/applications/dataplane/application.yaml +1 -1
  29. package/templates/applications/dataplane/env.template +1 -1
  30. package/templates/applications/mirrored-templates.json +2 -2
  31. package/templates/applications/miso-controller/application.yaml +18 -1
  32. package/templates/applications/miso-controller/env.template +3 -21
  33. package/templates/applications/miso-controller/rbac.yaml +10 -6
  34. package/templates/marketplace/main.json +5 -69
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Read the public Miso required-baseline onboarding projection.
3
+ *
4
+ * @fileoverview Public Miso onboarding readiness client
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { ApiClient } = require('./index');
12
+
13
+ const ENDPOINT = '/api/v1/health/onboarding-status';
14
+ const PROJECTION_KEYS = ['code', 'retryable', 'state'];
15
+ const VALID_PROJECTIONS = new Set([
16
+ 'pending|null|true',
17
+ 'complete|null|false',
18
+ 'incomplete|baseline-dimensions-incomplete|true',
19
+ 'incomplete|baseline-roles-incomplete|true'
20
+ ]);
21
+ const VALID_PAIRS = new Set([
22
+ 'pending|null|true::pending|null|true',
23
+ 'incomplete|baseline-dimensions-incomplete|true::pending|null|true',
24
+ 'complete|null|false::pending|null|true',
25
+ 'complete|null|false::incomplete|baseline-roles-incomplete|true',
26
+ 'complete|null|false::complete|null|false'
27
+ ]);
28
+
29
+ function onboardingStatusError(message, code = 'MISO_ONBOARDING_STATUS_INVALID') {
30
+ const error = new Error(message);
31
+ error.code = code;
32
+ return error;
33
+ }
34
+
35
+ function projectionSignature(projection) {
36
+ return `${projection.state}|${projection.code === null ? 'null' : projection.code}|${projection.retryable}`;
37
+ }
38
+
39
+ function hasValidProjectionTypes(projection) {
40
+ return typeof projection.state === 'string' &&
41
+ (projection.code === null || typeof projection.code === 'string') &&
42
+ typeof projection.retryable === 'boolean';
43
+ }
44
+
45
+ function parseProjection(value, name) {
46
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
47
+ throw onboardingStatusError(`Miso ${name} readiness projection is missing or malformed.`);
48
+ }
49
+ const keys = Object.keys(value).sort();
50
+ if (keys.length !== PROJECTION_KEYS.length || keys.some((key, index) => key !== PROJECTION_KEYS[index])) {
51
+ throw onboardingStatusError(`Miso ${name} readiness projection has unsupported fields.`);
52
+ }
53
+ const projection = { state: value.state, code: value.code, retryable: value.retryable };
54
+ if (!hasValidProjectionTypes(projection)) {
55
+ throw onboardingStatusError(`Miso ${name} readiness projection has invalid field types.`);
56
+ }
57
+ if (!VALID_PROJECTIONS.has(projectionSignature(projection))) {
58
+ throw onboardingStatusError(`Miso ${name} readiness projection has an unsupported state.`);
59
+ }
60
+ return Object.freeze(projection);
61
+ }
62
+
63
+ function parseOnboardingStatusResponse(response) {
64
+ if (!response || response.success !== true || response.status !== 200) {
65
+ throw onboardingStatusError(
66
+ 'Miso onboarding readiness is unavailable.',
67
+ 'MISO_ONBOARDING_STATUS_UNAVAILABLE'
68
+ );
69
+ }
70
+ const wireBody = response.data;
71
+ if (!wireBody || typeof wireBody !== 'object' || Array.isArray(wireBody) ||
72
+ Object.prototype.hasOwnProperty.call(wireBody, 'success')) {
73
+ throw onboardingStatusError('Miso onboarding readiness response envelope is malformed.');
74
+ }
75
+ const data = wireBody.data;
76
+ if (!data || typeof data !== 'object' || Array.isArray(data) ||
77
+ Object.prototype.hasOwnProperty.call(data, 'success')) {
78
+ throw onboardingStatusError('Miso onboarding readiness data envelope is malformed.');
79
+ }
80
+ const baselineDimensions = parseProjection(data.baselineDimensions, 'Dimension');
81
+ const baselineRoles = parseProjection(data.baselineRoles, 'Business Role');
82
+ const pair = `${projectionSignature(baselineDimensions)}::${projectionSignature(baselineRoles)}`;
83
+ if (!VALID_PAIRS.has(pair)) {
84
+ throw onboardingStatusError('Miso onboarding readiness contains an unsupported step ordering.');
85
+ }
86
+ return Object.freeze({ baselineDimensions, baselineRoles });
87
+ }
88
+
89
+ /**
90
+ * Fetch required-baseline readiness from Miso Controller.
91
+ * GET /api/v1/health/onboarding-status
92
+ * @requiresPermission {Controller} Public
93
+ * @param {string} controllerUrl
94
+ * @param {{timeoutMs?: number, client?: ApiClient}} [options]
95
+ * @returns {Promise<{baselineDimensions: Object, baselineRoles: Object}>}
96
+ */
97
+ async function fetchOnboardingStatus(controllerUrl, options = {}) {
98
+ const client = options.client || new ApiClient(controllerUrl);
99
+ const requestOptions = {};
100
+ if (options.timeoutMs !== undefined) requestOptions.timeoutMs = options.timeoutMs;
101
+ let response;
102
+ try {
103
+ response = await client.get(ENDPOINT, requestOptions);
104
+ } catch {
105
+ throw onboardingStatusError(
106
+ 'Miso onboarding readiness request failed.',
107
+ 'MISO_ONBOARDING_STATUS_UNAVAILABLE'
108
+ );
109
+ }
110
+ return parseOnboardingStatusResponse(response);
111
+ }
112
+
113
+ module.exports = {
114
+ ENDPOINT,
115
+ fetchOnboardingStatus,
116
+ parseOnboardingStatusResponse
117
+ };
@@ -28,6 +28,24 @@ const { refreshUrlsLocalRegistryFromBuilder } = require('../utils/urls-local-reg
28
28
  const { resolveControllerUrl } = require('../utils/controller-url');
29
29
  const config = require('../core/config');
30
30
 
31
+ /**
32
+ * Handle local credential persistence failure without exposing details in strict mode.
33
+ * @param {Error} error
34
+ * @param {boolean} required
35
+ * @returns {false}
36
+ */
37
+ function handleCredentialPersistenceFailure(error, required) {
38
+ if (required) {
39
+ const persistenceError = new Error(
40
+ 'Application credentials could not be saved to the supported local secret store.'
41
+ );
42
+ persistenceError.code = 'LOCAL_CREDENTIAL_PERSISTENCE_FAILED';
43
+ throw persistenceError;
44
+ }
45
+ logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
46
+ return false;
47
+ }
48
+
31
49
  /**
32
50
  * Build registration data payload from app configuration
33
51
  * @param {Object} appConfig - Application configuration
@@ -83,8 +101,11 @@ function buildRegistrationData(appConfig, options) {
83
101
  * @async
84
102
  * @param {Object} responseData - Registration response data
85
103
  * @param {string} apiUrl - API URL
104
+ * @param {Object} [options] - Persistence behavior
105
+ * @param {boolean} [options.required=false] - Throw a safe typed error when persistence fails
106
+ * @returns {Promise<boolean>} True when credentials were persisted
86
107
  */
87
- async function saveLocalCredentials(responseData, apiUrl) {
108
+ async function saveLocalCredentials(responseData, apiUrl, options = {}) {
88
109
  const registeredAppKey = responseData.application.key;
89
110
  const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
90
111
  const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
@@ -113,8 +134,9 @@ async function saveLocalCredentials(responseData, apiUrl) {
113
134
  logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
114
135
  }
115
136
  } catch (error) {
116
- logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
137
+ return handleCredentialPersistenceFailure(error, options.required === true);
117
138
  }
139
+ return true;
118
140
  }
119
141
 
120
142
  /**
@@ -20,6 +20,11 @@ const secretsEnsure = require('../core/secrets-ensure');
20
20
  const secretsEnvWrite = require('../core/secrets-env-write');
21
21
  const { getContainerPort } = require('../utils/port-resolver');
22
22
  const { applyAdminSecretsPlatformOverlay } = require('../core/admin-secrets-env-overlay');
23
+ const {
24
+ resolveDimensionAbacSetting,
25
+ applyDimensionAbacToEnvMap
26
+ } = require('../utils/dataplane-dimension-abac-setting');
27
+ const { resolveEffectiveRegistry } = require('../utils/resolve-docker-image-ref');
23
28
 
24
29
  /**
25
30
  * Merge app env with admin-secrets; overlay ensures platform keys come from admin file.
@@ -38,6 +43,13 @@ async function applyOnboardingPlatformModeIfNeeded(appName, merged) {
38
43
  merged.ONBOARDING_FULL_PLATFORM = mode === 'full' ? 'true' : 'false';
39
44
  }
40
45
 
46
+ async function applyDimensionAbacSettingIfNeeded(appName, merged) {
47
+ if (appName !== 'dataplane') return;
48
+ const userConfig = await config.getConfig();
49
+ const resolved = resolveDimensionAbacSetting(userConfig);
50
+ applyDimensionAbacToEnvMap(appName, merged, resolved.value);
51
+ }
52
+
41
53
  /**
42
54
  * Backfill only secrets needed for `aifabrix run <app>`: kv:// keys from that app's env.template
43
55
  * (catalog defaults via placeholderContext). Does not run full `ensureInfraSecrets`.
@@ -260,6 +272,29 @@ async function applyPostgresPasswordFromPgpass(merged, developerId) {
260
272
  if (pgpassPwd !== undefined) merged.POSTGRES_PASSWORD = pgpassPwd;
261
273
  }
262
274
 
275
+ /**
276
+ * Deliver existing local installation inputs to privileged Miso bootstrap only.
277
+ * @param {string} appName
278
+ * @param {Object} merged
279
+ * @param {Object} appConfig
280
+ * @param {Object|null} scopeOpts
281
+ */
282
+ function applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts) {
283
+ if (appName !== 'miso-controller') return;
284
+ const postgresPassword = merged.POSTGRES_PASSWORD;
285
+ if (typeof postgresPassword !== 'string' || postgresPassword.trim().length === 0) {
286
+ throw new Error(
287
+ 'Local PostgreSQL administrator password is unavailable. Re-run "aifabrix up-infra" before starting Miso Controller.'
288
+ );
289
+ }
290
+ merged.POSTGRES_ADMIN_PASSWORD = postgresPassword;
291
+ const { registry } = resolveEffectiveRegistry(appConfig, {
292
+ registry: scopeOpts?.registry
293
+ });
294
+ if (registry) merged.ACR_LOGIN_SERVER = registry;
295
+ else delete merged.ACR_LOGIN_SERVER;
296
+ }
297
+
263
298
  /**
264
299
  * @param {string} runEnvPath
265
300
  * @param {string} runEnvAdminPath
@@ -300,8 +335,10 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId
300
335
  mergeRunEnvWithAdminOverlay(merged, adminObj, appName);
301
336
  await applyOnboardingPlatformModeIfNeeded(appName, merged);
302
337
  await applyPostgresPasswordFromPgpass(merged, developerId);
338
+ applyMisoBootstrapInputsIfNeeded(appName, merged, appConfig, scopeOpts);
303
339
  injectDatabaseNamesAndUsers(merged, appConfig, scopeOpts);
304
340
  injectContainerPortForRun(merged, appConfig, appName);
341
+ await applyDimensionAbacSettingIfNeeded(appName, merged);
305
342
 
306
343
  const runEnvPath = path.join(devDir, '.env.run');
307
344
  const runEnvAdminPath = path.join(devDir, '.env.run.admin');
@@ -351,9 +388,9 @@ async function ensureRunEnvFilesPresent(
351
388
  * @throws {Error} If password keys appear in environment-like assignment
352
389
  */
353
390
  function assertNoPasswordLiteralsInCompose(composeContent) {
354
- const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
391
+ const badPattern = /\n\s+(-?\s*)(POSTGRES_PASSWORD|POSTGRES_ADMIN_PASSWORD|DB_\d+_PASSWORD)\s*[:=]/;
355
392
  if (badPattern.test(composeContent)) {
356
- throw new Error('Generated compose must not contain password literals (POSTGRES_PASSWORD, DB_*_PASSWORD). Use env_file only.');
393
+ throw new Error('Generated compose must not contain password literals (PostgreSQL or application database passwords). Use env_file only.');
357
394
  }
358
395
  }
359
396
 
@@ -26,6 +26,7 @@ function buildRunEnvRecovery(appName, prepared) {
26
26
  prepared.appConfig.developerId,
27
27
  {
28
28
  runEnvKey: prepared.mergedRunOptions.env,
29
+ registry: prepared.mergedRunOptions.registry,
29
30
  effectiveEnvironmentScopedResources:
30
31
  prepared.mergedRunOptions.effectiveEnvironmentScopedResources
31
32
  }
@@ -366,7 +366,7 @@ async function prepareEnvironment(appName, appConfig, options) {
366
366
  appConfig,
367
367
  devDir,
368
368
  developerId,
369
- { runEnvKey, effectiveEnvironmentScopedResources }
369
+ { runEnvKey, effectiveEnvironmentScopedResources, registry: options.registry }
370
370
  );
371
371
  const { hydrateMissingLocalParameters } = require('./run-parameter-sync');
372
372
  await hydrateMissingLocalParameters(appName, runEnvPath);
@@ -49,6 +49,34 @@ const {
49
49
  emitSystemBuilderAppManifestLineIfTTY
50
50
  } = require('../utils/manifest-source-emit');
51
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
+ }
52
80
 
53
81
  /**
54
82
  * Guided Keycloak → Miso Controller start with one spinner per app (no second health wait).
@@ -327,8 +355,7 @@ async function runGuidedUpPlatform(options, handleUpMiso, handleUpDataplane, han
327
355
  skipLoginIfAuthenticated: options && options.skipLoginIfAuthenticated === true
328
356
  };
329
357
 
330
- await withMutedLogger(() => prepareUrlsLocalRegistryForUpPlatform());
331
- await runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso);
358
+ await runGuidedMisoAndReadinessPhase(options, handleUpMiso, authOpts);
332
359
  await runGuidedKeycloakRealmPhase(options);
333
360
  await runGuidedAuthStep(handleLogin, authOpts, { startSpinner, stopSpinnerSuccess });
334
361
  const controllerUrl = await resolveGuidedAuthControllerUrl(authOpts);
@@ -388,6 +415,7 @@ module.exports = {
388
415
  runGuidedUpMiso,
389
416
  runGuidedUpDataplane,
390
417
  runGuidedUpPlatform,
418
+ runRequiredBaselineReadinessIfNeeded,
391
419
  runGuidedDownInfra,
392
420
  stripHealthPrefix
393
421
  };
@@ -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).
@@ -64,6 +74,11 @@ function registerSetup(program) {
64
74
  .option('--integration-server', 'Install the integration server (runs once in the miso environment)')
65
75
  .option('--no-integration-server', 'Do not install the integration server (default)')
66
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
+ )
67
82
  .option('-y, --yes', 'Skip destructive confirmation prompts (re-install, wipe data, teardown)')
68
83
  .addHelpText('after', SETUP_HELP_AFTER)
69
84
  .action(async(options) => {
@@ -296,7 +296,8 @@ async function runUpPlatform(opts = {}) {
296
296
  base: true,
297
297
  platformControllerUrl: authCtx.platformControllerUrl,
298
298
  skipLoginIfAuthenticated: authCtx.skipLoginIfAuthenticated,
299
- requireKeycloakRealm: opts.requireKeycloakRealm === true
299
+ requireKeycloakRealm: opts.requireKeycloakRealm === true,
300
+ requireBaselineReadiness: true
300
301
  },
301
302
  upMiso.handleUpMiso,
302
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
+ };
@@ -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 };
@@ -120,12 +139,13 @@ async function applySetupPlatformMode(options) {
120
139
  return mode;
121
140
  }
122
141
 
123
- async function runFreshInstallFlow(options) {
142
+ async function runFreshInstallFlow(options, settingContext) {
124
143
  await maybePinDeveloperIdForFreshInstall(options.developer);
125
144
  logger.log(infoLine('No infrastructure detected; starting fresh install.'));
126
145
  await applySetupPlatformMode(options);
127
146
  const adminCreds = await setupPrompts.promptAdminCredentials(options);
128
147
  await config.setAdminEmail(adminCreds.adminEmail);
148
+ await persistDimensionAbacSelection(settingContext);
129
149
  await setupModes.runFreshInstall(adminCreds);
130
150
  logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
131
151
  }
@@ -134,7 +154,7 @@ async function runFreshInstallFlow(options) {
134
154
  * @param {boolean} assumeYes
135
155
  * @returns {Promise<{ aborted: boolean, mode: string|null }>}
136
156
  */
137
- async function runExistingInfraFlow(assumeYes) {
157
+ async function runExistingInfraFlow(assumeYes, settingContext) {
138
158
  const mode = await setupPrompts.promptModeSelection();
139
159
  const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
140
160
  if (!proceed) {
@@ -145,6 +165,7 @@ async function runExistingInfraFlow(assumeYes) {
145
165
  if (mode === MODE.REINSTALL || mode === MODE.WIPE_DATA) {
146
166
  await applySetupPlatformMode({});
147
167
  }
168
+ await persistDimensionAbacSelection(settingContext);
148
169
  await dispatchMode(mode);
149
170
  logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
150
171
  return { aborted: false, mode };
@@ -197,20 +218,19 @@ async function dispatchMode(mode) {
197
218
  * @param {boolean} assumeYes
198
219
  * @returns {Promise<{ outcome: string, setupMode: string|null }>}
199
220
  */
200
- async function runSetupMainPath(options, assumeYes) {
201
- const cliMode = normalizeCliMode(options);
221
+ async function runSetupMainPath(options, assumeYes, cliMode, settingContext) {
202
222
  if (cliMode) {
203
- return runSetupWithCliMode(options, cliMode);
223
+ return runSetupWithCliMode(options, cliMode, settingContext);
204
224
  }
205
225
 
206
226
  logSetupHeader();
207
227
  logger.log(formatProgress('Detecting current installation state...'));
208
228
  const running = await isInfraRunning();
209
229
  if (!running) {
210
- await runFreshInstallFlow(options);
230
+ await runFreshInstallFlow(options, settingContext);
211
231
  return { outcome: 'success', setupMode: 'fresh' };
212
232
  }
213
- const r = await runExistingInfraFlow(assumeYes);
233
+ const r = await runExistingInfraFlow(assumeYes, settingContext);
214
234
  const outcome = r.aborted ? 'aborted' : 'success';
215
235
  return { outcome, setupMode: r.mode };
216
236
  }
@@ -300,7 +320,9 @@ async function handleSetup(options = {}) {
300
320
 
301
321
  beginSetupCommandFlow();
302
322
  try {
303
- 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);
304
326
  outcome = main.outcome;
305
327
  setupMode = main.setupMode;
306
328
  } catch (e) {