@aifabrix/builder 2.56.0 → 2.58.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 (74) hide show
  1. package/lib/agent-kit/session-report.js +76 -0
  2. package/lib/agent-kit/start.js +124 -57
  3. package/lib/agent-kit/tmux-sessions.js +163 -0
  4. package/lib/api/environments.api.js +38 -0
  5. package/lib/api/marketplace-bootstrap.api.js +31 -0
  6. package/lib/app/push.js +98 -41
  7. package/lib/build/standard-docker-build.js +13 -11
  8. package/lib/cli/setup-app.js +16 -5
  9. package/lib/cli/setup-dev-path-commands.js +49 -2
  10. package/lib/cli/setup-environment.js +89 -19
  11. package/lib/commands/agent-kit-help.js +32 -15
  12. package/lib/commands/agent-kit.js +90 -8
  13. package/lib/commands/dev-registry.js +155 -0
  14. package/lib/commands/dev-show-display.js +10 -2
  15. package/lib/commands/setup-image-refresh.js +26 -0
  16. package/lib/core/config-attach-extensions.js +4 -1
  17. package/lib/deployment/installation/azure-infra-stage.js +191 -0
  18. package/lib/deployment/installation/azure-preflight.js +147 -0
  19. package/lib/deployment/installation/azure-readiness.js +101 -0
  20. package/lib/deployment/installation/docker-register.js +112 -0
  21. package/lib/deployment/installation/environment-auth.js +76 -0
  22. package/lib/deployment/installation/environment-manifest.js +165 -0
  23. package/lib/deployment/installation/environment-stage.js +191 -0
  24. package/lib/deployment/installation/index.js +39 -0
  25. package/lib/deployment/installation/infra-catalog.js +211 -0
  26. package/lib/deployment/installation/infra-manifest.js +176 -0
  27. package/lib/deployment/installation/infra-projection.js +97 -0
  28. package/lib/deployment/installation/public-ip.js +65 -0
  29. package/lib/deployment/installation/registry-auth-mode.js +98 -0
  30. package/lib/deployment/installation/stage-input.js +184 -0
  31. package/lib/deployment/installation/stage-output.js +53 -0
  32. package/lib/deployment/installation/workspace-env.js +76 -0
  33. package/lib/deployment/installation/workspace-files.js +178 -0
  34. package/lib/deployment/installation/workspace-schema.js +40 -0
  35. package/lib/deployment/installation/workspace-secrets.js +66 -0
  36. package/lib/deployment/installation/workspace.js +128 -0
  37. package/lib/deployment/marketplace-environment.js +305 -0
  38. package/lib/generator/builders.js +12 -9
  39. package/lib/generator/helpers.js +55 -56
  40. package/lib/generator/split-variables.js +11 -5
  41. package/lib/internal/fs-real-sync.js +29 -0
  42. package/lib/role-assistant/package-validate.js +4 -0
  43. package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
  44. package/lib/role-assistant/test-runner.js +14 -17
  45. package/lib/schema/application-schema.json +20 -2
  46. package/lib/schema/infra-parameter.schema.json +7 -0
  47. package/lib/schema/infra.parameter.yaml +26 -36
  48. package/lib/schema/infrastructure-schema.json +1531 -397
  49. package/lib/schema/installation-environment.schema.json +601 -0
  50. package/lib/utils/app-register-config.js +2 -4
  51. package/lib/utils/build-resolve-image.js +2 -21
  52. package/lib/utils/cli-utils.js +8 -1
  53. package/lib/utils/config-registry-preference.js +106 -0
  54. package/lib/utils/docker-build.js +53 -19
  55. package/lib/utils/env-map.js +23 -6
  56. package/lib/utils/image-tags.js +88 -0
  57. package/lib/utils/promote-dev-docker-image.js +6 -0
  58. package/lib/utils/registry-credentials.js +236 -0
  59. package/lib/utils/resolve-docker-image-ref.js +78 -9
  60. package/lib/utils/secrets-helpers.js +9 -1
  61. package/lib/utils/variable-transformer.js +5 -4
  62. package/lib/validation/required-variables.js +41 -0
  63. package/lib/validation/validator.js +8 -5
  64. package/package.json +6 -1
  65. package/templates/README.md +4 -0
  66. package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
  67. package/templates/agent-kit/skills/shared/hosts.md +10 -10
  68. package/templates/applications/dataplane/application.yaml +1 -1
  69. package/templates/applications/dataplane/env.template +1 -1
  70. package/templates/applications/miso-controller/application.yaml +3 -2
  71. package/templates/applications/miso-controller/env.template +15 -6
  72. package/templates/marketplace/createUiDefinition.json +77 -0
  73. package/templates/marketplace/install.parameters.example.json +9 -0
  74. package/templates/marketplace/main.json +4087 -0
@@ -1,15 +1,18 @@
1
- /**
2
- * `aifabrix agent-kit` command group.
3
- * @fileoverview Install, init, status, update, validate, remote create, start
4
- */
1
+ /** `aifabrix agent-kit` command group. */
5
2
 
6
3
  'use strict';
7
4
 
8
5
  const logger = require('../utils/logger');
9
6
  const { handleCommandError } = require('../utils/cli-utils');
10
- const { formatSuccessLine, formatBlockingError, formatWarningLine } = require('../utils/cli-layout-chalk');
7
+ const {
8
+ formatSuccessLine,
9
+ formatBlockingError,
10
+ formatWarningLine
11
+ } = require('../utils/cli-layout-chalk');
11
12
  const { runInstall, runUpdate } = require('../agent-kit/run');
12
13
  const { runStart } = require('../agent-kit/start');
14
+ const { runList, runKill } = require('../agent-kit/tmux-sessions');
15
+ const { formatSessionList, formatMissingKillName } = require('../agent-kit/session-report');
13
16
  const path = require('path');
14
17
  const { runInit, runRemoteCreate } = require('../agent-kit/init');
15
18
  const { enrichInitHostOptions, enrichInitRemoteOptions, enrichRemoteOptions, enrichApproveAllOptions } = require('../agent-kit/remote-prompts');
@@ -30,6 +33,8 @@ const {
30
33
  REMOTE_GROUP_HELP,
31
34
  REMOTE_HELP,
32
35
  START_HELP,
36
+ LIST_HELP,
37
+ KILL_HELP,
33
38
  HOST_OPTION_HELP,
34
39
  APPROVE_ALL_OPTION_HELP
35
40
  } = require('./agent-kit-help');
@@ -110,6 +115,8 @@ function emitStartReport(json, asJson) {
110
115
  logger.log(`Session ${json.session}`);
111
116
  if (json.remoteControlName) {
112
117
  logger.log(`Remote ${json.remoteControlName}`);
118
+ } else if (json.remoteControl) {
119
+ logger.log('Remote Codex Remote Control');
113
120
  }
114
121
  logger.log(`Path ${json.path}`);
115
122
  logger.log(`Launch ${json.launch.join(' ')}`);
@@ -124,6 +131,32 @@ function emitStartReport(json, asJson) {
124
131
  logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Ready'));
125
132
  }
126
133
 
134
+ /**
135
+ * @param {object} json
136
+ * @param {boolean} asJson
137
+ * @returns {void}
138
+ */
139
+ function emitListReport(json, asJson) {
140
+ if (asJson) {
141
+ logger.log(JSON.stringify(json, null, 2));
142
+ return;
143
+ }
144
+ logger.log(formatSessionList(json));
145
+ }
146
+
147
+ /**
148
+ * @param {object} json
149
+ * @param {boolean} asJson
150
+ * @returns {void}
151
+ */
152
+ function emitKillReport(json, asJson) {
153
+ if (asJson) {
154
+ logger.log(JSON.stringify(json, null, 2));
155
+ return;
156
+ }
157
+ logger.log(formatSuccessLine(`Stopped tmux session ${json.session}`));
158
+ }
159
+
127
160
  /**
128
161
  * @param {Function} runner
129
162
  * @param {object} options
@@ -159,6 +192,8 @@ function setupAgentKitCommands(program) {
159
192
  addValidateCommand(group);
160
193
  addRemoteCommand(group);
161
194
  addStartCommand(group);
195
+ addListCommand(group);
196
+ addKillCommand(group);
162
197
  }
163
198
 
164
199
  /**
@@ -387,8 +422,8 @@ async function runValidateLeaf(options) {
387
422
  function addStartCommand(group) {
388
423
  group
389
424
  .command('start <app>')
390
- .description('Start Claude Code or Codex in tmux (does not install skills)')
391
- .option('--approve-all', 'Write auto-approve config for this app only')
425
+ .description('Start Claude Code or Codex with Remote Control in named tmux')
426
+ .option('--approve-all', 'Write auto-approve config (default for start)')
392
427
  .option('--skip-approve-all', 'Do not write auto-approve configs')
393
428
  .option('--yes', 'Same as --approve-all')
394
429
  .option('--detach', 'Create the tmux session in the background (no attach)')
@@ -398,8 +433,10 @@ function addStartCommand(group) {
398
433
  )
399
434
  .option(
400
435
  '--remote-control',
401
- 'Claude Code only: same session on this PC, claude.ai/code, and the Claude mobile app'
436
+ 'Enable native Remote Control for Claude or Codex (default)',
437
+ true
402
438
  )
439
+ .option('--no-remote-control', 'Start a local tmux-only session')
403
440
  .option('--dry-run', 'Print the launch command without starting it')
404
441
  .option('--json', 'Machine-readable output')
405
442
  .addHelpText('after', START_HELP)
@@ -414,4 +451,49 @@ function addStartCommand(group) {
414
451
  });
415
452
  }
416
453
 
454
+ /**
455
+ * @param {import('commander').Command} group
456
+ */
457
+ function addListCommand(group) {
458
+ group
459
+ .command('list')
460
+ .description('List active tmux sessions for the current user')
461
+ .option('--json', 'Machine-readable output')
462
+ .addHelpText('after', LIST_HELP)
463
+ .action(async(options) => {
464
+ try {
465
+ emitListReport(runList(options), Boolean(options.json));
466
+ } catch (error) {
467
+ handleCommandError(error, 'agent-kit list');
468
+ process.exitCode = 1;
469
+ }
470
+ });
471
+ }
472
+
473
+ /**
474
+ * @param {import('commander').Command} group
475
+ */
476
+ function addKillCommand(group) {
477
+ group
478
+ .command('kill [name]')
479
+ .description('Stop one tmux session by exact name')
480
+ .option('--json', 'Machine-readable output')
481
+ .addHelpText('after', KILL_HELP)
482
+ .action(async(name, options) => {
483
+ if (!name) {
484
+ const report = formatMissingKillName(Boolean(options.json));
485
+ logger.error(report.stderr);
486
+ if (report.stdout) logger.log(report.stdout);
487
+ process.exitCode = 1;
488
+ return;
489
+ }
490
+ try {
491
+ emitKillReport(runKill({ ...options, name }), Boolean(options.json));
492
+ } catch (error) {
493
+ handleCommandError(error, 'agent-kit kill');
494
+ process.exitCode = 1;
495
+ }
496
+ });
497
+ }
498
+
417
499
  module.exports = { setupAgentKitCommands };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `aifabrix dev set-registry` / `dev print-registry` logic.
3
+ *
4
+ * @fileoverview Installation registry preference and host-scoped credential management
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const config = require('../core/config');
12
+ const logger = require('../utils/logger');
13
+ const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
14
+ const { normalizeRegistryHost } = require('../utils/config-registry-preference');
15
+ const registryCredentials = require('../utils/registry-credentials');
16
+
17
+ /**
18
+ * Reads the whole of stdin (for --password-stdin).
19
+ * @param {NodeJS.ReadableStream} [stream] - Input stream
20
+ * @returns {Promise<string>}
21
+ */
22
+ function readAllStdin(stream = process.stdin) {
23
+ return new Promise((resolve, reject) => {
24
+ let data = '';
25
+ stream.setEncoding('utf8');
26
+ stream.on('data', (chunk) => {
27
+ data += chunk;
28
+ });
29
+ stream.on('end', () => resolve(data));
30
+ stream.on('error', reject);
31
+ });
32
+ }
33
+
34
+ function hasOption(value) {
35
+ return value !== undefined && value !== null;
36
+ }
37
+
38
+ function assertModeOptions(registry, opts) {
39
+ const credentialOption = hasOption(opts.username) || hasOption(opts.password) || opts.passwordStdin === true;
40
+ if (opts.clear && opts.clearCredentials) {
41
+ throw new Error('Use either --clear or --clear-credentials, not both');
42
+ }
43
+ if ((opts.clear || opts.clearCredentials) && credentialOption) {
44
+ throw new Error('--clear and --clear-credentials cannot be combined with --username or a password option');
45
+ }
46
+ if (opts.clear && hasOption(registry)) {
47
+ throw new Error('--clear removes the saved registry and takes no registry argument');
48
+ }
49
+ if (!opts.clear && !(typeof registry === 'string' && registry.trim())) {
50
+ throw new Error('Registry is required (for example: aifabrix dev set-registry myregistry.azurecr.io)');
51
+ }
52
+ }
53
+
54
+ function assertCredentialOptions(opts) {
55
+ if (hasOption(opts.password) && opts.passwordStdin === true) {
56
+ throw new Error('Use either --password or --password-stdin, not both');
57
+ }
58
+ const passwordGiven = hasOption(opts.password) || opts.passwordStdin === true;
59
+ if (hasOption(opts.username) !== passwordGiven) {
60
+ throw new Error('--username and a password (--password or --password-stdin) must be supplied together');
61
+ }
62
+ if (hasOption(opts.username) && !String(opts.username).trim()) {
63
+ throw new Error('--username must not be empty');
64
+ }
65
+ if (hasOption(opts.password) && String(opts.password) === '') {
66
+ throw new Error('--password must not be empty');
67
+ }
68
+ return passwordGiven;
69
+ }
70
+
71
+ async function resolvePassword(opts, readStdin) {
72
+ if (opts.passwordStdin !== true) return String(opts.password);
73
+ const password = (await readStdin()).replace(/\r?\n$/, '');
74
+ if (!password) {
75
+ throw new Error('--password-stdin received an empty password');
76
+ }
77
+ return password;
78
+ }
79
+
80
+ async function saveWithCredentials(host, username, password) {
81
+ const saved = await registryCredentials.saveRegistryCredentials(host, username, password);
82
+ try {
83
+ await config.setRegistry(host);
84
+ } catch (error) {
85
+ saved.rollback();
86
+ throw new Error(`Could not save the registry preference; credentials for ${host} were restored (${error.message})`);
87
+ }
88
+ }
89
+
90
+ async function runClearMode(registry, opts) {
91
+ if (opts.clear) {
92
+ await config.clearRegistry();
93
+ logger.log(formatSuccessLine('Registry cleared (application image.registry applies again)'));
94
+ return { action: 'clear', registry: null, credentials: false };
95
+ }
96
+ const host = normalizeRegistryHost(registry);
97
+ const removed = registryCredentials.removeRegistryCredentials(host);
98
+ logger.log(formatSuccessLine(removed ? `Credentials removed for ${host}` : `No stored credentials for ${host}`));
99
+ return { action: 'clear-credentials', registry: host, credentials: false };
100
+ }
101
+
102
+ /**
103
+ * Runs `dev set-registry`.
104
+ * @param {string|undefined} registry - Positional registry
105
+ * @param {Object} opts - Commander options
106
+ * @param {Object} [deps] - `readStdin` override (tests)
107
+ * @returns {Promise<{ action: string, registry: string|null, credentials: boolean }>}
108
+ */
109
+ async function runSetRegistry(registry, opts = {}, deps = {}) {
110
+ assertModeOptions(registry, opts);
111
+ if (opts.clear || opts.clearCredentials) return runClearMode(registry, opts);
112
+ const host = normalizeRegistryHost(registry);
113
+ const withCredentials = assertCredentialOptions(opts);
114
+ if (!withCredentials) {
115
+ await config.setRegistry(host);
116
+ logger.log(formatSuccessLine(`Registry set to ${host}`));
117
+ return { action: 'set', registry: host, credentials: false };
118
+ }
119
+ const password = await resolvePassword(opts, deps.readStdin || readAllStdin);
120
+ await saveWithCredentials(host, String(opts.username).trim(), password);
121
+ logger.log(formatSuccessLine(`Registry set to ${host}`));
122
+ logger.log(formatSuccessLine(`Credentials saved for ${host} in your local secrets file (encrypted)`));
123
+ if (opts.passwordStdin !== true) {
124
+ logger.log(formatWarningLine('--password can appear in shell history and process listings; prefer --password-stdin'));
125
+ }
126
+ return { action: 'set', registry: host, credentials: true };
127
+ }
128
+
129
+ /**
130
+ * `dev print-registry`: the saved value plus a newline, or an empty line. Stdout only.
131
+ * @returns {Promise<void>}
132
+ */
133
+ async function runPrintRegistry() {
134
+ const registry = await config.getRegistry();
135
+ process.stdout.write(`${registry || ''}\n`);
136
+ }
137
+
138
+ /**
139
+ * Safe credential status label for `dev show` (never decrypts).
140
+ * @param {string|null} registry - Saved registry
141
+ * @returns {string|null}
142
+ */
143
+ function registryCredentialLabel(registry) {
144
+ if (!registry) return null;
145
+ try {
146
+ const status = registryCredentials.getRegistryCredentialStatus(registry);
147
+ if (status === 'complete') return 'configured';
148
+ if (status === 'absent') return 'not configured';
149
+ return `incomplete (run: aifabrix dev set-registry ${registry} --clear-credentials)`;
150
+ } catch {
151
+ return 'unknown';
152
+ }
153
+ }
154
+
155
+ module.exports = { runSetRegistry, runPrintRegistry, registryCredentialLabel, readAllStdin };
@@ -336,6 +336,8 @@ function logConfigurationSection(p) {
336
336
  logRow('Environment', p.environment);
337
337
  logRow('Controller', p.controller);
338
338
  logRow('Format', p.formatVal);
339
+ logRow('Registry', p.registry);
340
+ if (p.registry) logRow('Registry login', p.registryCredentials);
339
341
  logRow('Platform mode', p.setupPlatformMode);
340
342
  logRow('Traefik', p.traefikEnabled ? 'on' : 'off');
341
343
  logRow('Scoped resources', p.scopedResourcesLabel);
@@ -389,14 +391,16 @@ async function loadDevShowData(devIdStr) {
389
391
  const ports = devConfig.getDevPorts(Number.isNaN(devIdNum) ? 0 : devIdNum);
390
392
  const [
391
393
  environment, tlsEnabled, controller, formatRaw, useScoped, secretsRaw, setupPlatformMode, traefikEnabled,
392
- remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost
394
+ remoteServer, dockerEndpoint, dockerTlsSkipVerify, mutagenFolder, syncSshUser, syncSshHost, registry
393
395
  ] = await Promise.all([
394
396
  config.getCurrentEnvironment(), config.getTlsEnabled(), config.getControllerUrl(),
395
397
  config.getFormat(), config.getUseEnvironmentScopedResources(), config.getAifabrixSecretsPath(),
396
398
  config.getSetupPlatformMode(), config.getTraefikEnabled(),
397
399
  config.getRemoteServer(), config.getDockerEndpoint(), config.getDockerTlsSkipVerify(),
398
- config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost()
400
+ config.getUserMutagenFolder(), config.getSyncSshUser(), config.getSyncSshHost(),
401
+ config.getRegistry ? config.getRegistry() : null
399
402
  ]);
403
+ const { registryCredentialLabel } = require('./dev-registry');
400
404
  const urlsLocalPath = getUrlsLocalYamlPath();
401
405
  const scopedResourcesLabel = useScoped ? 'on' : 'off (default)';
402
406
  const workResolved = paths.getAifabrixWork();
@@ -412,6 +416,8 @@ async function loadDevShowData(devIdStr) {
412
416
  tlsEnabled,
413
417
  controller,
414
418
  formatVal: formatRaw ?? null,
419
+ registry: registry || null,
420
+ registryCredentials: registryCredentialLabel(registry || null),
415
421
  scopedResourcesLabel,
416
422
  setupPlatformMode,
417
423
  traefikEnabled,
@@ -451,6 +457,8 @@ function renderDevShow(data) {
451
457
  tlsEnabled: data.tlsEnabled,
452
458
  controller: data.controller,
453
459
  formatVal: data.formatVal,
460
+ registry: data.registry,
461
+ registryCredentials: data.registryCredentials,
454
462
  setupPlatformMode: data.setupPlatformMode,
455
463
  traefikEnabled: data.traefikEnabled,
456
464
  scopedResourcesLabel: data.scopedResourcesLabel,
@@ -21,9 +21,30 @@ const infraHelpers = require('../infrastructure/helpers');
21
21
  const upCommon = require('./up-common');
22
22
  const { sectionTitle, formatDatasourceListRow } = require('../utils/cli-test-layout-chalk');
23
23
  const { startSpinner, stopSpinnerSuccess } = require('./setup-spinners');
24
+ const { ensureLoginForImageRefs } = require('../utils/registry-credentials');
24
25
 
25
26
  const PLATFORM_APPS = ['keycloak', 'miso-controller', 'dataplane'];
26
27
 
28
+ /**
29
+ * `image:` values of a compose file (plain scan; variables are left unresolved and skipped).
30
+ * @param {string} composePath - Compose file path
31
+ * @returns {string[]}
32
+ */
33
+ function readComposeImageRefs(composePath) {
34
+ let text = '';
35
+ try {
36
+ text = String(fs.readFileSync(composePath, 'utf8') || '');
37
+ } catch {
38
+ return [];
39
+ }
40
+ const refs = [];
41
+ for (const line of text.split(/\r?\n/)) {
42
+ const match = line.match(/^\s*image:\s*["']?([^"'\s#]+)["']?/);
43
+ if (match && !match[1].includes('$')) refs.push(match[1]);
44
+ }
45
+ return refs;
46
+ }
47
+
27
48
  /**
28
49
  * Pull infra-compose images (postgres, redis, optional pgadmin/redis-commander/traefik).
29
50
  *
@@ -42,6 +63,8 @@ async function pullInfraImages(opts = {}) {
42
63
  }
43
64
  return;
44
65
  }
66
+ // Authenticate once per private registry host before compose pulls (stored pairs only).
67
+ await ensureLoginForImageRefs(readComposeImageRefs(composePath));
45
68
  const project = infraHelpers.getInfraProjectName(devId);
46
69
  const composeCmd = await dockerUtils.getComposeCommand();
47
70
  if (!quiet) {
@@ -91,6 +114,8 @@ async function pullPlatformAppImage(appName, opts = {}) {
91
114
  }
92
115
  return false;
93
116
  }
117
+ // A partial stored credential pair fails closed here instead of degrading to a warning.
118
+ await ensureLoginForImageRefs([imageRef]);
94
119
  try {
95
120
  if (!quiet) {
96
121
  const spin = startSpinner(`Pulling ${appName} image...`);
@@ -142,5 +167,6 @@ module.exports = {
142
167
  PLATFORM_APPS,
143
168
  pullInfraImages,
144
169
  pullPlatformImages,
170
+ readComposeImageRefs,
145
171
  runSetupImageRefresh
146
172
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Attach token, path, format, and scoped-resources helpers to config exports.
2
+ * Attach token, path, format, scoped-resources, and registry helpers to config exports.
3
3
  *
4
4
  * @fileoverview Keeps lib/core/config.js under max-lines
5
5
  * @author AI Fabrix Team
@@ -42,6 +42,9 @@ function attachConfigExtensions(exportsObj, deps) {
42
42
  const { createScopedResourcesPreferenceFunctions } = require('../utils/config-scoped-resources-preference');
43
43
  Object.assign(exportsObj, createScopedResourcesPreferenceFunctions(getConfig, saveConfig));
44
44
 
45
+ const { createRegistryPreferenceFunctions } = require('../utils/config-registry-preference');
46
+ Object.assign(exportsObj, createRegistryPreferenceFunctions(getConfig, saveConfig));
47
+
45
48
  const { createSecretsPathFunctions } = require('../utils/config-secrets-path');
46
49
  Object.assign(exportsObj, createSecretsPathFunctions(getConfig, saveConfig));
47
50
  }
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview AZURE `--stage infra`: validates the workspace, resolves ACR and secrets, deploys
5
+ * the workspace Marketplace template snapshot, verifies image pull (managed identity or registry
6
+ * credentials) and that both applications started, and records outputs. Mori registration and
7
+ * tenant activation stay with Miso. Registry credentials are never written to the workspace.
8
+ */
9
+
10
+ // Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
11
+ const fs = require('../../internal/fs-real-sync');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const marketplace = require('../marketplace-environment');
15
+ const { normalizePreset } = require('../environment-config');
16
+ const { normalizeLicenseEdition, normalizeAzureLocation, ONBOARDING_MODES } = require('./infra-catalog');
17
+ const { materializeWorkspace, saveInfra } = require('./workspace');
18
+ const { missingRequiredFields } = require('./infra-manifest');
19
+ const { buildArmParameters, applyAllowMyIp } = require('./infra-projection');
20
+ const { materializeWorkspaceSecrets } = require('./workspace-secrets');
21
+ const { mergeWorkspaceEnv } = require('./workspace-env');
22
+ const { resolvePublicIpv4 } = require('./public-ip');
23
+ const preflight = require('./azure-preflight');
24
+ const readiness = require('./azure-readiness');
25
+ const registryAuth = require('./registry-auth-mode');
26
+
27
+ const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY', 'MORI_CALLBACK_API_KEY']);
28
+
29
+ function missingInputError(file, fields) {
30
+ const error = new Error(
31
+ `Installation workspace needs input before infra can run. Set these fields in ${file}, then rerun:\n - ${fields.join('\n - ')}`
32
+ );
33
+ error.code = 'WORKSPACE_INPUT_REQUIRED';
34
+ return error;
35
+ }
36
+
37
+ function applyLicenseOptions(doc, input) {
38
+ const license = doc.parameters.licenseEdition;
39
+ if (input.licenseEdition !== undefined) {
40
+ doc.parameters.licenseEdition = { ...license, value: normalizeLicenseEdition(input.licenseEdition), source: 'cli' };
41
+ } else {
42
+ doc.parameters.licenseEdition = { ...license, value: normalizeLicenseEdition(license.value) };
43
+ }
44
+ if (!ONBOARDING_MODES.includes(doc.parameters.onboardingMode.value)) {
45
+ throw new Error(`parameters.onboardingMode.value must be one of ${ONBOARDING_MODES.join(', ')}`);
46
+ }
47
+ }
48
+
49
+ async function applyAccessOptions(doc, input, deps) {
50
+ if (input.preset !== undefined) {
51
+ doc.preset = { ...doc.preset, value: normalizePreset(input.preset), source: 'cli' };
52
+ }
53
+ applyLicenseOptions(doc, input);
54
+ const enabled = input.allowMyIp || doc.allowMyIp.value === true;
55
+ if (!enabled) {
56
+ applyAllowMyIp(doc, { enabled: false });
57
+ return;
58
+ }
59
+ const access = await (deps.resolvePublicIpv4 || resolvePublicIpv4)();
60
+ applyAllowMyIp(doc, { enabled: true, ...access });
61
+ }
62
+
63
+ function withTemporaryParameters(document, action) {
64
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aifabrix-arm-'));
65
+ const file = path.join(dir, 'parameters.json');
66
+ try {
67
+ fs.chmodSync(dir, 0o700);
68
+ fs.writeFileSync(file, JSON.stringify(document), { mode: 0o600 });
69
+ return action(file);
70
+ } finally {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ function recordDeployment(doc, record, now) {
76
+ doc.deployment = {
77
+ ...doc.deployment,
78
+ status: { ...doc.deployment.status, value: 'succeeded' },
79
+ name: record.deploymentName,
80
+ id: record.deploymentId,
81
+ correlationId: record.correlationId || null,
82
+ inputHash: record.inputHash,
83
+ completedAt: record.recordedAt || now().toISOString()
84
+ };
85
+ for (const name of Object.keys(doc.outputs)) {
86
+ doc.outputs[name].value = record.outputs[name] ?? null;
87
+ }
88
+ }
89
+
90
+ function deployTemplate(workspace, doc, context) {
91
+ const { document, inputHash } = buildArmParameters(doc, context);
92
+ return withTemporaryParameters(document, parametersFile => marketplace.deployInfra({
93
+ installId: doc.installId.value,
94
+ envKey: null,
95
+ subscription: doc.subscriptionId.value,
96
+ resourceGroup: doc.resourceGroup.value,
97
+ location: doc.parameters.location.value,
98
+ template: workspace.paths.template,
99
+ parameters: parametersFile,
100
+ templateHash: context.templateHash,
101
+ inputHash
102
+ }, context.az));
103
+ }
104
+
105
+ async function verifyAndRecordReadiness(workspace, doc, deps) {
106
+ readiness.verifyRuntimeImagePull(doc, deps.az);
107
+ await readiness.waitForApplications(doc, deps.probe || marketplace.probeHttps);
108
+ const controllerUrl = `${String(doc.outputs.webAppUrl.value).replace(/\/+$/, '')}/miso`;
109
+ doc.controllerUrl = { ...doc.controllerUrl, value: controllerUrl };
110
+ doc.tenantActivation.url = { ...doc.tenantActivation.url, value: controllerUrl };
111
+ saveInfra(workspace, doc);
112
+ mergeWorkspaceEnv(workspace.paths.env, { CONTROLLER_URL: controllerUrl }, { root: workspace.root, replaceable: ['CONTROLLER_URL'] });
113
+ return controllerUrl;
114
+ }
115
+
116
+ function applyLocation(doc, input) {
117
+ if (input.location === undefined) return;
118
+ doc.parameters.location = { ...doc.parameters.location, value: normalizeAzureLocation(input.location), source: 'cli' };
119
+ }
120
+
121
+ async function resolvePullAuth(doc, acr, deps, az) {
122
+ const loadRegistryCredentials = deps.loadRegistryCredentials ||
123
+ require('../../utils/registry-credentials').loadRegistryCredentials;
124
+ const resolved = await registryAuth.resolveRegistryAuth(doc, acr, az, {
125
+ loadRegistryCredentials,
126
+ resolveAcrAdminCredentials: preflight.resolveAcrAdminCredentials
127
+ });
128
+ registryAuth.applyEffectiveAcrAuth(doc, resolved.mode);
129
+ return resolved.credentials;
130
+ }
131
+
132
+ async function prepareInfra(workspace, input, deps, az) {
133
+ const { doc, templateHash } = materializeWorkspace(workspace, input, deps);
134
+ applyLocation(doc, input);
135
+ const missing = missingRequiredFields(doc);
136
+ if (missing.length > 0) {
137
+ if (input.location !== undefined) saveInfra(workspace, doc);
138
+ throw missingInputError(workspace.paths.infra, missing);
139
+ }
140
+ registryAuth.applyRequestedAcrAuth(doc, input);
141
+ preflight.assertActiveAccount(doc, az);
142
+ const acr = preflight.resolveAcr(doc, az);
143
+ doc.parameters.acrLoginServer.value = acr.loginServer;
144
+ doc.parameters.acrResourceId.value = acr.id;
145
+ // Local pair first (a partial pair fails closed); Azure admin lookup only for explicit credentials.
146
+ const registryCredentials = await resolvePullAuth(doc, acr, deps, az);
147
+ preflight.assertDeploymentAuthority(doc, acr, az);
148
+ await applyAccessOptions(doc, input, deps);
149
+ saveInfra(workspace, doc);
150
+ return { doc, templateHash, acr, registryCredentials };
151
+ }
152
+
153
+ function deployWithStatus(workspace, doc, context) {
154
+ doc.deployment.status = { ...doc.deployment.status, value: 'running' };
155
+ saveInfra(workspace, doc);
156
+ try {
157
+ return deployTemplate(workspace, doc, context);
158
+ } catch (error) {
159
+ doc.deployment.status = { ...doc.deployment.status, value: 'failed' };
160
+ saveInfra(workspace, doc);
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Runs the AZURE infra stage.
167
+ * @param {Object} workspace - Opened workspace
168
+ * @param {Object} input - Validated stage input
169
+ * @param {Object} [deps] - Injected dependencies (az, probe, resolvePublicIpv4, secretsApi, now, templatePath,
170
+ * loadRegistryCredentials)
171
+ * @returns {Promise<{ doc: Object, controllerUrl: string, activationUrl: string }>}
172
+ */
173
+ async function runAzureInfraStage(workspace, input, deps = {}) {
174
+ const context = { az: deps.az || marketplace.runAz, now: deps.now || (() => new Date()), probe: deps.probe };
175
+ const { doc, templateHash, acr, registryCredentials } = await prepareInfra(workspace, input, deps, context.az);
176
+ const env = await materializeWorkspaceSecrets({
177
+ vendor: 'AZURE', envPath: workspace.paths.env, root: workspace.root, names: INFRA_SECRETS, secretsApi: deps.secretsApi
178
+ });
179
+ const record = deployWithStatus(workspace, doc, {
180
+ env, acr, templateHash, az: context.az, registryCredentials
181
+ });
182
+ recordDeployment(doc, record, context.now);
183
+ // Never persist registry credentials in the workspace manifest.
184
+ doc.parameters.acrAdminUsername = { ...doc.parameters.acrAdminUsername, value: '', source: 'managedIdentityMode' };
185
+ doc.parameters.acrAdminPassword = { ...doc.parameters.acrAdminPassword, value: '', source: 'managedIdentityMode' };
186
+ saveInfra(workspace, doc);
187
+ const controllerUrl = await verifyAndRecordReadiness(workspace, doc, context);
188
+ return { doc, controllerUrl, activationUrl: doc.tenantActivation.url.value };
189
+ }
190
+
191
+ module.exports = { runAzureInfraStage, INFRA_SECRETS };