@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
@@ -56,16 +56,17 @@ function readDeveloperIdFromConfig(config) {
56
56
  * Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
57
57
  * @param {string} content - Env file content
58
58
  * @param {string} outputPath - Resolved path of the .env file being written
59
+ * @param {string} [appName] - System key (defaults to output directory name)
59
60
  * @returns {string} Content with /mnt/data replaced by path to mount directory
60
61
  */
61
- function substituteMntDataForLocal(content, outputPath) {
62
+ function substituteMntDataForLocal(content, outputPath, appName) {
62
63
  const outputDir = path.dirname(outputPath);
63
64
  // Avoid introducing a drive letter for root-relative paths (e.g. "\tmp\out"),
64
65
  // which are common in unit tests and some Windows path constructions.
65
66
  const isWinRootRelative = process.platform === 'win32' && /^\\(?!\\)/.test(outputDir);
66
67
  const localMountPath = isWinRootRelative
67
- ? path.win32.normalize(path.win32.join(outputDir, 'mount'))
68
- : path.resolve(outputDir, 'mount');
68
+ ? path.win32.normalize(path.win32.join(outputDir, 'mount', 'data', appName || path.basename(outputDir)))
69
+ : path.resolve(outputDir, 'mount', 'data', appName || path.basename(outputDir));
69
70
  if (!fs.existsSync(localMountPath)) {
70
71
  fs.mkdirSync(localMountPath, { recursive: true });
71
72
  }
@@ -140,12 +141,12 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
140
141
  baseContent = await fsp.readFile(outputPath, 'utf8');
141
142
  } else if (appName) {
142
143
  baseContent = await generateEnvContent(appName, null, 'local', false);
143
- baseContent = substituteMntDataForLocal(baseContent, outputPath);
144
+ baseContent = substituteMntDataForLocal(baseContent, outputPath, appName);
144
145
  } else {
145
146
  baseContent = runContent;
146
147
  }
147
148
  let toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
148
- toWrite = substituteMntDataForLocal(toWrite, outputPath);
149
+ toWrite = substituteMntDataForLocal(toWrite, outputPath, appName);
149
150
  await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
150
151
  logger.log(
151
152
  formatSuccessLine(
@@ -163,7 +164,7 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
163
164
  async function writeEnvOutputForLocal(appName, outputPath) {
164
165
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
165
166
  let localContent = await generateEnvContent(appName, null, 'local', false);
166
- localContent = substituteMntDataForLocal(localContent, outputPath);
167
+ localContent = substituteMntDataForLocal(localContent, outputPath, appName);
167
168
  let toWrite = localContent;
168
169
  if (fs.existsSync(outputPath)) {
169
170
  const existingContent = await fsp.readFile(outputPath, 'utf8');
@@ -319,7 +320,7 @@ async function patchEnvContentForLocal(envContent, variables) {
319
320
  async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel, copyOptions = {}) {
320
321
  const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
321
322
  const raw = fs.readFileSync(envPath, 'utf8');
322
- const synced = substituteMntDataForLocal(raw, outputPath);
323
+ const synced = substituteMntDataForLocal(raw, outputPath, copyOptions.appName || path.basename(path.dirname(envPath)));
323
324
  let toWrite = synced;
324
325
  if (!copyOptions.freshEnv && fs.existsSync(outputPath)) {
325
326
  const existingContent = fs.readFileSync(outputPath, 'utf8');
@@ -344,7 +345,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
344
345
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
345
346
  const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
346
347
  let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
347
- localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
348
+ localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath, appName);
348
349
  let toWrite = localEnvContent;
349
350
  if (!extraOpts.freshEnv && fs.existsSync(outputPath)) {
350
351
  const existingContent = fs.readFileSync(outputPath, 'utf8');
@@ -366,7 +367,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
366
367
  async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
367
368
  const envContent = fs.readFileSync(envPath, 'utf8');
368
369
  let patchedContent = await patchEnvContentForLocal(envContent, variables);
369
- patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
370
+ patchedContent = substituteMntDataForLocal(patchedContent, outputPath, variables.app?.key || path.basename(path.dirname(envPath)));
370
371
  fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
371
372
  logger.log(formatSuccessLine(`Copied .env to: ${envOutputPathLabel}`));
372
373
  }
@@ -405,7 +406,7 @@ async function copyOrRegenerateEnvForNamedApp(opts) {
405
406
  return;
406
407
  }
407
408
  if (fs.existsSync(envPath)) {
408
- await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true });
409
+ await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true, appName });
409
410
  return;
410
411
  }
411
412
  await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
@@ -182,7 +182,9 @@ function pickBlockingDatasourceKey(rows) {
182
182
  }
183
183
 
184
184
  function issueKey(issue) {
185
- const code = issue && issue.code ? String(issue.code) : '';
185
+ const code = issue && (issue.stableCode || issue.code)
186
+ ? String(issue.stableCode || issue.code)
187
+ : '';
186
188
  const msg = issue && issue.message ? String(issue.message) : '';
187
189
  return `${code}::${msg}`.toLowerCase();
188
190
  }
@@ -402,4 +404,3 @@ module.exports = {
402
404
  systemCertStatus,
403
405
  drillDownCommand
404
406
  };
405
-
@@ -48,7 +48,7 @@ function expandTag(entry, version) {
48
48
  * @throws {Error} On blank entries, unsupported placeholders, missing version or invalid tags
49
49
  */
50
50
  function resolveImageTags(tagInput, appVersion) {
51
- if (tagInput === undefined || tagInput === null || String(tagInput).trim() === '') {
51
+ if (tagInput === undefined || tagInput === null) {
52
52
  return [DEFAULT_TAG];
53
53
  }
54
54
  const entries = String(tagInput).split(',').map((entry) => entry.trim());
@@ -72,7 +72,7 @@ function resolveImageTags(tagInput, appVersion) {
72
72
  */
73
73
  function resolveBuildTagPlan(tagInput, appVersion) {
74
74
  const tags = resolveImageTags(tagInput, appVersion);
75
- const explicitSingle = tags.length === 1 && tagInput !== undefined && tagInput !== null && String(tagInput).trim() !== '';
75
+ const explicitSingle = tagInput !== undefined && tagInput !== null && !String(tagInput).includes(',');
76
76
  if (explicitSingle && !FLOATING_TAGS.includes(tags[0])) {
77
77
  tags.push(DEFAULT_TAG);
78
78
  }
@@ -42,7 +42,7 @@ function isPlatformCatalogKvRef(val) {
42
42
 
43
43
  /**
44
44
  * True when repair should normalize a kv value to path-style kv://systemKey/variable.
45
- * Platform catalog refs (e.g. kv://secrets-openaiApiKeyVault) are never legacy.
45
+ * Platform catalog refs (e.g. kv://redis-passwordKeyVault) are never legacy.
46
46
  *
47
47
  * @param {string} val - Value from authentication.security or configuration
48
48
  * @returns {boolean}
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Platform and environment configuration resolution (plan 200.0).
3
+ *
4
+ * The deployment's Azure resource group is matched against the manifest `platforms` list, yielding
5
+ * `{platform}` and `{platform.name}`. Platform values expand from those, then the matching
6
+ * `environments` entry overrides by name. The result is one flat set of names that `env.template`
7
+ * references as `{NAME}`.
8
+ *
9
+ * Pure by contract: manifest + resource group + environment key in, resolved set out. No
10
+ * filesystem, no Azure, no network — the same function must be implementable in Miso and produce
11
+ * identical output for the shared test vectors.
12
+ *
13
+ * @fileoverview Resource-group driven configuration resolution
14
+ * @author AI Fabrix Team
15
+ * @version 1.0.0
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ /** Local Docker has no resource group; every local run resolves as this platform. */
21
+ const LOCAL_PLATFORM = 'dev';
22
+ /** Capture for `*` in a resource pattern: one group-name segment. */
23
+ const NAME_CAPTURE = '[A-Za-z0-9][A-Za-z0-9-]*';
24
+ const NAME_WILDCARD = '*';
25
+
26
+ function escapeLiteral(text) {
27
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+
30
+ /**
31
+ * Compiles a `resource` pattern into a matcher.
32
+ * `{platform}` matches one of `options`; `*` captures a name segment.
33
+ * @param {Object} entry - Platform entry
34
+ * @returns {{ regex: RegExp, order: string[] }} Matcher and the capture order
35
+ */
36
+ function compileResourcePattern(entry) {
37
+ const order = [];
38
+ let source = '';
39
+ const pattern = String(entry.resource || '');
40
+ const tokens = pattern.split(/(\{platform\}|\*)/);
41
+ for (const token of tokens) {
42
+ if (token === '{platform}') {
43
+ const options = Array.isArray(entry.options) ? entry.options : [];
44
+ source += `(${options.map(escapeLiteral).join('|')})`;
45
+ order.push('platform');
46
+ } else if (token === NAME_WILDCARD) {
47
+ source += `(${NAME_CAPTURE})`;
48
+ order.push('name');
49
+ } else if (token) {
50
+ source += escapeLiteral(token);
51
+ }
52
+ }
53
+ return { regex: new RegExp(`^${source}$`), order };
54
+ }
55
+
56
+ /**
57
+ * @param {Object} entry - Platform entry
58
+ * @throws {Error} When the entry cannot yield a platform
59
+ */
60
+ function assertPlatformDeclaration(entry) {
61
+ const hasToken = String(entry.resource || '').includes('{platform}');
62
+ const hasOptions = Array.isArray(entry.options) && entry.options.length > 0;
63
+ if (hasToken && !hasOptions) {
64
+ throw new Error(`Platform "${entry.resource}" uses {platform} but declares no options`);
65
+ }
66
+ if (!hasToken && !entry.platform) {
67
+ throw new Error(`Platform "${entry.resource}" has no {platform} token, so it must declare platform:`);
68
+ }
69
+ if (hasToken && entry.platform) {
70
+ throw new Error(`Platform "${entry.resource}" declares both {platform} and platform:; use one`);
71
+ }
72
+ }
73
+
74
+ function matchEntry(entry, resourceGroup) {
75
+ assertPlatformDeclaration(entry);
76
+ const { regex, order } = compileResourcePattern(entry);
77
+ const found = regex.exec(resourceGroup);
78
+ if (!found) return null;
79
+ const captured = {};
80
+ order.forEach((key, index) => {
81
+ captured[key] = found[index + 1];
82
+ });
83
+ const platform = captured.platform || entry.platform;
84
+ const name = entry.name === NAME_WILDCARD ? captured.name : entry.name;
85
+ if (!name) {
86
+ throw new Error(`Platform "${entry.resource}" uses name: * but the pattern captures no name segment`);
87
+ }
88
+ return { entry, platform, name };
89
+ }
90
+
91
+ /**
92
+ * Matches a resource group against the manifest platforms.
93
+ * @param {Array} platforms - Manifest `platforms`
94
+ * @param {string} resourceGroup - Target Azure resource group
95
+ * @returns {{ entry: Object, platform: string, name: string }} The single match
96
+ * @throws {Error} On no match or more than one match
97
+ */
98
+ function matchPlatform(platforms, resourceGroup) {
99
+ const entries = Array.isArray(platforms) ? platforms : [];
100
+ const group = String(resourceGroup || '').trim();
101
+ if (!group) {
102
+ throw new Error('A resource group is required to resolve a platform');
103
+ }
104
+ const matches = entries.map(entry => matchEntry(entry, group)).filter(Boolean);
105
+ if (matches.length === 0) {
106
+ const tried = entries.map(entry => `"${entry.resource}"`).join(', ') || '(none declared)';
107
+ throw new Error(`Resource group "${group}" matches no platform. Patterns tried: ${tried}`);
108
+ }
109
+ if (matches.length > 1) {
110
+ const ambiguous = matches.map(match => `"${match.entry.resource}"`).join(', ');
111
+ throw new Error(`Resource group "${group}" matches more than one platform: ${ambiguous}. Make the patterns exclusive.`);
112
+ }
113
+ return matches[0];
114
+ }
115
+
116
+ /**
117
+ * Expands `{platform}`, `{platform.name}` and `{resourceGroup}` in a value.
118
+ * @param {string} value - Raw value
119
+ * @param {Object} context - `platform`, `name`, `resourceGroup`
120
+ * @returns {string}
121
+ * @throws {Error} On an unknown token
122
+ */
123
+ function expandPlatformTokens(value, context) {
124
+ const text = value === undefined || value === null ? '' : String(value);
125
+ return text.replace(/\{([a-zA-Z][a-zA-Z.]*)\}/g, (match, token) => {
126
+ if (token === 'platform') return context.platform;
127
+ if (token === 'platform.name') return context.name;
128
+ if (token === 'resourceGroup') return context.resourceGroup;
129
+ throw new Error(`Unsupported token ${match}; use {platform}, {platform.name} or {resourceGroup}`);
130
+ });
131
+ }
132
+
133
+ function applyItems(target, items, context) {
134
+ for (const item of Array.isArray(items) ? items : []) {
135
+ target.set(item.name, expandPlatformTokens(item.value, context));
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Local runs have no resource group, but still need platform values. The first entry that can
141
+ * produce the local platform supplies them; an `environments` entry still overrides by name.
142
+ * A `*` name cannot be captured without a group, so it resolves to ''.
143
+ * @param {Array} platforms - Manifest platforms
144
+ * @returns {{ entry: Object, platform: string, name: string }|null}
145
+ */
146
+ function matchLocalPlatform(platforms) {
147
+ const entry = platforms.find(item =>
148
+ item.platform === LOCAL_PLATFORM ||
149
+ (Array.isArray(item.options) && item.options.includes(LOCAL_PLATFORM)));
150
+ if (!entry) return null;
151
+ return { entry, platform: LOCAL_PLATFORM, name: entry.name === NAME_WILDCARD ? '' : entry.name };
152
+ }
153
+
154
+ /**
155
+ * @param {Object} manifest - Application manifest
156
+ * @param {string} resourceGroup - Target resource group ('' for local Docker)
157
+ * @returns {{ entry: Object, platform: string, name: string }|null}
158
+ */
159
+ function matchIfDeclared(manifest, resourceGroup, allowLocalPlatform) {
160
+ const platforms = manifest && Array.isArray(manifest.platforms) ? manifest.platforms : [];
161
+ if (platforms.length === 0) return null;
162
+ if (!resourceGroup) return allowLocalPlatform ? matchLocalPlatform(platforms) : null;
163
+ return matchPlatform(platforms, resourceGroup);
164
+ }
165
+
166
+ function environmentEntry(manifest, environment) {
167
+ const entries = manifest && Array.isArray(manifest.environments) ? manifest.environments : [];
168
+ return entries.find(item => item.environment === environment) || null;
169
+ }
170
+
171
+ function applyBuiltIns(values, context, environment) {
172
+ const builtIns = [
173
+ // PLATFORM is omitted when nothing determined it, so a consumer fails closed on {PLATFORM}
174
+ // rather than receiving a guess.
175
+ ...(context.platform ? [['PLATFORM', context.platform]] : []),
176
+ ['PLATFORM_NAME', context.name],
177
+ ['RESOURCE_GROUP', context.resourceGroup],
178
+ ['ENVIRONMENT', environment]
179
+ ];
180
+ // Built-ins never overwrite an authored name of the same spelling.
181
+ for (const [key, value] of builtIns) {
182
+ if (!values.has(key)) values.set(key, value);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Resolves the configuration set for one deployment.
188
+ * @param {Object} manifest - Application manifest with optional `platforms` / `environments`
189
+ * @param {Object} target - `resourceGroup` (omitted for local Docker), `environment`, and
190
+ * `allowLocalPlatform` (default true). A caller that is not a local run — the Controller, which
191
+ * knows the environment but may not know the group — passes false so a missing group resolves no
192
+ * platform at all instead of silently claiming `dev`.
193
+ * @returns {{ platform: string|null, name: string|null, resourceGroup: string|null, values: Object }}
194
+ */
195
+ function resolveConfiguration(manifest, target = {}) {
196
+ const resourceGroup = target.resourceGroup ? String(target.resourceGroup).trim() : '';
197
+ const environment = target.environment ? String(target.environment).trim() : '';
198
+ const allowLocalPlatform = target.allowLocalPlatform !== false;
199
+ const matched = matchIfDeclared(manifest, resourceGroup, allowLocalPlatform);
200
+ const localPlatform = allowLocalPlatform ? LOCAL_PLATFORM : '';
201
+ const context = {
202
+ platform: matched ? matched.platform : localPlatform,
203
+ name: matched ? matched.name : '',
204
+ resourceGroup
205
+ };
206
+
207
+ const values = new Map();
208
+ if (matched) applyItems(values, matched.entry.configuration, context);
209
+ const envEntry = environmentEntry(manifest, environment);
210
+ if (envEntry) applyItems(values, envEntry.configuration, context);
211
+ applyBuiltIns(values, context, environment);
212
+
213
+ return {
214
+ platform: context.platform || null,
215
+ name: matched && matched.name ? matched.name : null,
216
+ resourceGroup: resourceGroup || null,
217
+ values: Object.fromEntries(values)
218
+ };
219
+ }
220
+
221
+ module.exports = {
222
+ LOCAL_PLATFORM,
223
+ matchPlatform,
224
+ expandPlatformTokens,
225
+ resolveConfiguration
226
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Prepare persistent storage before starting a local application container.
3
+ * @fileoverview Local storage mount and configured DATA_DIR initialization
4
+ */
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const dotenv = require('dotenv');
10
+
11
+ /**
12
+ * Read only a DATA_DIR beneath the container's storage mount.
13
+ * @param {string|undefined} envFile - Resolved application env file
14
+ * @returns {string[]} Relative directory components
15
+ */
16
+ function dataDirectoryParts(envFile) {
17
+ if (!envFile || !fs.existsSync(envFile)) return [];
18
+ const { DATA_DIR: dataDir } = dotenv.parse(fs.readFileSync(envFile, 'utf8'));
19
+ if (!dataDir || !dataDir.startsWith('/mnt/data/')) return [];
20
+ const parts = dataDir.slice('/mnt/data/'.length).split('/').filter(part => part && part !== '.');
21
+ if (parts.some(part => part === '..' || part.includes('\\') || part.includes('\0'))) {
22
+ throw new Error('DATA_DIR must remain inside the /mnt/data storage mount');
23
+ }
24
+ return parts;
25
+ }
26
+
27
+ /**
28
+ * Create the mount root and any configured DATA_DIR inside it, preserving state.
29
+ * Refuse child symlinks so initialization cannot write outside the storage root.
30
+ * @param {string} localPath - Host directory mounted at /mnt/data
31
+ * @param {string|undefined} envFile - Resolved application env file
32
+ * @returns {void}
33
+ */
34
+ function prepareLocalDataMount(localPath, envFile) {
35
+ try {
36
+ const parts = dataDirectoryParts(envFile);
37
+ fs.mkdirSync(localPath, { recursive: true });
38
+ let directory = localPath;
39
+ for (const part of parts) {
40
+ directory = path.join(directory, part);
41
+ try {
42
+ const stat = fs.lstatSync(directory);
43
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
44
+ throw new Error('DATA_DIR components must be directories, not symlinks or files');
45
+ }
46
+ } catch (error) {
47
+ if (error.code !== 'ENOENT') throw error;
48
+ fs.mkdirSync(directory);
49
+ }
50
+ }
51
+ } catch (error) {
52
+ // Filesystem errors can include env content or paths; report only their code.
53
+ const reason = error.code || error.message;
54
+ throw new Error(`Unable to prepare local storage before container startup: ${reason}`);
55
+ }
56
+ }
57
+
58
+ module.exports = { prepareLocalDataMount };
@@ -148,16 +148,21 @@ function qualifyRepository(registry, repository) {
148
148
  }
149
149
 
150
150
  /**
151
- * Deployment/registration image reference (`registry/name:tag`) using the shared precedence.
151
+ * Deployment/registration image reference (`registry/name:tag`).
152
+ *
153
+ * Deliberately ignores the installation registry (`dev set-registry`): a generated manifest is a
154
+ * committed artifact shared by the team, so it must carry the application's own `image.registry`,
155
+ * not one developer's local default. `push` applies the installation registry when it publishes,
156
+ * and an explicit `options.registry` still wins here for CI that targets another registry.
152
157
  * @param {Object} variables - Application manifest
153
158
  * @param {string} fallbackName - Name when the manifest has no image name or app key
154
- * @param {Object} [options] - Same options as {@link resolveEffectiveRegistry}
159
+ * @param {Object} [options] - `registry` (explicit override only)
155
160
  * @returns {string}
156
161
  */
157
162
  function buildManifestImageReference(variables, fallbackName, options = {}) {
158
163
  const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
159
164
  const tag = variables?.image?.tag || 'latest';
160
- const { registry } = resolveEffectiveRegistry(variables, options);
165
+ const { registry } = resolveEffectiveRegistry(variables, { ...options, installationRegistry: '' });
161
166
  return `${qualifyRepository(registry, imageName)}:${tag}`;
162
167
  }
163
168
 
@@ -55,7 +55,6 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
55
55
  const EMPTY_ALLOWED_KV_FALLBACK = new Set([
56
56
  'redis-passwordKeyVault',
57
57
  'azure-openaiapi-urlKeyVault',
58
- 'secrets-azureOpenaiApiKeyVault',
59
58
  'miso-controller-secrets-apiKeyVault'
60
59
  ]);
61
60
 
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Map an application's `.env` variables onto Miso catalog keys.
3
+ *
4
+ * The `.env` is keyed by environment variable (`MORI_POLICY_PRIVATE_KEY`) while the system
5
+ * secrets API is keyed by catalog key (`secrets-policyPrivateKeyVault`). The mapping is not
6
+ * invented here: it is read from the application's own `env.template`, because the
7
+ * `NAME=kv://<catalogKey>` line is already the declaration that decides where the value is
8
+ * read from at runtime. A second table in the CLI would be free to drift from it.
9
+ *
10
+ * A variable whose template line is not a `kv://` reference is ordinary configuration, not a
11
+ * secret, and is skipped rather than pushed.
12
+ *
13
+ * Note this is the flat `kv://<catalogKey>` form used by the Controller's system secrets.
14
+ * The Dataplane's credential store uses `kv://<systemKey>/<path>`; see
15
+ * `credential-secrets-env-kv.js` for that one. They are not interchangeable.
16
+ *
17
+ * @fileoverview env.template kv:// declarations → catalog keys
18
+ * @author AI Fabrix Team
19
+ * @version 1.0.0
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ /** `NAME=value`, ignoring blank lines, comments and `export ` prefixes. */
25
+ const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
26
+
27
+ /**
28
+ * Strip surrounding quotes and trailing whitespace from an env value.
29
+ * @param {string} raw - Raw right-hand side
30
+ * @returns {string} Cleaned value
31
+ */
32
+ function cleanValue(raw) {
33
+ const trimmed = String(raw === undefined || raw === null ? '' : raw).trim();
34
+ if (trimmed.length >= 2) {
35
+ const first = trimmed[0];
36
+ const last = trimmed[trimmed.length - 1];
37
+ if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
38
+ return trimmed.slice(1, -1);
39
+ }
40
+ }
41
+ return trimmed;
42
+ }
43
+
44
+ /**
45
+ * Parse `NAME=value` lines into a Map, last assignment winning.
46
+ * @param {string} content - File content
47
+ * @returns {Map<string, string>} Variable name → value
48
+ */
49
+ function parseEnvAssignments(content) {
50
+ const out = new Map();
51
+ for (const line of String(content || '').split(/\r?\n/)) {
52
+ if (!line || /^\s*#/.test(line)) {
53
+ continue;
54
+ }
55
+ const match = line.match(ASSIGNMENT);
56
+ if (match) {
57
+ out.set(match[1], cleanValue(match[2]));
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /**
64
+ * Catalog keys declared by an `env.template`, keyed by environment variable name.
65
+ * Only `kv://` lines are secrets; everything else is skipped.
66
+ * @param {string} templateContent - env.template content
67
+ * @returns {Map<string, string>} Variable name → catalog key
68
+ */
69
+ function catalogKeysFromTemplate(templateContent) {
70
+ const out = new Map();
71
+ for (const [name, value] of parseEnvAssignments(templateContent)) {
72
+ if (!value.startsWith('kv://')) {
73
+ continue;
74
+ }
75
+ const catalogKey = value.slice('kv://'.length).trim();
76
+ if (catalogKey && !catalogKey.includes('/')) {
77
+ out.set(name, catalogKey);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /**
84
+ * Pair the values in a `.env` with the catalog keys its `env.template` declares.
85
+ *
86
+ * Skipped rows are returned rather than dropped, so the command can say why a variable was
87
+ * not pushed instead of silently ignoring it.
88
+ *
89
+ * @param {string} envContent - Operator's .env content
90
+ * @param {string} templateContent - The application's env.template content
91
+ * @param {Object} [options] - `only` restricts to one variable name
92
+ * @returns {{ pushable: Array<Object>, skipped: Array<Object> }} Rows carrying value and catalog key
93
+ */
94
+ function mapEnvToCatalogKeys(envContent, templateContent, options = {}) {
95
+ const declared = catalogKeysFromTemplate(templateContent);
96
+ const values = parseEnvAssignments(envContent);
97
+ const only = options.only ? String(options.only).trim() : '';
98
+ const pushable = [];
99
+ const skipped = [];
100
+ for (const [name, value] of values) {
101
+ if (only && name !== only) {
102
+ continue;
103
+ }
104
+ const catalogKey = declared.get(name);
105
+ if (!catalogKey) {
106
+ skipped.push({ name, reason: 'not a kv:// reference in env.template' });
107
+ continue;
108
+ }
109
+ if (!value) {
110
+ skipped.push({ name, catalogKey, reason: 'no value in the env file' });
111
+ continue;
112
+ }
113
+ pushable.push({ name, catalogKey, value });
114
+ }
115
+ if (only && pushable.length === 0 && skipped.length === 0) {
116
+ skipped.push({ name: only, reason: 'not present in the env file' });
117
+ }
118
+ return { pushable, skipped };
119
+ }
120
+
121
+ module.exports = {
122
+ parseEnvAssignments,
123
+ catalogKeysFromTemplate,
124
+ mapEnvToCatalogKeys
125
+ };
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs').promises;
11
11
  const path = require('path');
12
+ const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
12
13
 
13
14
  /**
14
15
  * Prepare object for JSON serialization (handles circular refs)
@@ -45,7 +46,7 @@ async function writeTestLog(appKey, data, logType = 'test-integration', integrat
45
46
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
46
47
  const filename = `${logType}-${timestamp}.json`;
47
48
  const filePath = path.join(logsDir, filename);
48
- const sanitized = sanitizeForLog(data);
49
+ const sanitized = sanitizeForLog(sanitizeBinaryEvidence(data));
49
50
  await fs.writeFile(filePath, JSON.stringify(sanitized, null, 2), 'utf8');
50
51
  return filePath;
51
52
  }
@@ -16,6 +16,7 @@ const path = require('path');
16
16
  const { formatValidationErrors } = require('../utils/error-formatter');
17
17
  const { validateFieldReferences } = require('../datasource/field-reference-validator');
18
18
  const { validateAbac } = require('../datasource/abac-validator');
19
+ const { validateBinaryDocumentsContract } = require('../datasource/binary-documents-validator');
19
20
  const { validateManifestRuntimeConfiguration } = require('./runtime-configuration-rules');
20
21
  const { validateAppContractSurface } = require('./openapi-contract-surface-validator');
21
22
 
@@ -147,9 +148,13 @@ function validateDatasources(manifest, ajv, externalDatasourceSchema, errors, wa
147
148
  } else {
148
149
  const fieldRefErrors = validateFieldReferences(datasource);
149
150
  const abacErrors = validateAbac(datasource);
151
+ const binaryDocuments = validateBinaryDocumentsContract(datasource);
150
152
  const prefix = `Datasource ${index + 1} (${datasource.key || 'unknown'}): `;
151
153
  fieldRefErrors.forEach(e => errors.push(prefix + e));
152
154
  abacErrors.forEach(e => errors.push(prefix + e));
155
+ // Codes are the dataplane's, so the offline answer matches the upload answer.
156
+ binaryDocuments.errors.forEach(e => errors.push(`${prefix}${e.code}: ${e.message}`));
157
+ binaryDocuments.warnings.forEach(w => warnings.push(`${prefix}${w.code}: ${w.message}`));
153
158
  }
154
159
  });
155
160
  }
@@ -426,5 +426,7 @@ module.exports = {
426
426
  validateDatasourceFileContractSurface,
427
427
  validateVendorOperationContract,
428
428
  validateServiceContractDatasourceContracts,
429
- findVendorOperation
429
+ findVendorOperation,
430
+ resolveVendorOpenApiSpec,
431
+ inferIntegrationContextFromPath
430
432
  };
@@ -111,11 +111,15 @@ function validateDatasourceFileExtras(parsed, filePath) {
111
111
  const { collectExternalDatasourceWarnings } = require('./datasource-warnings');
112
112
  const { validateDatasourceFileContractSurface } = require('./openapi-contract-surface-validator');
113
113
  const errors = [...validateFieldReferences(parsed), ...validateAbac(parsed)];
114
+ const { validateBinaryDocumentsContract } = require('../datasource/binary-documents-validator');
115
+ const binary = validateBinaryDocumentsContract(parsed);
116
+ errors.push(...binary.errors.map(issue => `${issue.code}: ${issue.message}`));
114
117
  const contractSurface = validateDatasourceFileContractSurface(parsed, filePath);
115
118
  errors.push(...contractSurface.errors);
116
119
  const warnings = [
117
120
  ...collectExternalDatasourceWarnings(parsed),
118
- ...contractSurface.warnings
121
+ ...contractSurface.warnings,
122
+ ...binary.warnings.map(issue => `${issue.code}: ${issue.message}`)
119
123
  ];
120
124
  return { errors, warnings };
121
125
  }