@aifabrix/builder 2.59.0 → 2.61.2

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 (145) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/agent-kit/git-identity.js +180 -0
  14. package/lib/agent-kit/setup.js +3 -0
  15. package/lib/agent-kit/start.js +45 -1
  16. package/lib/api/configuration.api.js +131 -0
  17. package/lib/api/role-assistant-test-job.api.js +60 -0
  18. package/lib/api/system-secrets.api.js +72 -0
  19. package/lib/api/work-search.api.js +13 -6
  20. package/lib/app/deploy.js +8 -1
  21. package/lib/app/run-docker-fallback.js +6 -1
  22. package/lib/app/run-helpers.js +2 -1
  23. package/lib/app/run-parameter-sync.js +142 -0
  24. package/lib/app/show-display.js +1 -0
  25. package/lib/app/show-online.js +15 -0
  26. package/lib/build/docker-build-args.js +5 -2
  27. package/lib/build/index.js +3 -2
  28. package/lib/build/standard-docker-build.js +6 -3
  29. package/lib/cli/setup-app.js +19 -1
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-utility.js +24 -1
  32. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  33. package/lib/commands/datasource-capability.js +4 -2
  34. package/lib/commands/env-secret-context.js +113 -0
  35. package/lib/commands/env-secret-list.js +200 -0
  36. package/lib/commands/env-secret-push-confirm.js +54 -0
  37. package/lib/commands/env-secret-push-run.js +227 -0
  38. package/lib/commands/env-secret-push.js +168 -0
  39. package/lib/commands/repair-datasource-apply.js +2 -0
  40. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  41. package/lib/commands/repair-datasource-run.js +1 -0
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/setup-modes.js +1 -8
  44. package/lib/commands/setup-prompts.js +2 -180
  45. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  46. package/lib/commands/verify-operations-steps.js +13 -1
  47. package/lib/commands/wizard-config-normalizer.js +7 -4
  48. package/lib/commands/wizard-core.js +5 -157
  49. package/lib/commands/wizard-file-saving.js +163 -0
  50. package/lib/core/env-platform-expand.js +97 -0
  51. package/lib/core/secrets-env-content.js +42 -4
  52. package/lib/core/secrets-env-write.js +10 -3
  53. package/lib/core/secrets-load.js +4 -2
  54. package/lib/datasource/binary-documents-validator.js +190 -0
  55. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  56. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  57. package/lib/datasource/capability/upsert-operations.js +138 -0
  58. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  59. package/lib/datasource/validate.js +12 -5
  60. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  61. package/lib/deployment/installation/infra-catalog.js +2 -5
  62. package/lib/generator/builders.js +17 -0
  63. package/lib/generator/helpers.js +23 -2
  64. package/lib/generator/index.js +13 -6
  65. package/lib/lifecycle/product-model.js +4 -3
  66. package/lib/lifecycle/report-display.js +3 -2
  67. package/lib/parameters/infra-parameter-catalog.js +1 -1
  68. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  69. package/lib/programmatic/builder-help-governance.js +1 -1
  70. package/lib/programmatic/builder-help.js +1 -1
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  75. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  76. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  77. package/lib/role-assistant/test-runner-workhub.js +28 -19
  78. package/lib/schema/application-schema.json +205 -2
  79. package/lib/schema/external-datasource.schema.json +23 -3
  80. package/lib/schema/infra-parameter.schema.json +139 -33
  81. package/lib/schema/infra.parameter.yaml +416 -66
  82. package/lib/schema/infrastructure-schema.json +10 -35
  83. package/lib/utils/compose-generate-docker-compose.js +16 -9
  84. package/lib/utils/datasource-binary-evidence.js +92 -0
  85. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  86. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  87. package/lib/utils/datasource-test-run-display.js +8 -2
  88. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  89. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  90. package/lib/utils/docker-build.js +29 -8
  91. package/lib/utils/docker-manifest-public-port.js +36 -0
  92. package/lib/utils/env-copy.js +11 -10
  93. package/lib/utils/external-system-system-test-tty.js +3 -2
  94. package/lib/utils/image-tags.js +2 -2
  95. package/lib/utils/platform-kv-ref.js +1 -1
  96. package/lib/utils/platform-resolution.js +226 -0
  97. package/lib/utils/prepare-local-data-mount.js +58 -0
  98. package/lib/utils/resolve-docker-image-ref.js +8 -3
  99. package/lib/utils/secrets-helpers.js +0 -1
  100. package/lib/utils/system-secret-mapping.js +125 -0
  101. package/lib/utils/test-log-writer.js +2 -1
  102. package/lib/validation/external-manifest-validator.js +5 -0
  103. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  104. package/lib/validation/validate-external-file.js +5 -1
  105. package/package.json +5 -4
  106. package/templates/README.md +2 -1
  107. package/templates/agent-kit/agent-kit.yaml +4 -1
  108. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  109. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  110. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
  111. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  112. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  113. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
  114. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
  115. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  116. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
  117. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  118. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  119. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  120. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  121. package/templates/agent-kit/skills/shared/status.md +76 -0
  122. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  123. package/templates/applications/builder-api/application.yaml +1 -1
  124. package/templates/applications/builder-api/env.template +5 -1
  125. package/templates/applications/dataplane/application.yaml +30 -2
  126. package/templates/applications/dataplane/env.template +36 -5
  127. package/templates/applications/keycloak/application.yaml +6 -1
  128. package/templates/applications/miso-controller/application.yaml +96 -1
  129. package/templates/applications/miso-controller/env.template +63 -37
  130. package/templates/applications/miso-controller/rbac.yaml +17 -0
  131. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  132. package/templates/marketplace/main.json +73 -225
  133. package/templates/python/Dockerfile.hbs +2 -0
  134. package/templates/python/docker-compose.hbs +1 -1
  135. package/templates/typescript/Dockerfile.hbs +2 -0
  136. package/templates/typescript/docker-compose.hbs +12 -0
  137. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
  138. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  139. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  140. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  141. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  142. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  143. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  144. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  145. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Parameter-backed installation configuration API.
3
+ *
4
+ * @fileoverview Read safe configuration metadata from Miso Controller
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { ApiClient } = require('./index');
12
+
13
+ const PAGE_SIZE = 100;
14
+
15
+ /**
16
+ * @param {string} envKey - Environment key
17
+ * @returns {string} Configuration collection endpoint
18
+ */
19
+ function configurationEndpoint(envKey) {
20
+ return `/api/v1/environments/${encodeURIComponent(envKey)}/configuration`;
21
+ }
22
+
23
+ /**
24
+ * @param {Object} meta - Pagination metadata
25
+ * @param {number} requestedPage - Requested one-based page
26
+ * @param {boolean} hasRows - Whether the page contains rows
27
+ * @returns {number} Validated total page count
28
+ */
29
+ function validatePageMeta(meta, requestedPage, hasRows) {
30
+ const currentPage = Number(meta && meta.currentPage);
31
+ const totalPages = Number(meta && meta.totalPages);
32
+ if (!Number.isInteger(currentPage) || currentPage !== requestedPage) {
33
+ throw new Error('Configuration list response has invalid pagination metadata');
34
+ }
35
+ if (!Number.isInteger(totalPages) || totalPages < 0 || (hasRows && totalPages === 0)) {
36
+ throw new Error('Configuration list response has invalid pagination metadata');
37
+ }
38
+ return totalPages;
39
+ }
40
+
41
+ /**
42
+ * @param {Object} response - API response
43
+ * @param {number} requestedPage - Requested one-based page
44
+ * @returns {{ rows: Object[], totalPages: number }} Validated page
45
+ */
46
+ function readConfigurationPage(response, requestedPage) {
47
+ if (response && response.success === false) {
48
+ throw new Error(response.formattedError || response.error || 'Configuration list request failed');
49
+ }
50
+ const body = response && response.data !== undefined ? response.data : response;
51
+ if (!body || !Array.isArray(body.data) || !body.meta) {
52
+ throw new Error('Configuration list response is not a paginated result');
53
+ }
54
+ const totalPages = validatePageMeta(body.meta, requestedPage, body.data.length > 0);
55
+ return { rows: body.data, totalPages };
56
+ }
57
+
58
+ /**
59
+ * List configuration declarations, optionally for one exact owner.
60
+ *
61
+ * GET /api/v1/environments/{envKey}/configuration
62
+ * @requiresPermission {Controller} controller:monitor or admin:read
63
+ * @async
64
+ * @function listEnvironmentConfiguration
65
+ * @param {string} controllerUrl - Controller base URL
66
+ * @param {Object} authConfig - Authentication configuration
67
+ * @param {string} envKey - Environment key
68
+ * @param {string} [application] - Exact owning application key
69
+ * @param {Object} [options] - Optional safe search
70
+ * @param {string} [options.search] - Partial key/name search
71
+ * @returns {Promise<Object[]>} Safe configuration metadata rows
72
+ */
73
+ async function listEnvironmentConfiguration(controllerUrl, authConfig, envKey, application, options = {}) {
74
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
75
+ throw new Error('controllerUrl is required and must be a string');
76
+ }
77
+ const environment = typeof envKey === 'string' ? envKey.trim() : '';
78
+ const owner = typeof application === 'string' ? application.trim() : '';
79
+ if (!environment) {
80
+ throw new Error('envKey is required and must be a string');
81
+ }
82
+ const client = new ApiClient(controllerUrl, authConfig);
83
+ const common = {
84
+ pageSize: PAGE_SIZE,
85
+ sort: owner ? 'key' : 'application'
86
+ };
87
+ if (owner) common.application = owner;
88
+ if (options.search) common.search = String(options.search).trim();
89
+ const rows = [];
90
+ for (let page = 1;; page += 1) {
91
+ const response = await client.get(configurationEndpoint(environment), {
92
+ params: { ...common, page }
93
+ });
94
+ const result = readConfigurationPage(response, page);
95
+ rows.push(...result.rows);
96
+ if (page >= result.totalPages) return rows;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Update one authoritatively non-sensitive configuration declaration.
102
+ * @param {string} controllerUrl - Controller base URL
103
+ * @param {Object} authConfig - Authentication configuration
104
+ * @param {string} envKey - Environment key
105
+ * @param {string} application - Exact application owner
106
+ * @param {string} key - Configuration declaration key
107
+ * @param {Object} update - Value and concurrency revision
108
+ * @param {string} update.value - Non-sensitive value
109
+ * @param {string} update.revision - Opaque revision from the configuration list
110
+ * @returns {Promise<Object>} Safe updated metadata
111
+ */
112
+ async function putEnvironmentConfiguration(
113
+ controllerUrl, authConfig, envKey, application, key, update
114
+ ) {
115
+ const value = update && update.value;
116
+ const revision = update && update.revision;
117
+ const required = { controllerUrl, envKey, application, key, revision };
118
+ for (const [name, input] of Object.entries(required)) {
119
+ if (!input || typeof input !== 'string') throw new Error(`${name} is required`);
120
+ }
121
+ if (typeof value !== 'string') throw new Error('value must be a string');
122
+ const client = new ApiClient(controllerUrl, authConfig);
123
+ const endpoint = `${configurationEndpoint(envKey)}/${encodeURIComponent(application)}/` +
124
+ encodeURIComponent(key);
125
+ return await client.put(endpoint, { body: { value }, headers: { 'If-Match': revision } });
126
+ }
127
+
128
+ module.exports = {
129
+ listEnvironmentConfiguration,
130
+ putEnvironmentConfiguration
131
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Dataplane asynchronous Role Assistant test job API.
3
+ *
4
+ * @fileoverview Enqueue and poll published Role Assistant cases
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { createDataplaneApiClient } = require('./index');
10
+ const { throwIfApiFailed } = require('./api-response-errors');
11
+ const { unwrapApiData } = require('../utils/external-system-readiness-core');
12
+
13
+ function jobPath(systemIdOrKey, suffix = '') {
14
+ const key = encodeURIComponent(String(systemIdOrKey || '').trim());
15
+ return `/api/v1/external/systems/${key}/role-assistant-tests/jobs${suffix}`;
16
+ }
17
+
18
+ function unwrapJob(res) {
19
+ const value = unwrapApiData(res);
20
+ if (value && typeof value === 'object' && !Array.isArray(value) && value.data) {
21
+ return value.data;
22
+ }
23
+ return value;
24
+ }
25
+
26
+ async function enqueueRoleAssistantTestJob(
27
+ dataplaneUrl,
28
+ authConfig,
29
+ systemIdOrKey,
30
+ body
31
+ ) {
32
+ const client = createDataplaneApiClient(dataplaneUrl, authConfig);
33
+ const path = jobPath(systemIdOrKey);
34
+ const res = await client.post(path, { body, timeoutMs: 60000 });
35
+ throwIfApiFailed(res, {
36
+ endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
37
+ });
38
+ return unwrapJob(res);
39
+ }
40
+
41
+ async function getRoleAssistantTestJob(
42
+ dataplaneUrl,
43
+ authConfig,
44
+ systemIdOrKey,
45
+ jobId
46
+ ) {
47
+ const suffix = `/${encodeURIComponent(String(jobId || '').trim())}`;
48
+ const client = createDataplaneApiClient(dataplaneUrl, authConfig);
49
+ const path = jobPath(systemIdOrKey, suffix);
50
+ const res = await client.get(path, { timeoutMs: 60000 });
51
+ throwIfApiFailed(res, {
52
+ endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
53
+ });
54
+ return unwrapJob(res);
55
+ }
56
+
57
+ module.exports = {
58
+ enqueueRoleAssistantTestJob,
59
+ getRoleAssistantTestJob
60
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * System secrets API (Miso Controller).
3
+ *
4
+ * Deliberately narrow: a value can be written and never read back, and only against a
5
+ * catalog key the installation already defines. There is no create endpoint, so this module
6
+ * must not grow one — a key that does not exist is a catalog change in aifabrix-miso, not
7
+ * something the CLI can fix.
8
+ *
9
+ * The PUT request body marks `value` as writeOnly. Nothing here returns a value, and no
10
+ * caller should construct a response shape that could carry one.
11
+ *
12
+ * @fileoverview System secrets API functions (put-only over catalog keys)
13
+ * @author AI Fabrix Team
14
+ * @version 1.0.0
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { ApiClient } = require('./index');
20
+
21
+ /**
22
+ * @param {string} envKey - Environment key
23
+ * @returns {string} Collection endpoint for that environment
24
+ */
25
+ function systemSecretsEndpoint(envKey) {
26
+ return `/api/v1/environments/${encodeURIComponent(envKey)}/system-secrets`;
27
+ }
28
+
29
+ /**
30
+ * Set one system secret value. The value is write-only: it cannot be read back, and this
31
+ * function returns the server response unchanged without echoing what was sent.
32
+ *
33
+ * PUT /api/v1/environments/{envKey}/system-secrets/{catalogKey}
34
+ * @requiresPermission {Miso} controller:admin
35
+ * @async
36
+ * @function putSystemSecret
37
+ * @param {string} controllerUrl - Controller base URL
38
+ * @param {Object} authConfig - Authentication configuration
39
+ * @param {string} envKey - Environment key
40
+ * @param {string} catalogKey - Catalog key the installation already defines
41
+ * @param {string} value - Secret value, read from a file by the caller
42
+ * @param {string} revision - Opaque revision returned by the configuration list
43
+ * @returns {Promise<Object>} Server response; carries no value
44
+ * @throws {Error} If the request fails. The message must never include the value
45
+ */
46
+ async function putSystemSecret(controllerUrl, authConfig, envKey, catalogKey, value, revision) {
47
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
48
+ throw new Error('controllerUrl is required and must be a string');
49
+ }
50
+ if (!envKey || typeof envKey !== 'string') {
51
+ throw new Error('envKey is required and must be a string');
52
+ }
53
+ if (!catalogKey || typeof catalogKey !== 'string') {
54
+ throw new Error('catalogKey is required and must be a string');
55
+ }
56
+ if (typeof value !== 'string' || value.length === 0) {
57
+ // Names the key, never the value, because this message reaches the terminal.
58
+ throw new Error(`No value supplied for ${catalogKey}`);
59
+ }
60
+ if (!revision || typeof revision !== 'string') {
61
+ throw new Error(`No configuration revision supplied for ${catalogKey}`);
62
+ }
63
+ const client = new ApiClient(controllerUrl, authConfig);
64
+ return await client.put(`${systemSecretsEndpoint(envKey)}/${encodeURIComponent(catalogKey)}`, {
65
+ body: { value },
66
+ headers: { 'If-Match': revision }
67
+ });
68
+ }
69
+
70
+ module.exports = {
71
+ putSystemSecret
72
+ };
@@ -121,16 +121,23 @@ async function getWorkRoles(dataplaneUrl, authConfig) {
121
121
 
122
122
  /**
123
123
  * GET-equivalent work.skills for one roleRef.
124
- * Requests detail=full so search.resourceTypes (executable grammar) is present.
125
- * Compact summary omits that slice and cannot evaluate named viewpoint ops.
124
+ * Defaults to detail=full so search.resourceTypes (executable grammar) is present.
125
+ * A caller may request progressive resource/operation detail for skills-only checks.
126
126
  * @requiresPermission {Dataplane} Authenticated (oauth2: [])
127
127
  * @async
128
128
  */
129
- async function getWorkSkills(dataplaneUrl, authConfig, roleRef) {
130
- return executeLayerBTool(dataplaneUrl, authConfig, LAYER_B_TOOLS.skills, {
129
+ async function getWorkSkills(dataplaneUrl, authConfig, roleRef, options = {}) {
130
+ const args = {
131
131
  roleRef: String(roleRef || '').trim(),
132
- detail: 'full'
133
- });
132
+ detail: String(options.detail || 'full').trim()
133
+ };
134
+ if (typeof options.resourceType === 'string' && options.resourceType.trim()) {
135
+ args.resourceType = options.resourceType.trim();
136
+ }
137
+ if (typeof options.operation === 'string' && options.operation.trim()) {
138
+ args.operation = options.operation.trim();
139
+ }
140
+ return executeLayerBTool(dataplaneUrl, authConfig, LAYER_B_TOOLS.skills, args);
134
141
  }
135
142
 
136
143
  /**
package/lib/app/deploy.js CHANGED
@@ -268,7 +268,14 @@ async function executeStandardDeployment(appName, options) {
268
268
  const controllerUrl = config.controllerUrl || 'unknown';
269
269
  const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
270
270
 
271
- const { manifest, manifestPath } = await generateAndValidateManifest(appName, options);
271
+ // A controller deployment ships tokens plus the platforms/environments sections, so the
272
+ // Controller resolves them per installation and can promote dev -> tst without regenerating.
273
+ // Local runs and an explicit --resource-group resolve here instead.
274
+ const resolveHere = Boolean(options.local || options.resourceGroup);
275
+ const { manifest, manifestPath } = await generateAndValidateManifest(appName, {
276
+ ...options,
277
+ ...(resolveHere ? { target: { environment: config.envKey, resourceGroup: options.resourceGroup } } : {})
278
+ });
272
279
  applyManifestOverrides(manifest, options);
273
280
  validateImageIsPullable(manifest.image, appName);
274
281
  displayDeploymentInfo(manifest, manifestPath);
@@ -8,6 +8,8 @@
8
8
  'use strict';
9
9
 
10
10
  const chalk = require('chalk');
11
+ const { prepareLocalDataMount } = require('../utils/prepare-local-data-mount');
12
+ const { resolveLocalDataMountPathForCompose } = require('../utils/compose-generate-docker-compose');
11
13
  const logger = require('../utils/logger');
12
14
  const { execWithDockerEnv } = require('../utils/docker-exec');
13
15
  const { getContainerPort } = require('../utils/port-resolver');
@@ -87,7 +89,10 @@ async function executeDockerRunUp(o) {
87
89
  ];
88
90
 
89
91
  if (needsStorage) {
90
- parts.push(`-v ${shellSingleQuote(storageVolumeName(appName, appConfig.developerId))}:/mnt/data`);
92
+ const localPath = resolveLocalDataMountPathForCompose({ appName, appConfig });
93
+ if (localPath) prepareLocalDataMount(localPath, runEnvPath);
94
+ const source = localPath || storageVolumeName(appName, appConfig.developerId);
95
+ parts.push(`-v ${shellSingleQuote(source)}:/mnt/data`);
91
96
  }
92
97
  if (runEnvPath && typeof runEnvPath === 'string') {
93
98
  parts.push(`--env-file ${shellSingleQuote(runEnvPath)}`);
@@ -368,6 +368,8 @@ async function prepareEnvironment(appName, appConfig, options) {
368
368
  developerId,
369
369
  { runEnvKey, effectiveEnvironmentScopedResources }
370
370
  );
371
+ const { hydrateMissingLocalParameters } = require('./run-parameter-sync');
372
+ await hydrateMissingLocalParameters(appName, runEnvPath);
371
373
 
372
374
  const composeOptions = {
373
375
  ...options,
@@ -438,4 +440,3 @@ module.exports = {
438
440
  displayRunStatus,
439
441
  cleanApplicationsDir: runEnvCompose.cleanApplicationsDir
440
442
  };
441
-
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Hydrate missing application-owned parameters before a local governed run.
3
+ *
4
+ * Local/database controllers deliberately leave database generators to Builder. In
5
+ * client-credentials mode the application cannot start until those values are in its
6
+ * bootstrap snapshot, so Builder fills only unresolved declared rows through the
7
+ * application's own client token. Existing Controller values are never overwritten.
8
+ *
9
+ * @fileoverview Local run parameter hydration for governed bootstrap
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs').promises;
15
+ const path = require('path');
16
+ const { ApiClient } = require('../api');
17
+ const { getToken } = require('../api/auth.api');
18
+ const { getControllerDeploymentType } = require('../api/controller-health.api');
19
+ const { resolveControllerUrl } = require('../utils/controller-url');
20
+ const { getBuilderPath } = require('../utils/paths');
21
+ const { mapEnvToCatalogKeys, parseEnvAssignments } = require('../utils/system-secret-mapping');
22
+ const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
23
+
24
+ const LOCAL_DEPLOYMENTS = new Set(['local', 'database']);
25
+ const IDENTITY_NAMES = new Set([
26
+ 'MISO_CLIENTID',
27
+ 'MISO_CLIENT_ID',
28
+ 'MISO_CLIENTSECRET',
29
+ 'MISO_CLIENT_SECRET'
30
+ ]);
31
+
32
+ function statusOf(response) {
33
+ const value = response && (response.status || response.statusCode || response.response?.status);
34
+ return Number.isInteger(value) ? value : 0;
35
+ }
36
+
37
+ function failed(response) {
38
+ return Boolean(response && (response.success === false || statusOf(response) >= 400));
39
+ }
40
+
41
+ function tokenFrom(response) {
42
+ const body = response && response.data !== undefined ? response.data : response;
43
+ const payload = body && body.data !== undefined ? body.data : body;
44
+ const token = payload && payload.token;
45
+ if (!token || typeof token !== 'string') {
46
+ throw new Error('Controller client-token response is invalid');
47
+ }
48
+ return token;
49
+ }
50
+
51
+ function credentialPair(env) {
52
+ const clientId = env.get('MISO_CLIENTID') || env.get('MISO_CLIENT_ID');
53
+ const clientSecret = env.get('MISO_CLIENTSECRET') || env.get('MISO_CLIENT_SECRET');
54
+ if (!clientId || !clientSecret) {
55
+ throw new Error('MISO client credentials are required for governed local configuration');
56
+ }
57
+ return { clientId, clientSecret };
58
+ }
59
+
60
+ function uniqueRows(mapping, catalog = getInfraParameterCatalog()) {
61
+ const rows = new Map();
62
+ for (const row of mapping.pushable) {
63
+ if (IDENTITY_NAMES.has(row.name)) continue;
64
+ const generatorType = catalog.findEntryForKey(row.catalogKey)?.generator?.type;
65
+ if (generatorType !== 'databaseUrl' && generatorType !== 'databasePassword') continue;
66
+ const prior = rows.get(row.catalogKey);
67
+ if (prior && prior.value !== row.value) {
68
+ throw new Error(`Conflicting local values resolve to parameter '${row.catalogKey}'`);
69
+ }
70
+ rows.set(row.catalogKey, row);
71
+ }
72
+ return [...rows.values()];
73
+ }
74
+
75
+ async function isMissing(client, key) {
76
+ const response = await client.get(`/api/v1/parameters/${encodeURIComponent(key)}`);
77
+ if (!failed(response)) return false;
78
+ if (statusOf(response) === 404) return true;
79
+ throw new Error(`Unable to inspect governed parameter '${key}'`);
80
+ }
81
+
82
+ async function writeMissing(client, row) {
83
+ const response = await client.put(`/api/v1/parameters/${encodeURIComponent(row.catalogKey)}`, {
84
+ body: { value: row.value }
85
+ });
86
+ if (failed(response)) {
87
+ throw new Error(`Unable to initialize governed parameter '${row.catalogKey}'`);
88
+ }
89
+ }
90
+
91
+ async function resolveSyncContext(appName, runEnvPath, deps) {
92
+ const readFile = deps.readFile || fs.readFile;
93
+ const envContent = await readFile(runEnvPath, 'utf8');
94
+ const env = parseEnvAssignments(envContent);
95
+ if (env.get('MISO_AUTH_MODE') !== 'client-credentials') return null;
96
+ const controllerUrl = await (deps.resolveControllerUrl || resolveControllerUrl)();
97
+ if (!controllerUrl) throw new Error('Controller URL is required for governed local configuration');
98
+ const deploymentType = await (deps.getControllerDeploymentType || getControllerDeploymentType)(controllerUrl);
99
+ if (!LOCAL_DEPLOYMENTS.has(String(deploymentType || '').toLowerCase())) return null;
100
+ const templatePath = path.join((deps.getBuilderPath || getBuilderPath)(appName), 'env.template');
101
+ const templateContent = await readFile(templatePath, 'utf8');
102
+ const catalog = (deps.getInfraParameterCatalog || getInfraParameterCatalog)();
103
+ return {
104
+ controllerUrl,
105
+ credentials: credentialPair(env),
106
+ rows: uniqueRows(mapEnvToCatalogKeys(envContent, templateContent), catalog)
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Initialize unresolved local parameter rows needed by a client-credentials snapshot.
112
+ * @param {string} appName Application key
113
+ * @param {string} runEnvPath Resolved app-only run env
114
+ * @param {Object} [deps] Test dependencies
115
+ * @returns {Promise<number>} Number of rows initialized
116
+ */
117
+ async function hydrateMissingLocalParameters(appName, runEnvPath, deps = {}) {
118
+ const context = await resolveSyncContext(appName, runEnvPath, deps);
119
+ if (!context || context.rows.length === 0) return 0;
120
+ const response = await (deps.getToken || getToken)(
121
+ context.credentials.clientId,
122
+ context.credentials.clientSecret,
123
+ context.controllerUrl
124
+ );
125
+ const client = deps.client || new ApiClient(context.controllerUrl, {
126
+ type: 'client-token', token: tokenFrom(response)
127
+ });
128
+ let initialized = 0;
129
+ for (const row of context.rows) {
130
+ if (!(await isMissing(client, row.catalogKey))) continue;
131
+ await writeMissing(client, row);
132
+ initialized += 1;
133
+ }
134
+ return initialized;
135
+ }
136
+
137
+ module.exports = {
138
+ credentialPair,
139
+ hydrateMissingLocalParameters,
140
+ tokenFrom,
141
+ uniqueRows
142
+ };
@@ -234,6 +234,7 @@ function logExternalSystemMain(ext) {
234
234
  logger.log(` Version: ${ext.version ?? '—'}`);
235
235
  logger.log(` Credential: ${formatCredentialShowLine(ext)}`);
236
236
  logger.log(` Status: ${formatDataplaneStatusDisplay(ext.status)}`);
237
+ logger.log(` System link: ${ext.systemUrl ?? '—'}`);
237
238
  logger.log(` API docs: ${ext.openApiDocsPageUrl ?? ext.apiDocumentUrl ?? '—'}`);
238
239
  logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
239
240
  logger.log(` OpenAPI spec: ${ext.apiDocumentUrl ?? '—'}`);
@@ -89,6 +89,7 @@ function buildOnlineSummaryFromDataplane(appKey, controllerUrl, dataplaneUrl, ex
89
89
 
90
90
  async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCert, authBundle) {
91
91
  await enrichExternalShowSummary(summary, appKey, verifyCert, authBundle);
92
+ attachSystemUrl(summary);
92
93
  attachRunDefaultsFromUserConfig(summary, appKey, await getConfig());
93
94
  await attachDeclarativeUrlsToShowApplication(summary, appKey);
94
95
  if (json) {
@@ -100,6 +101,20 @@ async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCe
100
101
  displayShow(summary, { permissionsOnly: !!permissionsOnly });
101
102
  }
102
103
 
104
+ /**
105
+ * Expose the Dataplane system browser route for humans and agents.
106
+ * Preserve the deployment base path (for example /data).
107
+ * @param {Object} summary - Online application summary
108
+ */
109
+ function attachSystemUrl(summary) {
110
+ const ext = summary.externalSystem;
111
+ if (!summary.isExternal || !ext || ext.error || !ext.dataplaneUrl) return;
112
+ const baseUrl = ext.dataplaneUrl.replace(/\/+$/, '');
113
+ const systemKey = ext.systemKey || summary.appKey;
114
+ const section = /^(ra-|ek-)/.test(systemKey) ? 'enterprise-knowledge' : 'connected-systems';
115
+ ext.systemUrl = `${baseUrl}/${section}/${encodeURIComponent(systemKey)}`;
116
+ }
117
+
103
118
  async function runOnlineFromDataplane(appKey, json, permissionsOnly, verifyCert, authConfig, authResult) {
104
119
  const environment = await resolveEnvironment();
105
120
  const dataplaneUrl = await resolveDataplaneUrl(authResult.actualControllerUrl, environment, authConfig);
@@ -52,10 +52,13 @@ function applyMisoControllerUiBuildArgs(buildArgs, envMap) {
52
52
  *
53
53
  * @param {string} appName
54
54
  * @param {string} [contextPath] Docker build context (repo root for miso-controller)
55
+ * @param {Object} [options] - `runEnvKey`: environment key for manifest {NAME} resolution
56
+ * (defaults to the current CLI environment inside generateEnvContent)
55
57
  * @returns {Promise<Object.<string, string>>}
56
58
  */
57
- async function resolveDockerBuildArgsForApp(appName, contextPath) {
58
- const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
59
+ async function resolveDockerBuildArgsForApp(appName, contextPath, options = {}) {
60
+ const runEnvKey = options && options.runEnvKey ? String(options.runEnvKey) : undefined;
61
+ const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker', runEnvKey });
59
62
  const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
60
63
  const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
61
64
  const buildArgs = {};
@@ -191,12 +191,13 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
191
191
  * @param {Object} options - Build options
192
192
  */
193
193
 
194
- async function postBuildTasks(appName, buildConfig) {
194
+ async function postBuildTasks(appName, buildConfig, options = {}) {
195
195
  try {
196
196
  // Validate that env.template + secrets resolve cleanly, but never write <appPath>/.env or
197
197
  // envOutputPath. Build args still flow through resolveAndGetEnvMap (in-memory). Run
198
198
  // `aifabrix resolve <app>` to materialize an on-disk .env.
199
- await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker', false, { noWrite: true });
199
+ const runEnvKey = options && options.env ? String(options.env) : undefined;
200
+ await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker', false, { noWrite: true, runEnvKey });
200
201
  logger.log(formatSuccessLine('Env resolution validated (in-memory only; run "aifabrix resolve ' + appName + '" for on-disk .env)'));
201
202
  } catch (error) {
202
203
  logger.log(formatWarningLine(`Could not resolve env: ${error.message}`));
@@ -79,17 +79,20 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
79
79
  buildHelpers
80
80
  );
81
81
  await ensureRunSecretsForApp(appName);
82
- const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath);
82
+ const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath, { runEnvKey: options.env });
83
83
  const { primary, aliases, tags } = options.tagPlan;
84
+ // `build.target` selects a stage of a multi-stage Dockerfile, so one source tree can
85
+ // produce more than one image. Absent, Docker builds the last stage as before.
86
+ const target = buildConfig && buildConfig.target;
84
87
  await dockerBuild.executeDockerBuildWithTag(
85
88
  effectiveImageName,
86
89
  imageName,
87
90
  dockerfilePath,
88
91
  contextPath,
89
92
  primary,
90
- { ...options, buildArgs, tagAliases: aliases }
93
+ { ...options, buildArgs, tagAliases: aliases, target }
91
94
  );
92
- await postBuildTasks(appName, buildConfig);
95
+ await postBuildTasks(appName, buildConfig, options);
93
96
  logger.log(formatSuccessParagraph('Build completed successfully!'));
94
97
  return tags.map((tag) => `${effectiveImageName}:${tag}`).join(', ');
95
98
  }
@@ -243,6 +243,10 @@ function registerBuildCommand(program) {
243
243
  'Image tag or comma-separated tags (default: latest); {version} expands to app.version, e.g. "latest,{version}"'
244
244
  )
245
245
  .option('-r, --registry <registry>', 'Registry host for the image name (overrides dev set-registry and application.yaml)')
246
+ .option(
247
+ '-e, --env <env>',
248
+ 'Environment key used to resolve manifest {NAME} values (dev|tst|pro); defaults to the current CLI environment'
249
+ )
246
250
  .option(
247
251
  '--base',
248
252
  'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
@@ -255,6 +259,7 @@ function registerBuildCommand(program) {
255
259
  Examples:
256
260
  $ aifabrix build myapp
257
261
  $ aifabrix build myapp --tag v1.0.0 # also tags latest
262
+ $ aifabrix build myapp --env dev # resolve manifest {NAME} values for dev
258
263
  $ aifabrix build myapp --base --from-dev --tag 'latest,{version}'
259
264
  $ aifabrix build myapp --registry myregistry.azurecr.io`)
260
265
  .action(async(appName, options) => {
@@ -362,7 +367,7 @@ function setupDockerfileGenerateCommand(program) {
362
367
  }
363
368
 
364
369
  function setupDeployCommand(program) {
365
- program.command('deploy <appKey|systemKey>')
370
+ addDeployTargetOptions(program.command('deploy <appKey|systemKey>'))
366
371
  .description('Deploy builder app or external system via Miso Controller (Azure or --local)')
367
372
  .addHelpText('after', DEPLOY_HELP_AFTER)
368
373
  .option('--local', 'Send manifest to controller then run app locally (app: same as aifabrix run <app>; external: restart dataplane)')
@@ -410,6 +415,19 @@ function setupDeployCommand(program) {
410
415
  });
411
416
  }
412
417
 
418
+ /**
419
+ * Deployment-target options. A controller deployment normally ships {NAME} tokens plus the
420
+ * platforms/environments sections; these flags force local resolution instead.
421
+ * @param {Command} command - Commander command
422
+ * @returns {Command} The command
423
+ */
424
+ function addDeployTargetOptions(command) {
425
+ return command.option(
426
+ '-g, --resource-group <name>',
427
+ 'Target Azure resource group; resolves platforms values in the generated manifest instead of shipping tokens'
428
+ );
429
+ }
430
+
413
431
  function setupPushDeployDockerfileCommands(program) {
414
432
  program.command('push <appKey>')
415
433
  .description('Push builder appKey image to Azure Container Registry')