@aifabrix/builder 2.60.0 → 2.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) 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/api/applications.api.js +22 -2
  14. package/lib/api/bootstrap-snapshot.api.js +115 -0
  15. package/lib/api/configuration.api.js +131 -0
  16. package/lib/api/system-secrets.api.js +72 -0
  17. package/lib/app/deploy.js +14 -5
  18. package/lib/app/run-docker-fallback.js +6 -1
  19. package/lib/app/run-env-compose.js +5 -6
  20. package/lib/app/run-helpers.js +2 -1
  21. package/lib/app/run-parameter-sync.js +142 -0
  22. package/lib/app/run.js +6 -4
  23. package/lib/build/docker-build-args.js +5 -2
  24. package/lib/build/index.js +3 -2
  25. package/lib/build/standard-docker-build.js +6 -2
  26. package/lib/channels/channel-artifact-generator.js +13 -3
  27. package/lib/cli/infra-guided-footers.js +10 -8
  28. package/lib/cli/infra-guided.js +30 -10
  29. package/lib/cli/setup-app.js +5 -0
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  32. package/lib/cli/setup-platform.js +2 -0
  33. package/lib/commands/auth-config.js +2 -0
  34. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  35. package/lib/commands/datasource-capability.js +4 -2
  36. package/lib/commands/env-secret-context.js +113 -0
  37. package/lib/commands/env-secret-list.js +200 -0
  38. package/lib/commands/env-secret-push-confirm.js +54 -0
  39. package/lib/commands/env-secret-push-run.js +227 -0
  40. package/lib/commands/env-secret-push.js +168 -0
  41. package/lib/commands/login.js +4 -0
  42. package/lib/commands/repair-datasource-apply.js +5 -1
  43. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  44. package/lib/commands/repair-datasource-legacy.js +94 -0
  45. package/lib/commands/repair-datasource-run.js +1 -0
  46. package/lib/commands/setup-image-refresh.js +26 -0
  47. package/lib/commands/setup-modes.js +8 -9
  48. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  49. package/lib/commands/setup-prompts.js +2 -180
  50. package/lib/commands/setup.js +3 -0
  51. package/lib/commands/up-builder-api.js +24 -8
  52. package/lib/commands/up-common.js +16 -1
  53. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  54. package/lib/commands/up-dataplane-credentials.js +21 -0
  55. package/lib/commands/up-dataplane.js +37 -18
  56. package/lib/commands/up-integration-server.js +112 -0
  57. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  58. package/lib/commands/verify-operations-steps.js +13 -1
  59. package/lib/commands/wizard-config-normalizer.js +12 -5
  60. package/lib/commands/wizard-core.js +5 -157
  61. package/lib/commands/wizard-file-saving.js +163 -0
  62. package/lib/core/admin-secrets-env-overlay.js +8 -3
  63. package/lib/core/env-platform-expand.js +5 -1
  64. package/lib/core/environment-access-policy.js +121 -0
  65. package/lib/core/environment-mode-policy.js +177 -0
  66. package/lib/core/local-env-overrides.js +149 -0
  67. package/lib/core/secrets-ensure.js +4 -2
  68. package/lib/core/secrets-env-content.js +17 -10
  69. package/lib/core/secrets-env-write.js +11 -4
  70. package/lib/core/secrets-load.js +4 -2
  71. package/lib/datasource/binary-documents-validator.js +190 -0
  72. package/lib/datasource/capability/capability-manifest.js +1 -0
  73. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  74. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  75. package/lib/datasource/capability/upsert-operations.js +138 -0
  76. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  77. package/lib/datasource/validate.js +12 -5
  78. package/lib/deployment/installation/index.js +14 -1
  79. package/lib/deployment/installation/infra-catalog.js +3 -0
  80. package/lib/deployment/installation/local-environment-stage.js +285 -0
  81. package/lib/deployment/installation/local-installation.js +76 -0
  82. package/lib/deployment/installation/stage-input.js +5 -1
  83. package/lib/generator/builders.js +3 -1
  84. package/lib/generator/index.js +3 -0
  85. package/lib/generator/split-variables.js +2 -2
  86. package/lib/lifecycle/product-model.js +4 -3
  87. package/lib/lifecycle/report-display.js +3 -2
  88. package/lib/parameters/database-adoption.js +125 -0
  89. package/lib/parameters/infra-parameter-catalog.js +1 -1
  90. package/lib/parameters/physical-database-naming.js +136 -0
  91. package/lib/parameters/physical-secret-name.js +194 -0
  92. package/lib/parameters/rsa-secret-values.js +165 -0
  93. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  94. package/lib/programmatic/builder-help-governance.js +1 -1
  95. package/lib/programmatic/builder-help.js +1 -1
  96. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  97. package/lib/programmatic/run-channel-package.js +3 -1
  98. package/lib/programmatic/workspace-context.js +37 -2
  99. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  100. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  101. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  102. package/lib/role-assistant/test-runner-workhub.js +28 -19
  103. package/lib/schema/application-schema.json +14 -2
  104. package/lib/schema/external-datasource.schema.json +23 -3
  105. package/lib/schema/infra.parameter.yaml +440 -56
  106. package/lib/schema/infrastructure-schema.json +213 -120
  107. package/lib/utils/compose-generate-docker-compose.js +9 -14
  108. package/lib/utils/compose-generator.js +4 -7
  109. package/lib/utils/compose-miso-env.js +2 -2
  110. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  111. package/lib/utils/datasource-binary-evidence.js +92 -0
  112. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  113. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  114. package/lib/utils/datasource-test-run-display.js +8 -2
  115. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  116. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  117. package/lib/utils/declarative-url-ports.js +3 -0
  118. package/lib/utils/env-copy.js +11 -10
  119. package/lib/utils/env-environment-file-paths.js +105 -0
  120. package/lib/utils/environment-scoped-resources.js +48 -14
  121. package/lib/utils/external-system-system-test-tty.js +3 -2
  122. package/lib/utils/image-tags.js +2 -2
  123. package/lib/utils/paths-system-builder-keys.js +7 -1
  124. package/lib/utils/paths.js +7 -2
  125. package/lib/utils/platform-kv-ref.js +1 -1
  126. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  127. package/lib/utils/prepare-local-data-mount.js +58 -0
  128. package/lib/utils/redis-env-scope.js +21 -1
  129. package/lib/utils/registry-auth-sources.js +154 -0
  130. package/lib/utils/registry-credentials.js +131 -14
  131. package/lib/utils/secrets-generator.js +30 -21
  132. package/lib/utils/secrets-helpers.js +5 -8
  133. package/lib/utils/secrets-kv-scope.js +127 -28
  134. package/lib/utils/secrets-materialize-local.js +9 -0
  135. package/lib/utils/secrets-missing-error.js +35 -1
  136. package/lib/utils/system-secret-mapping.js +125 -0
  137. package/lib/utils/test-log-writer.js +2 -1
  138. package/lib/utils/token-manager.js +14 -8
  139. package/lib/utils/url-public-path-prefix.js +6 -10
  140. package/lib/validation/external-manifest-validator.js +5 -0
  141. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  142. package/lib/validation/validate-external-file.js +5 -1
  143. package/package.json +6 -4
  144. package/templates/agent-kit/agent-kit.yaml +1 -1
  145. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  146. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  147. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
  148. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
  149. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  150. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
  151. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  152. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  153. package/templates/agent-kit/skills/shared/status.md +3 -12
  154. package/templates/applications/builder-api/application.yaml +1 -1
  155. package/templates/applications/builder-api/env.template +5 -1
  156. package/templates/applications/dataplane/application.yaml +2 -2
  157. package/templates/applications/dataplane/env.template +10 -12
  158. package/templates/applications/integration-server/application.yaml +91 -0
  159. package/templates/applications/integration-server/env.template +215 -0
  160. package/templates/applications/keycloak/application.yaml +6 -1
  161. package/templates/applications/mirrored-templates.json +52 -0
  162. package/templates/applications/miso-controller/application.yaml +116 -2
  163. package/templates/applications/miso-controller/env.template +62 -17
  164. package/templates/applications/miso-controller/rbac.yaml +17 -0
  165. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  166. package/templates/marketplace/createUiDefinition.json +52 -4
  167. package/templates/marketplace/main.json +88 -10
  168. package/templates/python/docker-compose.hbs +1 -1
  169. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
  170. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  171. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  172. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  173. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  174. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  175. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  176. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  177. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Which environments a local installation may build in and select (plan 210.0).
3
+ *
4
+ * Two rules, kept together because they describe one position: `tst` and `pro` are
5
+ * **promotion targets**, not working environments.
6
+ *
7
+ * **Build is a dev activity.** `tst` and `pro` receive a promoted image — the same image run
8
+ * as another container under an environment-suffixed name. Building into them would mean the
9
+ * artifact in production was never the artifact that was tested, which is the property
10
+ * promotion exists to guarantee. Miso does not police this; a build leaves no trace of where
11
+ * it happened, so the refusal has to be here.
12
+ *
13
+ * **`tst` and `pro` need the front door.** They exist only in `full` platform mode, where
14
+ * Traefik routes each environment under its own path. `single-traefik` is a front door for
15
+ * **one** environment, and `single` is direct ports: several environments on one host collide
16
+ * on ports there. Allowing the selection in either would produce an environment that is
17
+ * unreachable rather than one that failed to start, so it is refused up front.
18
+ *
19
+ * Pure except for `resolveEffectiveBuildEnvironment`, which reads the configured environment.
20
+ *
21
+ * @fileoverview Build and platform-mode environment policy for local installations
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const {
27
+ SETUP_PLATFORM_MODE,
28
+ normalizeSetupPlatformMode
29
+ } = require('./setup-platform-mode');
30
+
31
+ /** Environments that receive a promoted artifact and are never built into. */
32
+ const PROMOTION_ONLY_ENVIRONMENTS = Object.freeze(['tst', 'pro']);
33
+
34
+ /**
35
+ * @param {unknown} envKey - Environment key
36
+ * @returns {string} Lowercase trimmed key
37
+ */
38
+ function normalizeEnvKey(envKey) {
39
+ return String(envKey ?? '').trim().toLowerCase();
40
+ }
41
+
42
+ /**
43
+ * @param {unknown} envKey - Environment key
44
+ * @returns {boolean} True when the environment only ever receives a promoted artifact
45
+ */
46
+ function isPromotionOnlyEnvironment(envKey) {
47
+ return PROMOTION_ONLY_ENVIRONMENTS.includes(normalizeEnvKey(envKey));
48
+ }
49
+
50
+ /**
51
+ * Refuse a build that would target a promotion-only environment.
52
+ *
53
+ * `--env` has no default, so an omitted flag silently takes the configured environment. The
54
+ * message distinguishes the two cases: being told "build cannot target pro" after typing no
55
+ * environment at all is not actionable on its own.
56
+ *
57
+ * @param {unknown} envKey - Resolved environment key
58
+ * @param {Object} [opts] - Context
59
+ * @param {boolean} [opts.explicit] - Whether the caller passed --env
60
+ * @throws {Error} When the environment is tst or pro
61
+ */
62
+ function assertBuildEnvironmentAllowed(envKey, opts = {}) {
63
+ const key = normalizeEnvKey(envKey);
64
+ if (!isPromotionOnlyEnvironment(key)) {
65
+ return;
66
+ }
67
+ const cause = opts.explicit
68
+ ? `--env ${key}`
69
+ : `the current environment is ${key}`;
70
+ throw new Error(
71
+ `Cannot build into ${key} (${cause}). ${key} receives a promoted image, never a locally ` +
72
+ 'built one, so that what runs there is what was tested in dev.\n' +
73
+ 'Build in dev with: aifabrix build <appKey> --env dev\n' +
74
+ `Then promote into ${key} with: aifabrix env`
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Refuse selecting a promotion-only environment outside the front-door platform mode.
80
+ *
81
+ * @param {unknown} envKey - Environment key being selected
82
+ * @param {unknown} mode - setupPlatformMode value
83
+ * @throws {Error} When the environment needs full mode and the install is not in it
84
+ */
85
+ function assertEnvironmentAllowedForPlatformMode(envKey, mode) {
86
+ const key = normalizeEnvKey(envKey);
87
+ if (!isPromotionOnlyEnvironment(key)) {
88
+ return;
89
+ }
90
+ const normalized = normalizeSetupPlatformMode(mode);
91
+ if (normalized === SETUP_PLATFORM_MODE.FULL) {
92
+ return;
93
+ }
94
+ const reason =
95
+ normalized === SETUP_PLATFORM_MODE.SINGLE_TRAEFIK
96
+ ? 'single-traefik is a front door for one environment only'
97
+ : 'single mode publishes direct ports, and several environments on one host collide on ports';
98
+ throw new Error(
99
+ `Environment ${key} requires platform mode "full" (HTTPS front door with per-environment ` +
100
+ `paths). This installation is "${normalized}": ${reason}.\n` +
101
+ 'Switch with: aifabrix setup --platform full'
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Refuse a promotion-only environment against the installation's configured mode.
107
+ *
108
+ * @param {unknown} envKey - Environment key being selected
109
+ * @returns {Promise<void>}
110
+ * @throws {Error} When the environment needs full mode and the install is not in it
111
+ */
112
+ async function assertEnvironmentAllowedForCurrentMode(envKey) {
113
+ if (!isPromotionOnlyEnvironment(envKey)) {
114
+ return;
115
+ }
116
+ const config = require('./config');
117
+ const mode =
118
+ typeof config.getSetupPlatformMode === 'function'
119
+ ? await config.getSetupPlatformMode()
120
+ : SETUP_PLATFORM_MODE.SINGLE;
121
+ assertEnvironmentAllowedForPlatformMode(envKey, mode);
122
+ }
123
+
124
+ /**
125
+ * The environment a build will actually target.
126
+ *
127
+ * Mirrors how the secret path resolves it (`buildScopedKvContext`): an omitted `--env` falls
128
+ * through to the configured environment, so a guard that only inspected the flag would let
129
+ * `aifabrix build` run against `pro` on a machine configured for it.
130
+ *
131
+ * @param {Object} [options] - Build options carrying `env`
132
+ * @returns {Promise<{ envKey: string, explicit: boolean }>}
133
+ */
134
+ async function resolveEffectiveBuildEnvironment(options = {}) {
135
+ if (options && options.env) {
136
+ return { envKey: normalizeEnvKey(options.env), explicit: true };
137
+ }
138
+ const config = require('./config');
139
+ if (typeof config.getCurrentEnvironment !== 'function') {
140
+ return { envKey: 'dev', explicit: false };
141
+ }
142
+ let current;
143
+ try {
144
+ current = await config.getCurrentEnvironment();
145
+ } catch {
146
+ // An unreadable config is a different failure, reported properly by whatever next needs
147
+ // it — secrets resolution cannot proceed without it either. Turning it into "cannot
148
+ // build" would stop every build on a machine whose config has an unrelated problem, and
149
+ // it protects nothing: reaching tst or pro requires either an explicit --env, which
150
+ // never reads config, or a readable config naming one.
151
+ return { envKey: 'dev', explicit: false };
152
+ }
153
+ return { envKey: normalizeEnvKey(current) || 'dev', explicit: false };
154
+ }
155
+
156
+ /**
157
+ * Resolve the build environment and refuse it when it is promotion-only.
158
+ *
159
+ * @param {Object} [options] - Build options carrying `env`
160
+ * @returns {Promise<string>} The allowed environment key
161
+ * @throws {Error} When the environment is tst or pro
162
+ */
163
+ async function assertBuildEnvironmentForOptions(options = {}) {
164
+ const { envKey, explicit } = await resolveEffectiveBuildEnvironment(options);
165
+ assertBuildEnvironmentAllowed(envKey, { explicit });
166
+ return envKey;
167
+ }
168
+
169
+ module.exports = {
170
+ PROMOTION_ONLY_ENVIRONMENTS,
171
+ isPromotionOnlyEnvironment,
172
+ assertBuildEnvironmentAllowed,
173
+ assertEnvironmentAllowedForPlatformMode,
174
+ assertEnvironmentAllowedForCurrentMode,
175
+ resolveEffectiveBuildEnvironment,
176
+ assertBuildEnvironmentForOptions
177
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Builder-owned env values for locally installed platform applications.
3
+ *
4
+ * `templates/applications/{miso-controller,builder-api,keycloak}/env.template` are
5
+ * byte-identical mirrors of the Miso master, and `dataplane` mirrors the dataplane
6
+ * repository. An edit to a mirror survives until the next `chore(templates): sync`
7
+ * commit and then disappears without a diff anyone reads, so a value Builder decides
8
+ * cannot live there. It is applied here instead, after the template is resolved.
9
+ *
10
+ * @fileoverview Local-install env overrides owned by Builder, not by a mirrored template
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ /** Applications the CLI registers itself through `.../applications/bootstrap`. */
16
+ const CLI_BOOTSTRAPPED_APP_KEYS = Object.freeze(['dataplane']);
17
+
18
+ /**
19
+ * Controller onboarding owns the miso-only platform applications. Dataplane remains a
20
+ * CLI-owned workload and must never be inserted into this list.
21
+ */
22
+ const DEFAULT_LOCAL_ONBOARDING_APPS = Object.freeze(['builder-api']);
23
+
24
+ /** Config key an installation may set to choose its own onboarding list. */
25
+ const ONBOARDING_APPS_CONFIG_KEY = 'onboardingApps';
26
+
27
+ /**
28
+ * Keys Builder's mirror carried that the Miso master does not.
29
+ *
30
+ * They were edits to a mirrored file, which is the thing this module exists to prevent: the
31
+ * mirror has since fallen 57 lines behind its master, and re-syncing it would have dropped
32
+ * them silently. They are declared here instead, so the next sync cannot lose them.
33
+ *
34
+ * - `INFRASTRUCTURE_NAME` is the only one carrying a value, used by controller and onboarding
35
+ * flows. Losing it would change behaviour, not merely remove a blank line.
36
+ * - The other three shipped empty. They are kept so a local install's env has exactly the keys
37
+ * it had before this sync: an absent key and an empty one are usually the same thing, and
38
+ * "usually" is not a basis for changing every installation at once.
39
+ *
40
+ * `ACR_LOGIN_SERVER` is deliberately empty. Miso removed it with the note that its proper home
41
+ * is Builder's run tooling, which knows the per-developer answer — so an installation that
42
+ * needs it should have it injected there rather than shipped one test installation's registry
43
+ * to everybody, which is what happened the last time it carried a value.
44
+ */
45
+ const MISO_CONTROLLER_LOCAL_ONLY_ENV = Object.freeze({
46
+ INFRASTRUCTURE_NAME: '${DEV_USERNAME}.${DEV_ROOT_DOMAIN}',
47
+ KEYCLOAK_PUBLIC_KEY: '',
48
+ OTEL_SERVICE_VERSION: '',
49
+ ACR_LOGIN_SERVER: ''
50
+ });
51
+
52
+ function normalizeAppKeyList(value) {
53
+ const raw = Array.isArray(value) ? value : String(value ?? '').split(',');
54
+ const seen = new Set();
55
+ for (const entry of raw) {
56
+ const key = String(entry ?? '').trim().toLowerCase();
57
+ if (key) {
58
+ seen.add(key);
59
+ }
60
+ }
61
+ return [...seen];
62
+ }
63
+
64
+ /**
65
+ * Applications controller onboarding may register on this installation.
66
+ *
67
+ * Builder API is always retained because Controller onboarding owns that miso-only
68
+ * application. This also migrates older explicit-empty setup values safely.
69
+ *
70
+ * @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
71
+ * @returns {string[]} Application keys, never including one the CLI bootstraps
72
+ */
73
+ function resolveLocalOnboardingApps(cfg) {
74
+ const configured = cfg ? cfg[ONBOARDING_APPS_CONFIG_KEY] : undefined;
75
+ const list = configured === undefined || configured === null
76
+ ? [...DEFAULT_LOCAL_ONBOARDING_APPS]
77
+ : normalizeAppKeyList(configured);
78
+ const allowed = list.filter((key) => !CLI_BOOTSTRAPPED_APP_KEYS.includes(key));
79
+ return ['builder-api', ...allowed.filter((key) => key !== 'builder-api')];
80
+ }
81
+
82
+ /**
83
+ * Env values Builder owns for a locally installed application.
84
+ *
85
+ * @param {string} appName - Application key
86
+ * @param {Object} [cfg] - Parsed `~/.aifabrix/config.yaml`
87
+ * @returns {Object.<string, string>} Map to merge into resolved env content; empty when none apply
88
+ */
89
+ function localEnvOverridesForApp(appName, cfg) {
90
+ const key = String(appName ?? '').trim().toLowerCase();
91
+ if (key === 'miso-controller') {
92
+ return {
93
+ ONBOARDING_APPS: resolveLocalOnboardingApps(cfg).join(','),
94
+ ...MISO_CONTROLLER_LOCAL_ONLY_ENV
95
+ };
96
+ }
97
+ if (key === 'builder-api' || key === 'integration-server') {
98
+ return { MISO_ENVIRONMENT: 'miso' };
99
+ }
100
+ return {};
101
+ }
102
+
103
+ /**
104
+ * Merge Builder-owned values into resolved env content for a locally installed application.
105
+ *
106
+ * Applied after the template is resolved, so the installation's choice is the authority for
107
+ * these keys. The merge function is passed in rather than imported, because the only caller
108
+ * owns it and importing it here would close a require cycle.
109
+ *
110
+ * @param {string} content - Resolved env content
111
+ * @param {string} appName - Application key
112
+ * @param {(content: string, map: Object) => string} mergeEnvMapIntoContent - Merge helper
113
+ * @returns {Promise<string>} Content with overrides merged, unchanged when none apply
114
+ */
115
+ async function applyLocalEnvOverrides(content, appName, mergeEnvMapIntoContent) {
116
+ let cfg = null;
117
+ try {
118
+ cfg = await require('./config').getConfig();
119
+ } catch {
120
+ // No readable config (fresh machine, or resolve outside an installation): use defaults.
121
+ }
122
+ const overrides = localEnvOverridesForApp(appName, cfg);
123
+ if (!overrides || Object.keys(overrides).length === 0) {
124
+ return content;
125
+ }
126
+ return mergeEnvMapIntoContent(content, overrides);
127
+ }
128
+
129
+ /**
130
+ * The onboarding list for an installation that does or does not carry the integration
131
+ * server. Written to config so the choice survives, rather than being asked on every run.
132
+ *
133
+ * @param {boolean} integrationServer - Whether the installation carries it
134
+ * @returns {string[]} Value for the `onboardingApps` config key
135
+ */
136
+ function onboardingAppsForIntegrationServer(integrationServer) {
137
+ return integrationServer ? ['builder-api', 'integration-server'] : ['builder-api'];
138
+ }
139
+
140
+ module.exports = {
141
+ CLI_BOOTSTRAPPED_APP_KEYS,
142
+ DEFAULT_LOCAL_ONBOARDING_APPS,
143
+ ONBOARDING_APPS_CONFIG_KEY,
144
+ MISO_CONTROLLER_LOCAL_ONLY_ENV,
145
+ resolveLocalOnboardingApps,
146
+ localEnvOverridesForApp,
147
+ applyLocalEnvOverrides,
148
+ onboardingAppsForIntegrationServer
149
+ };
@@ -29,6 +29,7 @@ const {
29
29
  saveSecretsFile
30
30
  } = require('../utils/secrets-generator');
31
31
  const { encryptSecret } = require('../utils/secrets-encryption');
32
+ const { generateRsaSecretValues } = require('../parameters/rsa-secret-values');
32
33
  const { loadEnvTemplate } = require('../utils/secrets-helpers');
33
34
  const {
34
35
  buildInfraPlaceholderContext,
@@ -287,12 +288,13 @@ async function ensureSecretsForKeys(keys, options = {}) {
287
288
  if (toAdd.length === 0) return [];
288
289
 
289
290
  const encryptionKey = await config.getSecretsEncryptionKey();
291
+ const rsaValues = emptyForCredentials ? {} : generateRsaSecretValues(toAdd, { ...existing, ...suggested }, encryptionKey);
290
292
  const added = [];
291
293
 
292
294
  return addSecretsToFile({
293
295
  filePath: target.filePath,
294
- toAdd,
295
- suggested,
296
+ toAdd: [...new Set([...toAdd, ...Object.keys(rsaValues)])],
297
+ suggested: { ...rsaValues, ...suggested },
296
298
  emptyForCredentials,
297
299
  encryptionKey,
298
300
  added,
@@ -12,6 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
14
14
  const config = require('./config');
15
+ const { applyLocalEnvOverrides } = require('./local-env-overrides');
15
16
  const {
16
17
  interpolateEnvVars,
17
18
  collectMissingSecrets,
@@ -40,8 +41,9 @@ const secretsEnsure = require('./secrets-ensure');
40
41
  const { resolveSecretsPath, getActualSecretsPath } = require('../utils/secrets-path');
41
42
  const pathsUtil = require('../utils/paths');
42
43
  const { readAppEnvironmentScopedFlagForAppPath } = require('../utils/app-scoped-config');
43
- const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv } = require('../utils/environment-scoped-resources');
44
- const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
44
+ const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
45
+ const { isStrictScopedEnvironment } = require('../utils/secrets-kv-scope');
46
+ const { applyScopedRedisDbIndex } = require('../utils/redis-env-scope');
45
47
  const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
46
48
  const {
47
49
  mergeAdditionalPublishedPorts,
@@ -106,7 +108,8 @@ async function resolveKvReferences(
106
108
  secretsFilePaths,
107
109
  appName,
108
110
  envTemplatePath,
109
- envTemplate
111
+ envTemplate,
112
+ scopedKv
110
113
  })
111
114
  );
112
115
  }
@@ -237,12 +240,14 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
237
240
  const variablesPath = (options && options.envOnly) ? null : resolveApplicationConfigPath(appPath);
238
241
  const template = loadEnvTemplate(templatePath);
239
242
  const secretsPaths = await getActualSecretsPath(secretsPath, appName);
240
- if (force) {
243
+ const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
244
+ // --force backfills a missing secret by generating one. A strict environment must not be
245
+ // handed a value the Controller has never seen, so it errors on the missing key instead.
246
+ if (force && !isStrictScopedEnvironment(runEnvKey)) {
241
247
  const preferredPath = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
242
248
  await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, { preferredFilePath: preferredPath });
243
249
  }
244
250
  const secrets = await loadSecrets(secretsPath, appName);
245
- const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
246
251
  let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
247
252
  envKey: runEnvKey,
248
253
  effective,
@@ -262,10 +267,8 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
262
267
  Boolean(options.envOnly)
263
268
  );
264
269
  resolved = await applyEnvironmentTransformations(resolved, environment, variablesPath);
265
- if (effective) {
266
- const idx = redisDbIndexForScopedRunEnv(runEnvKey);
267
- resolved = applyRedisDbIndexToEnvContent(resolved, idx);
268
- }
270
+ resolved = applyScopedRedisDbIndex(resolved, runEnvKey, effective);
271
+ resolved = await applyLocalEnvOverrides(resolved, appName, mergeEnvMapIntoContent);
269
272
 
270
273
  return resolved;
271
274
  }
@@ -470,7 +473,11 @@ async function generateEnvFile(appName, secretsPath, environment = 'local', forc
470
473
  }
471
474
 
472
475
  // Always resolve so missing-secret / kv:// errors still surface in noWrite mode.
473
- const resolved = await generateEnvContent(appName, secretsPath, environment, force, { appPath, envOnly });
476
+ const resolved = await generateEnvContent(appName, secretsPath, environment, force, {
477
+ appPath,
478
+ envOnly,
479
+ runEnvKey: opts.runEnvKey
480
+ });
474
481
 
475
482
  if (noWrite) {
476
483
  return null;
@@ -157,8 +157,11 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
157
157
  const environment = options.environment || 'docker';
158
158
  const secretsPath = options.secretsPath || null;
159
159
  const force = options.force === true;
160
+ const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
160
161
 
161
- let resolved = await secrets.generateEnvContent(appName, secretsPath, environment, force);
162
+ let resolved = genOpts
163
+ ? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
164
+ : await secrets.generateEnvContent(appName, secretsPath, environment, force);
162
165
  resolved = await injectRegistryTokens(resolved, secretsPath, appName);
163
166
  resolved = await injectBashPrefixedExportLines(resolved, secretsPath, appName);
164
167
 
@@ -185,7 +188,8 @@ async function resolveAndWriteEnvFile(appName, options = {}) {
185
188
  * @async
186
189
  * @function resolveAndGetEnvMap
187
190
  * @param {string} appName - Application name
188
- * @param {Object} [options] - Options (same as resolveAndWriteEnvFile)
191
+ * @param {Object} [options] - Options (same as resolveAndWriteEnvFile); `runEnvKey` selects the
192
+ * manifest environment for {NAME} resolution
189
193
  * @returns {Promise<Object.<string, string>>} Map of variable name to value
190
194
  */
191
195
  async function resolveAndGetEnvMap(appName, options = {}) {
@@ -193,7 +197,10 @@ async function resolveAndGetEnvMap(appName, options = {}) {
193
197
  const environment = options.environment || 'docker';
194
198
  const secretsPath = options.secretsPath || null;
195
199
  const force = options.force === true;
196
- let content = await secrets.generateEnvContent(appName, secretsPath, environment, force);
200
+ const genOpts = options.runEnvKey ? { runEnvKey: String(options.runEnvKey) } : undefined;
201
+ let content = genOpts
202
+ ? await secrets.generateEnvContent(appName, secretsPath, environment, force, genOpts)
203
+ : await secrets.generateEnvContent(appName, secretsPath, environment, force);
197
204
  content = await injectRegistryTokens(content, secretsPath, appName);
198
205
  content = await injectBashPrefixedExportLines(content, secretsPath, appName);
199
206
  const map = parseEnvContentToMap(content);
@@ -201,7 +208,7 @@ async function resolveAndGetEnvMap(appName, options = {}) {
201
208
  const adminSecrets = require('./admin-secrets');
202
209
  const { applyAdminSecretsPlatformOverlay } = require('./admin-secrets-env-overlay');
203
210
  const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
204
- applyAdminSecretsPlatformOverlay(map, adminObj);
211
+ applyAdminSecretsPlatformOverlay(map, adminObj, appName);
205
212
  } catch {
206
213
  // admin-secrets.env optional until up-infra/setup
207
214
  }
@@ -167,9 +167,11 @@ async function mergeFromConfiguredSecretsPath(configSecretsPath, userSecrets, he
167
167
  }
168
168
 
169
169
  async function loadMergedConfigAndUserSecrets() {
170
- const userSecrets = loadPrimaryUserSecrets();
171
- const helpers = createMergeHelpers(userSecrets);
172
170
  const userPath = pathsUtil.getPrimaryUserSecretsLocalPath();
171
+ const userSecrets = await decryptSecretsObject(loadPrimaryUserSecrets(), {
172
+ defaultSourceLabel: userPath
173
+ });
174
+ const helpers = createMergeHelpers(userSecrets);
173
175
  /** @type {Record<string, string>} */
174
176
  const keySources = {};
175
177
  for (const k of Object.keys(userSecrets || {})) {
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Offline semantic checks for the binary/documents pairing (dataplane schema 2.9.13).
3
+ *
4
+ * The schema alone cannot express these: `output.mode: documents` and
5
+ * `fetch.responseType: binary` are individually valid anywhere, but only mean something
6
+ * when paired with a documentStorage entity and a binary-capable fetch source. The
7
+ * dataplane rejects the bad combinations at upload; these checks give the same answer
8
+ * offline, under the same error codes, so an author is not told "valid" by the CLI and
9
+ * "invalid" by the server.
10
+ *
11
+ * Codes mirror the dataplane validator and must not be renamed here.
12
+ *
13
+ * @fileoverview Semantic validation for binary responses and document output
14
+ * @author AI Fabrix Team
15
+ * @version 1.0.0
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ /** Sources whose response body the runtime can read as bytes. */
21
+ const BINARY_FETCH_SOURCES = new Set(['openapi', 'http']);
22
+
23
+ /** documentStorage.binaryOperationRef default, per type/document-storage.json. */
24
+ const DEFAULT_BINARY_OPERATION = 'get';
25
+
26
+ const CODES = Object.freeze({
27
+ entityType: 'cip_documents_output_entity_type',
28
+ storageDisabled: 'cip_documents_output_storage_disabled',
29
+ binarySource: 'cip_fetch_binary_source_invalid',
30
+ withoutBinaryFetch: 'cip_documents_output_without_binary_fetch'
31
+ });
32
+
33
+ /**
34
+ * @param {Object} datasource - Datasource definition
35
+ * @returns {Array<[string, Object]>} CIP operation entries, empty when not a CIP datasource
36
+ */
37
+ function cipOperations(datasource) {
38
+ const cip = datasource?.execution?.engine === 'cip' ? datasource.execution.cip : null;
39
+ const operations = cip && cip.operations;
40
+ if (!operations || typeof operations !== 'object') {
41
+ return [];
42
+ }
43
+ return Object.entries(operations).filter(([, operation]) => operation && typeof operation === 'object');
44
+ }
45
+
46
+ /**
47
+ * @param {Object} operation - CIP operation
48
+ * @returns {Array<Object>} Its steps
49
+ */
50
+ function stepsOf(operation) {
51
+ return Array.isArray(operation.steps) ? operation.steps : [];
52
+ }
53
+
54
+ /**
55
+ * A step declares a binary body. The schema also promotes a binary Content-Type when
56
+ * responseType is omitted, but that is a runtime decision we cannot make offline, so only
57
+ * the explicit declaration counts here.
58
+ * @param {Object} step - CIP step
59
+ * @returns {boolean} True when the step fetches a binary body
60
+ */
61
+ function isBinaryFetch(step) {
62
+ return Boolean(step && step.fetch && String(step.fetch.responseType || '').trim().toLowerCase() === 'binary');
63
+ }
64
+
65
+ /**
66
+ * @param {Object} step - CIP step
67
+ * @returns {boolean} True when the step finalizes as documents
68
+ */
69
+ function isDocumentsOutput(step) {
70
+ return Boolean(step && step.output && step.output.mode === 'documents');
71
+ }
72
+
73
+ /**
74
+ * `binary` is a body contract, so it only applies where the runtime holds the response
75
+ * bytes. A datasource, viewpoint or record source produces rows, never a file body.
76
+ * @param {string} operationName - Operation key
77
+ * @param {Array<Object>} steps - Operation steps
78
+ * @param {Array<Object>} errors - Collected errors
79
+ * @returns {void}
80
+ */
81
+ function checkBinaryFetchSources(operationName, steps, errors) {
82
+ steps.forEach((step, index) => {
83
+ if (!isBinaryFetch(step)) {
84
+ return;
85
+ }
86
+ const source = String(step.fetch.source || 'openapi').trim().toLowerCase();
87
+ if (BINARY_FETCH_SOURCES.has(source)) {
88
+ return;
89
+ }
90
+ errors.push({
91
+ code: CODES.binarySource,
92
+ message:
93
+ `operations.${operationName}.steps[${index}].fetch.responseType is "binary" but ` +
94
+ `fetch.source is "${source || 'unset'}"; binary bodies come from openapi or http sources only`
95
+ });
96
+ });
97
+ }
98
+
99
+ /**
100
+ * @param {Object} datasource - Datasource definition
101
+ * @param {string} operationName - Operation key that emits documents
102
+ * @param {number} stepIndex - Index of the output step
103
+ * @param {Array<Object>} errors - Collected errors
104
+ * @returns {void}
105
+ */
106
+ function checkDocumentsEntity(datasource, operationName, stepIndex, errors) {
107
+ const where = `operations.${operationName}.steps[${stepIndex}].output.mode is "documents"`;
108
+ if (!['documentstorage', 'document-storage'].includes(String(datasource.entityType || '').trim().toLowerCase().replace(/_/g, '-'))) {
109
+ errors.push({
110
+ code: CODES.entityType,
111
+ message: `${where} but entityType is "${datasource.entityType || 'unset'}"; documents output requires entityType documentStorage`
112
+ });
113
+ return;
114
+ }
115
+ const storage = datasource.documentStorage;
116
+ if (!storage || storage.enabled === false) {
117
+ errors.push({
118
+ code: CODES.storageDisabled,
119
+ message: `${where} but documentStorage is ${storage ? 'not enabled' : 'missing'}; set documentStorage.enabled true`
120
+ });
121
+ }
122
+ }
123
+
124
+ /**
125
+ * The operation named by binaryOperationRef must actually retrieve bytes, otherwise the
126
+ * document pipeline has nothing to store. A missing reference and a reference to a
127
+ * JSON-only operation are the same defect from the author's point of view.
128
+ * @param {Object} datasource - Datasource definition
129
+ * @param {string} operationName - Operation key that emits documents
130
+ * @param {Array<Object>} warnings - Collected warnings
131
+ * @returns {void}
132
+ */
133
+ function checkBinaryOperationRef(datasource, operationName, warnings) {
134
+ const storage = datasource.documentStorage || {};
135
+ const ref = String(storage.binaryOperationRef || DEFAULT_BINARY_OPERATION);
136
+ const operations = new Map(cipOperations(datasource));
137
+ const referenced = operations.get(ref);
138
+ if (ref === operationName && referenced && stepsOf(referenced).some(isBinaryFetch)) {
139
+ return;
140
+ }
141
+ const reason = ref !== operationName && referenced
142
+ ? `binaryOperationRef "${ref}" does not name this operation "${operationName}"`
143
+ : referenced
144
+ ? `operation "${ref}" has no step with fetch.responseType "binary"`
145
+ : `operation "${ref}" does not exist in execution.cip.operations`;
146
+ warnings.push({
147
+ code: CODES.withoutBinaryFetch,
148
+ message:
149
+ `operations.${operationName} emits documents but ${reason}; ` +
150
+ 'set documentStorage.binaryOperationRef to the operation that fetches the file'
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Semantic checks for the binary/documents pairing.
156
+ * @param {Object} datasource - Datasource definition
157
+ * @returns {{ errors: Array<Object>, warnings: Array<Object> }} Findings with dataplane codes
158
+ */
159
+ function validateBinaryDocumentsContract(datasource) {
160
+ const errors = [];
161
+ const warnings = [];
162
+ if (!datasource || typeof datasource !== 'object') {
163
+ return { errors, warnings };
164
+ }
165
+ for (const [operationName, operation] of cipOperations(datasource)) {
166
+ if (operation.enabled === false) continue;
167
+ const steps = stepsOf(operation);
168
+ checkBinaryFetchSources(operationName, steps, errors);
169
+ steps.forEach((step, index) => {
170
+ if (!isDocumentsOutput(step)) {
171
+ return;
172
+ }
173
+ checkDocumentsEntity(datasource, operationName, index, errors);
174
+ const preceding = steps.slice(0, index + 1).some(candidate =>
175
+ isBinaryFetch(candidate) && BINARY_FETCH_SOURCES.has(String(candidate.fetch.source || 'openapi').trim().toLowerCase()));
176
+ if (!preceding) {
177
+ warnings.push({ code: CODES.withoutBinaryFetch,
178
+ message: `operations.${operationName} has no step with fetch.responseType "binary" preceding documents output` });
179
+ } else {
180
+ checkBinaryOperationRef(datasource, operationName, warnings);
181
+ }
182
+ });
183
+ }
184
+ return { errors, warnings };
185
+ }
186
+
187
+ module.exports = {
188
+ CODES,
189
+ validateBinaryDocumentsContract
190
+ };
@@ -9,6 +9,7 @@
9
9
  'use strict';
10
10
 
11
11
  const FORBIDDEN_ROOT_KEYS = new Set([
12
+ '_comManifestRoleCatalog',
12
13
  'context',
13
14
  'origin',
14
15
  'generatedBy',