@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,54 @@
1
+ /**
2
+ * Confirmation gate for bulk installation-secret writes.
3
+ *
4
+ * @fileoverview Prevent accidental env secret bulk pushes
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const readline = require('readline');
10
+ const logger = require('../utils/logger');
11
+ const { formatWarningLine } = require('../utils/cli-test-layout-chalk');
12
+
13
+ /**
14
+ * @param {string} question - Prompt text
15
+ * @param {Object} [streams] - Injectable input/output streams
16
+ * @returns {Promise<string>} User response
17
+ */
18
+ function promptLine(question, streams = {}) {
19
+ const input = streams.input || process.stdin;
20
+ const output = streams.output || process.stdout;
21
+ const rl = readline.createInterface({ input, output });
22
+ return new Promise(resolve => {
23
+ rl.question(question, answer => {
24
+ rl.close();
25
+ resolve(String(answer || '').trim());
26
+ });
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Require explicit approval only when every mapped secret would be written.
32
+ * @param {Object} ctx - Resolved environment context
33
+ * @param {Object} options - CLI options
34
+ * @param {Object} [deps] - Injectable prompt and TTY state
35
+ * @returns {Promise<boolean>} True when execution may continue
36
+ */
37
+ async function confirmBulkSecretPush(ctx, options = {}, deps = {}) {
38
+ if (options.key || options.dryRun || options.yes) return true;
39
+ const isTTY = deps.isTTY === undefined
40
+ ? Boolean(process.stdin.isTTY && process.stdout.isTTY) : deps.isTTY;
41
+ if (!isTTY) {
42
+ throw new Error('Bulk configuration push requires --yes in non-interactive or AI usage');
43
+ }
44
+ logger.log(formatWarningLine(
45
+ `Push all declared configuration from the .env for ${options.app} to ${ctx.envKey}?`
46
+ ));
47
+ const prompt = deps.prompt || promptLine;
48
+ const answer = await prompt('Type "yes" to confirm (anything else cancels): ');
49
+ if (String(answer).trim().toLowerCase() === 'yes') return true;
50
+ logger.log('Cancelled. No configuration values were pushed.');
51
+ return false;
52
+ }
53
+
54
+ module.exports = { promptLine, confirmBulkSecretPush };
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Orchestration and reporting for `aifabrix env secret push`.
3
+ *
4
+ * Reporting is the security-sensitive half: every line names a variable, a catalog key and
5
+ * an outcome, and there is no branch that can print a value. Kept separate from the row
6
+ * mechanics so the redaction rule is easy to read in one place.
7
+ *
8
+ * @fileoverview env secret push: run, report, exit code
9
+ * @author AI Fabrix Team
10
+ * @version 1.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const chalk = require('chalk');
16
+ const logger = require('../utils/logger');
17
+ const {
18
+ OUTCOME, readEnvFile, pushRow
19
+ } = require('./env-secret-push');
20
+ const { parseEnvAssignments } = require('../utils/system-secret-mapping');
21
+ const {
22
+ listEnvironmentConfiguration,
23
+ putEnvironmentConfiguration
24
+ } = require('../api/configuration.api');
25
+ const { putSystemSecret } = require('../api/system-secrets.api');
26
+
27
+ const GLYPH = Object.freeze({ ok: '✔', fail: '✖', skip: '·' });
28
+
29
+ /**
30
+ * @param {Object} row - Reported row
31
+ * @returns {string} One aligned line naming the variable, key and outcome
32
+ */
33
+ function formatRow(row, widths = { name: 34, key: 44 }) {
34
+ const glyph = row.outcome === OUTCOME.updated ? chalk.green(GLYPH.ok)
35
+ : row.outcome === OUTCOME.skipped ? chalk.gray(GLYPH.skip)
36
+ : chalk.red(GLYPH.fail);
37
+ const name = String(row.name).padEnd(widths.name);
38
+ const key = String(row.catalogKey || '—').padEnd(widths.key);
39
+ const detail = row.reason ? `${row.outcome} (${row.reason})` : row.outcome;
40
+ return ` ${name}-> ${key}${glyph} ${detail}`;
41
+ }
42
+
43
+ /**
44
+ * Keep variable and declaration names complete and aligned.
45
+ * @param {Array<Object>} rows - Report rows
46
+ * @returns {{ name: number, key: number }} Result-specific widths
47
+ */
48
+ function reportWidths(rows) {
49
+ return {
50
+ name: Math.max(34, ...rows.map(row => String(row.name).length)),
51
+ key: Math.max(44, ...rows.map(row => String(row.catalogKey || '—').length))
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Print the installation before the first write. Pushing into the wrong installation is
57
+ * silent and unrecoverable, because no value can be read back to show what was overwritten.
58
+ * @param {Object} ctx - controllerUrl, envKey
59
+ * @returns {void}
60
+ */
61
+ function reportTarget(ctx) {
62
+ logger.log(`Controller: ${ctx.controllerUrl} environment: ${ctx.envKey}`);
63
+ logger.log('');
64
+ }
65
+
66
+ /**
67
+ * @param {Array<Object>} results - Reported rows
68
+ * @param {boolean} dryRun - Whether anything was written
69
+ * @returns {number} Exit code: non-zero when any row failed
70
+ */
71
+ function reportSummary(results, dryRun) {
72
+ const failed = results.filter(row => row.outcome !== OUTCOME.updated && row.outcome !== OUTCOME.skipped);
73
+ const updated = results.filter(row => row.outcome === OUTCOME.updated);
74
+ const skipped = results.filter(row => row.outcome === OUTCOME.skipped);
75
+ // Count only the rows that would actually be written. Counting skipped rows as "would be
76
+ // pushed" overstates what the command does, which is the one thing a dry run must not do.
77
+ const considered = results.length - skipped.length;
78
+ logger.log('');
79
+ if (dryRun) {
80
+ const tail = skipped.length > 0 ? ` ${skipped.length} skipped.` : '';
81
+ logger.log(`Dry run: ${updated.length} of ${considered} configuration value(s) would be pushed.${tail} Nothing was written.`);
82
+ return 0;
83
+ }
84
+ if (failed.length === 0) {
85
+ logger.log(`${updated.length} of ${considered} pushed.`);
86
+ return 0;
87
+ }
88
+ logger.log(`${updated.length} of ${considered} pushed; ${failed.length} failed.`);
89
+ reportRemediation(failed);
90
+ return 1;
91
+ }
92
+
93
+ /**
94
+ * One hint per failure class, because each has a different owner.
95
+ * @param {Array<Object>} failed - Failing rows
96
+ * @returns {void}
97
+ */
98
+ function reportRemediation(failed) {
99
+ if (failed.some(row => row.outcome === OUTCOME.notInCatalog)) {
100
+ logger.log('The controller no longer recognizes those configuration declarations.');
101
+ logger.log('Refresh with aifabrix env secret list, then retry using the current declaration.');
102
+ }
103
+ if (failed.some(row => row.outcome === OUTCOME.forbidden)) {
104
+ logger.log('Setting a system secret requires the controller:admin scope. Run: aifabrix login');
105
+ }
106
+ if (failed.some(row => row.outcome === OUTCOME.unreachable)) {
107
+ logger.log('Nothing was written for those keys. Check the controller URL above and retry.');
108
+ }
109
+ if (failed.some(row => row.outcome === OUTCOME.conflict)) {
110
+ logger.log('The configuration changed before the write. Run the push again to use a fresh revision.');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Map .env variable names to authoritative Controller declarations.
116
+ * @param {string} envContent - Application .env content
117
+ * @param {Array<Object>} declarations - Controller configuration rows
118
+ * @param {string} [only] - Optional exact .env variable name
119
+ * @param {string} [application] - Expected exact application owner
120
+ * @returns {{ pushable: Array<Object>, skipped: Array<Object> }} Revision-bound rows
121
+ */
122
+ function mapEnvConfiguration(envContent, declarations, only, application) {
123
+ const pushable = [];
124
+ const skipped = [];
125
+ const values = parseEnvAssignments(envContent);
126
+ for (const [name, value] of values) {
127
+ if (only && name !== only) continue;
128
+ const matches = declarations.filter(item => item && item.name === name &&
129
+ (!application || item.application === application));
130
+ if (matches.length !== 1) {
131
+ skipped.push({ name, reason: matches.length ? 'ambiguous declaration' : 'not declared' });
132
+ continue;
133
+ }
134
+ const match = matches[0];
135
+ if (typeof match.sensitive !== 'boolean' || !match.key || !match.application || !match.revision) {
136
+ skipped.push({ name, catalogKey: match.key, reason: 'incomplete declaration metadata' });
137
+ continue;
138
+ }
139
+ if (match.sensitive && !value) {
140
+ skipped.push({ name, catalogKey: match.key, reason: 'no secret value in the env file' });
141
+ continue;
142
+ }
143
+ pushable.push({
144
+ name, value, catalogKey: match.key, application: match.application,
145
+ revision: match.revision, sensitive: match.sensitive
146
+ });
147
+ }
148
+ if (only && !values.has(only)) skipped.push({ name: only, reason: 'not present in the env file' });
149
+ return { pushable, skipped };
150
+ }
151
+
152
+ /**
153
+ * @param {Object} deps - Injected api
154
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
155
+ * @param {Array<Object>} ready - Rows to push
156
+ * @returns {Promise<Array<Object>>} Reported rows
157
+ */
158
+ async function pushAll(deps, ctx, ready) {
159
+ const results = [];
160
+ for (const row of ready) {
161
+ const result = await pushRow(deps, ctx, row);
162
+ results.push(result);
163
+ if (result.fatal) {
164
+ // 403 fails identically for every remaining key; report them rather than retry.
165
+ for (const remaining of ready.slice(results.length)) {
166
+ results.push({ name: remaining.name, catalogKey: remaining.catalogKey, outcome: result.outcome });
167
+ }
168
+ break;
169
+ }
170
+ }
171
+ return results;
172
+ }
173
+
174
+ /**
175
+ * Run the push.
176
+ * @param {Object} ctx - controllerUrl, authConfig, envKey, envFilePath
177
+ * @param {Object} [options] - `key`, `dryRun`, `api`
178
+ * @returns {Promise<{ exitCode: number, results: Array<Object> }>} Outcome per row
179
+ */
180
+ async function runEnvSecretPush(ctx, options = {}) {
181
+ const envContent = readEnvFile(ctx.envFilePath, options);
182
+ const api = options.api || {
183
+ listEnvironmentConfiguration, putSystemSecret, putEnvironmentConfiguration
184
+ };
185
+ const declarations = await api.listEnvironmentConfiguration(
186
+ ctx.controllerUrl, ctx.authConfig, ctx.envKey, options.application
187
+ );
188
+ const { pushable, skipped } = mapEnvConfiguration(
189
+ envContent, declarations, options.key, options.application
190
+ );
191
+ reportTarget(ctx);
192
+ // Every mapped row is attempted. Whether an unlisted key can be written differs by
193
+ // installation, so only the server can answer it.
194
+ const results = options.dryRun
195
+ ? dryRunRows(pushable)
196
+ : await pushAll(options, ctx, pushable);
197
+ const reported = results.concat(skipped.map(row => ({ ...row, outcome: OUTCOME.skipped })));
198
+ const widths = reportWidths(reported);
199
+ for (const row of reported) {
200
+ logger.log(formatRow(row, widths));
201
+ }
202
+ return { exitCode: reportSummary(reported, options.dryRun === true), results: reported };
203
+ }
204
+
205
+ /**
206
+ * Dry-run rows: what would be attempted. Catalog validity is decided by PUT on a real run.
207
+ * @param {Array<Object>} pushable - Mapped rows
208
+ * @returns {Array<Object>} Reportable rows that write nothing
209
+ */
210
+ function dryRunRows(pushable) {
211
+ return pushable.map(row => ({
212
+ name: row.name,
213
+ catalogKey: row.catalogKey,
214
+ outcome: OUTCOME.updated
215
+ }));
216
+ }
217
+
218
+ module.exports = {
219
+ formatRow,
220
+ reportWidths,
221
+ reportTarget,
222
+ reportSummary,
223
+ dryRunRows,
224
+ pushAll,
225
+ mapEnvConfiguration,
226
+ runEnvSecretPush
227
+ };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Push declared configuration values from an operator's env file into an installation.
3
+ *
4
+ * Every value in this module comes from a file and goes to one API call. It is never an
5
+ * argument (so it cannot reach shell history or a process list), never logged, and never
6
+ * placed in a message, a progress line or a JSON envelope — on success or on failure.
7
+ * Rows are reported by variable name, declaration key and outcome only.
8
+ *
9
+ * The command updates Controller declarations and cannot create a new declaration.
10
+ *
11
+ * @fileoverview aifabrix env secret push
12
+ * @author AI Fabrix Team
13
+ * @version 1.0.0
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execFileSync } = require('child_process');
21
+ const { putSystemSecret } = require('../api/system-secrets.api');
22
+ const {
23
+ listEnvironmentConfiguration,
24
+ putEnvironmentConfiguration
25
+ } = require('../api/configuration.api');
26
+
27
+ /** Outcome codes reported per row; none of them can carry a value. */
28
+ const OUTCOME = Object.freeze({
29
+ updated: 'updated',
30
+ notInCatalog: 'not declared',
31
+ forbidden: 'not permitted',
32
+ conflict: 'configuration changed; retry',
33
+ unreachable: 'controller unreachable',
34
+ skipped: 'skipped'
35
+ });
36
+
37
+ /**
38
+ * HTTP status from an API client error, when it carries one.
39
+ * @param {Error} error - Thrown error
40
+ * @returns {number} Status code, or 0 when the failure was not an HTTP response
41
+ */
42
+ function statusOf(error) {
43
+ const status = error && (error.status || error.statusCode ||
44
+ (error.response && error.response.status));
45
+ return Number.isInteger(status) ? status : 0;
46
+ }
47
+
48
+ /**
49
+ * The API client resolves non-2xx responses as `{ success: false, status }` rather than
50
+ * throwing, so a failure that is only caught in a try/catch is reported as success. Verified
51
+ * against the live controller: a PUT to an undefined catalog key resolves with status 404.
52
+ * @param {Object} response - Resolved API response
53
+ * @returns {boolean} True when the call did not succeed
54
+ */
55
+ function isFailedResponse(response) {
56
+ if (!response || typeof response !== 'object') {
57
+ return false;
58
+ }
59
+ if (response.success === false) {
60
+ return true;
61
+ }
62
+ const status = statusOf(response);
63
+ return status >= 400;
64
+ }
65
+
66
+ /**
67
+ * Classify a failure by owner. 403, 404 and transport failures need different messages
68
+ * because different people fix them; collapsing them to "failed to push" helps nobody.
69
+ * @param {Error} error - Thrown error
70
+ * @returns {{ outcome: string, fatal: boolean }} Outcome and whether remaining keys are pointless
71
+ */
72
+ function classifyFailure(error) {
73
+ const status = statusOf(error);
74
+ if (status === 403 || status === 401) {
75
+ // Every remaining key fails the same way, so stop rather than repeat it.
76
+ return { outcome: OUTCOME.forbidden, fatal: true };
77
+ }
78
+ if (status === 404) {
79
+ return { outcome: OUTCOME.notInCatalog, fatal: false };
80
+ }
81
+ if (status === 409) {
82
+ return { outcome: OUTCOME.conflict, fatal: true };
83
+ }
84
+ return { outcome: OUTCOME.unreachable, fatal: false };
85
+ }
86
+
87
+ /**
88
+ * A secrets file that git does not ignore is one `git add .` away from being committed, and
89
+ * a committed secret is disclosed no matter what happens afterwards. Refuse rather than
90
+ * read it. An untracked path outside any repository is fine — there is nothing to commit to.
91
+ *
92
+ * @param {string} filePath - Env file path
93
+ * @param {Object} [deps] - `execFileSync` for tests
94
+ * @returns {boolean} True when git ignores the file, or the path is not in a repository
95
+ */
96
+ function isIgnoredByGit(filePath, deps = {}) {
97
+ const exec = deps.execFileSync || execFileSync;
98
+ const cwd = path.dirname(filePath);
99
+ try {
100
+ exec('git', ['check-ignore', '-q', '--', filePath], { cwd, stdio: 'ignore' });
101
+ return true;
102
+ } catch (error) {
103
+ // check-ignore exits 1 when the path is NOT ignored, and 128 outside a repository.
104
+ return statusOf(error) === 128 || (error && error.status === 128);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {string} filePath - Env file path
110
+ * @param {Object} [deps] - `execFileSync` for tests
111
+ * @returns {string} File content
112
+ * @throws {Error} When the file is missing or git does not ignore it
113
+ */
114
+ function readEnvFile(filePath, deps = {}) {
115
+ if (!filePath || !fs.existsSync(filePath)) {
116
+ throw new Error(`Env file not found: ${filePath || '(no path resolved)'}`);
117
+ }
118
+ if (!isIgnoredByGit(filePath, deps)) {
119
+ throw new Error(
120
+ `Refusing to read ${filePath}: git does not ignore it. ` +
121
+ 'A secrets file inside a tracked path can be committed by accident. ' +
122
+ 'Add it to .gitignore, or pass --file with a path outside the repository.'
123
+ );
124
+ }
125
+ return fs.readFileSync(filePath, 'utf8');
126
+ }
127
+
128
+ /**
129
+ * Push one row. Returns an outcome; never returns or throws the value.
130
+ * @param {Object} deps - Injected api
131
+ * @param {Object} ctx - controllerUrl, authConfig, envKey
132
+ * @param {Object} row - name, catalogKey, value
133
+ * @returns {Promise<{ name: string, catalogKey: string, outcome: string, fatal?: boolean }>} Result
134
+ */
135
+ async function pushRow(deps, ctx, row) {
136
+ const api = deps.api || {
137
+ putSystemSecret, listEnvironmentConfiguration, putEnvironmentConfiguration
138
+ };
139
+ try {
140
+ const common = [ctx.controllerUrl, ctx.authConfig, ctx.envKey];
141
+ const response = row.sensitive
142
+ ? await api.putSystemSecret(
143
+ ...common, row.catalogKey, row.value, row.revision
144
+ )
145
+ : await api.putEnvironmentConfiguration(
146
+ ...common, row.application, row.catalogKey,
147
+ { value: row.value, revision: row.revision }
148
+ );
149
+ if (isFailedResponse(response)) {
150
+ const { outcome, fatal } = classifyFailure(response);
151
+ return { name: row.name, catalogKey: row.catalogKey, outcome, fatal };
152
+ }
153
+ return { name: row.name, catalogKey: row.catalogKey, outcome: OUTCOME.updated };
154
+ } catch (error) {
155
+ const { outcome, fatal } = classifyFailure(error);
156
+ return { name: row.name, catalogKey: row.catalogKey, outcome, fatal };
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ OUTCOME,
162
+ classifyFailure,
163
+ statusOf,
164
+ isIgnoredByGit,
165
+ isFailedResponse,
166
+ readEnvFile,
167
+ pushRow
168
+ };
@@ -8,6 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { repairKeyedDocument } = require('./repair-datasource-keyed-document');
11
12
  const { repairOpenapiSection } = require('./repair-datasource-openapi');
12
13
  const { repairSyncOperationsFromCapabilities } = require('./repair-datasource-sync-operations');
13
14
  const { normalizeAutoRbacOperationKeys } = require('./repair-datasource-auto-rbac');
@@ -132,6 +133,7 @@ function runRepairDatasourceFile(parsed, options, changes, helpers) {
132
133
 
133
134
  const none = helpers.isNoneEntityType(parsed?.entityType);
134
135
 
136
+ updated = repairKeyedDocument(parsed, options, out) || updated;
135
137
  updated = applyBaseDatasourceRepairs(parsed, none, out, options, helpers) || updated;
136
138
  updated = applyOpenapiDatasourceRepairs(parsed, out) || updated;
137
139
  updated = applyOptionalDatasourceRepairs(parsed, options, none, out, helpers) || updated;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @fileoverview Generate keyed document retrieval from vendor OpenAPI response media types.
3
+ * @author AI Fabrix Team
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const { toCamelCaseKey } = require('./repair-datasource-auto-rbac');
9
+ const { loadConfigFile } = require('../utils/config-format');
10
+ const { documentKeyToLocalOpenApiPath } = require('./repair-openapi-export');
11
+ const { findVendorOperation } = require('../validation/openapi-contract-vendor-validation');
12
+
13
+ /** @param {Object} ds @param {Object} options @returns {Object|null} */
14
+ function loadVendorSpec(ds, options) {
15
+ if (options.openapiSpec) return options.openapiSpec;
16
+ if (!options.appPath || !ds.openapi?.documentKey) return null;
17
+ const file = documentKeyToLocalOpenApiPath(options.appPath, ds.systemKey || '', ds.openapi.documentKey);
18
+ return fs.existsSync(file) ? loadConfigFile(file) : null;
19
+ }
20
+
21
+ /** @param {Object} operation @returns {Object[]} */
22
+ function binaryResponses(operation) {
23
+ return Object.entries(operation?.responses || {})
24
+ .filter(([status]) => /^2(?:\d\d|XX)$/i.test(status))
25
+ .map(([, response]) => response)
26
+ .filter(response => Object.keys(response?.content || {}).some(type =>
27
+ /^(application\/(octet-stream|pdf)|image\/[^;]+)(;|$)/i.test(type)));
28
+ }
29
+
30
+ /** @param {Object} ds @param {Object} operation @param {Object[]} responses */
31
+ function addMetadataMappings(ds, operation, responses) {
32
+ ds.fieldMappings = ds.fieldMappings || {};
33
+ ds.fieldMappings.attributes = ds.fieldMappings.attributes || {};
34
+ const attributes = ds.fieldMappings.attributes;
35
+ for (const response of responses) {
36
+ for (const name of Object.keys(response.headers || {})) {
37
+ addMappingStub(attributes, name, 'headers');
38
+ }
39
+ }
40
+ for (const param of operation.parameters || []) {
41
+ if (param.required && ['query', 'path'].includes(param.in) && param.name) {
42
+ addMappingStub(attributes, param.name, 'params');
43
+ }
44
+ }
45
+ }
46
+
47
+ /** @param {Object} attributes @param {string} name @param {string} source */
48
+ function addMappingStub(attributes, name, source) {
49
+ const key = toCamelCaseKey(name);
50
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(key) || Object.hasOwn(attributes, key)) return;
51
+ attributes[key] = { expression: `{{raw.${source}.${name}}}` };
52
+ }
53
+
54
+ /** @param {Object} ds @param {string} ref @returns {Object[]} */
55
+ function ensureGetSteps(ds, ref) {
56
+ ds.execution = ds.execution || {};
57
+ ds.execution.engine = 'cip';
58
+ ds.execution.cip = ds.execution.cip || { version: '1.0', operations: {} };
59
+ ds.execution.cip.operations = ds.execution.cip.operations || {};
60
+ const operations = ds.execution.cip.operations;
61
+ if (!operations.get) {
62
+ operations.get = { steps: [
63
+ { fetch: { source: 'openapi', openapiRef: ref, responseType: 'binary' } },
64
+ { map: { useFieldMappings: true } },
65
+ { filter: { abac: { mode: 'mandatory' } } },
66
+ { output: { mode: 'documents' } }
67
+ ] };
68
+ }
69
+ return Array.isArray(operations.get.steps) ? operations.get.steps : [];
70
+ }
71
+
72
+ /**
73
+ * Leave list-based document sync and author-owned unrelated operations unchanged.
74
+ * @param {Object} ds - Datasource, mutated
75
+ * @param {Object} options - Vendor spec or local appPath
76
+ * @param {string[]} changes - Repair log
77
+ * @returns {boolean}
78
+ */
79
+ function repairKeyedDocument(ds, options = {}, changes = []) {
80
+ const candidate = keyedDocumentCandidate(ds, options);
81
+ if (!candidate) return false;
82
+ const { ref, vendor, responses } = candidate;
83
+ const before = JSON.stringify(ds);
84
+ const getSteps = ensureGetSteps(ds, ref);
85
+ if (!getSteps.some(step => step?.fetch) || !getSteps.some(step => step?.output)) return false;
86
+ for (const step of getSteps) {
87
+ if (step?.fetch && (step.fetch.openapiRef || 'get') === ref) step.fetch.responseType = 'binary';
88
+ if (step?.output) step.output.mode = 'documents';
89
+ }
90
+ ds.documentStorage = { enabled: true, ingestAfterSync: true,
91
+ ...ds.documentStorage, responseType: 'binary', binaryOperationRef: 'get' };
92
+ addMetadataMappings(ds, vendor, responses);
93
+ if (JSON.stringify(ds) === before) return false;
94
+ changes.push('Configured keyed binary get with documents output and header/parameter mappings');
95
+ return true;
96
+ }
97
+
98
+ /** @param {Object} ds @param {Object} options @returns {Object|null} */
99
+ function keyedDocumentCandidate(ds, options) {
100
+ if (!['documentStorage', 'document-storage'].includes(ds?.entityType)) return null;
101
+ if (!canGenerateKeyedDocument(ds)) return null;
102
+ const operations = ds.execution?.cip?.operations || {};
103
+ const steps = operations.get?.steps || [];
104
+ const fetch = steps.find(step => step?.fetch)?.fetch;
105
+ if (fetch && !['openapi', undefined].includes(fetch.source)) return null;
106
+ const ref = fetch?.openapiRef || 'get';
107
+ const binding = ds.openapi?.operations?.[ref];
108
+ const vendor = findVendorOperation(loadVendorSpec(ds, options), binding);
109
+ const responses = binaryResponses(vendor);
110
+ return responses.length ? { ref, vendor, responses } : null;
111
+ }
112
+
113
+ /** @param {Object} ds @returns {boolean} */
114
+ function canGenerateKeyedDocument(ds) {
115
+ if (ds.execution?.cip?.operations?.list || ds.openapi?.operations?.list) return false;
116
+ if (ds.capabilities?.some(cap => (cap.key || cap) === 'list')) return false;
117
+ if (ds.documentStorage?.enabled === false) return false;
118
+ if (ds.execution?.engine && ds.execution.engine !== 'cip') return false;
119
+ return !ds.documentStorage?.binaryOperationRef || ds.documentStorage.binaryOperationRef === 'get';
120
+ }
121
+
122
+ module.exports = { repairKeyedDocument };
@@ -37,6 +37,7 @@ function runDatasourceRepairs(appPath, datasourceFiles, options, dryRun, changes
37
37
  try {
38
38
  const parsed = loadConfigFile(filePath);
39
39
  const { updated: fileUpdated, changes: fileChanges } = repairDatasourceFile(parsed, {
40
+ appPath,
40
41
  expose: options.expose,
41
42
  sync: options.sync,
42
43
  test: options.test,
@@ -41,6 +41,10 @@ const {
41
41
  patchCaseStatusAfterRun
42
42
  } = require('../role-assistant/test-pack-sync');
43
43
  const { collectString } = require('./governance');
44
+ const {
45
+ hasRoleAssistantTestSubject,
46
+ runPublishedCaseAsSubject
47
+ } = require('../role-assistant/test-job-runner');
44
48
  const {
45
49
  sectionTitle,
46
50
  headerKeyValue,
@@ -338,6 +342,9 @@ async function resolveExecutableCaseResult(roleKey, roleCase, opts, result, verd
338
342
  run: false,
339
343
  noPublish: true
340
344
  });
345
+ if (hasRoleAssistantTestSubject(opts)) {
346
+ return runPublishedCaseAsSubject(roleKey, roleCase, opts, authContext);
347
+ }
341
348
  const evidencePreflight = await assertRoleAssistantEvidencePreflight(roleCase, authContext);
342
349
  const executionResult = await runRoleAssistantCaseLifecycle(roleKey, roleCase, authContext, opts);
343
350
  const executionVerdict = lifecycleVerdictFromCaseExecution(executionResult, verdict);
@@ -322,8 +322,7 @@ function removeUserLocalSecrets() {
322
322
  }
323
323
 
324
324
  /**
325
- * Fresh install: wizard collects admin email + password, then AI tool keys,
326
- * then up-infra + up-platform.
325
+ * Fresh install: wizard collects admin credentials, then runs up-infra + up-platform.
327
326
  *
328
327
  * @async
329
328
  * @param {{ adminEmail: string, adminPassword?: string, passwordBundle?: object }} adminCreds
@@ -345,9 +344,6 @@ async function runFreshInstall(adminCreds) {
345
344
  });
346
345
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');
347
346
  await runSetupImageRefresh({ quiet: true });
348
- // Bring up infra first, then collect optional platform-level secrets (AI tool)
349
- // so the flow feels like a product installer: infra → platform configuration → platform services.
350
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
351
347
  // Fresh-install should reset dev defaults (env + controller URL) the same way
352
348
  // as other setup recovery paths, otherwise the platform can come up pointed at
353
349
  // stale configuration.
@@ -382,7 +378,6 @@ async function runReinstall() {
382
378
  await runSetupImageRefresh({ quiet: true });
383
379
  const adminCreds = await setupPrompts.promptAdminCredentials();
384
380
  await config.setAdminEmail(adminCreds.adminEmail);
385
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
386
381
  const infraSpin = startSpinner('Starting infrastructure...');
387
382
  await startInfraFromConfig({
388
383
  adminEmail: adminCreds.adminEmail,
@@ -436,7 +431,6 @@ async function runWipeData() {
436
431
  });
437
432
  stopSpinnerSuccess(wipeSpin, 'Databases reset');
438
433
  await runSetupImageRefresh({ quiet: true });
439
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
440
434
  const infraSpin = startSpinner('Starting infrastructure...');
441
435
  await startInfraFromConfig({});
442
436
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');
@@ -460,7 +454,6 @@ async function runWipeData() {
460
454
  */
461
455
  async function runUpdateImages() {
462
456
  await runSetupImageRefresh({ quiet: true });
463
- await setupPrompts.promptAiTool({ silentIfConfigured: true });
464
457
  const infraSpin = startSpinner('Starting infrastructure...');
465
458
  await startInfraFromConfig({});
466
459
  stopSpinnerSuccess(infraSpin, 'Infrastructure ready');