@aifabrix/builder 2.50.0 → 2.51.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 (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Offline validate then write *-deploy.json (same quality as validate + same write as json).
3
+ *
4
+ * @fileoverview aifabrix validate-json command
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const validate = require('../validation/validate');
15
+ const generator = require('../generator');
16
+ const { listIntegrationAppNames, listBuilderAppNames } = require('../utils/paths');
17
+ const {
18
+ formatSuccessLine,
19
+ formatBlockingError,
20
+ formatWarningLine
21
+ } = require('../utils/cli-test-layout-chalk');
22
+
23
+ /**
24
+ * @param {string} appKey
25
+ * @returns {boolean}
26
+ */
27
+ function looksLikeExistingFilePath(appKey) {
28
+ try {
29
+ return Boolean(appKey && fs.existsSync(appKey) && fs.statSync(appKey).isFile());
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @param {object} opts
37
+ * @returns {string[]}
38
+ */
39
+ function collectBatchAppNames(opts) {
40
+ const names = [];
41
+ if (opts.integration === true) {
42
+ names.push(...listIntegrationAppNames());
43
+ }
44
+ if (opts.builder === true) {
45
+ names.push(...listBuilderAppNames());
46
+ }
47
+ return [...new Set(names)];
48
+ }
49
+
50
+ /**
51
+ * @param {string} appKey
52
+ * @param {object} result
53
+ * @param {string} outFormat
54
+ */
55
+ function emitManifestLine(appKey, result, outFormat) {
56
+ if (outFormat === 'json' || !result?.appPath) {
57
+ return;
58
+ }
59
+ try {
60
+ const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
61
+ emitManifestMetadataLineIfTTY(logger, {
62
+ appKey,
63
+ appPath: result.appPath,
64
+ envOnly: false,
65
+ json: false
66
+ });
67
+ } catch {
68
+ /* ignore */
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate one app/systemKey; write deploy JSON only when local validate succeeds.
74
+ * @param {string} appKey
75
+ * @param {object} [options]
76
+ * @returns {Promise<{ appKey: string, valid: boolean, validation: object, deploy?: object|null, error?: string }>}
77
+ */
78
+ async function validateThenWriteDeploy(appKey, options = {}) {
79
+ let validation;
80
+ try {
81
+ validation = await validate.validateAppOrFile(appKey, { ...options, certSync: false });
82
+ } catch (err) {
83
+ return {
84
+ appKey,
85
+ valid: false,
86
+ validation: { valid: false, errors: [err.message || String(err)] },
87
+ deploy: null,
88
+ error: err.message || String(err)
89
+ };
90
+ }
91
+ if (!validation.valid) {
92
+ return { appKey, valid: false, validation, deploy: null };
93
+ }
94
+ const deploy = await generator.generateDeployJsonWithValidation(appKey, options);
95
+ const ok = deploy.success === true;
96
+ return {
97
+ appKey,
98
+ valid: ok,
99
+ validation,
100
+ deploy,
101
+ error: ok ? undefined : (deploy.validation?.errors || []).join('; ') || 'deploy JSON validation failed'
102
+ };
103
+ }
104
+
105
+ /**
106
+ * @param {object} item
107
+ * @param {string} outFormat
108
+ */
109
+ function displayOneHumanResult(item, outFormat) {
110
+ emitManifestLine(item.appKey, item.validation, outFormat);
111
+ if (outFormat === 'json') {
112
+ return;
113
+ }
114
+ validate.displayValidationResults(item.validation);
115
+ if (!item.validation.valid) {
116
+ logger.log(formatBlockingError(`Skipped deploy JSON for ${item.appKey} (validation failed)`));
117
+ return;
118
+ }
119
+ if (item.deploy?.success && item.deploy.path) {
120
+ logger.log(formatSuccessLine(`Generated deployment JSON: ${item.deploy.path}`));
121
+ const warnings = item.deploy.validation?.warnings || [];
122
+ if (warnings.length > 0) {
123
+ warnings.forEach((w) => logger.log(formatWarningLine(w)));
124
+ }
125
+ return;
126
+ }
127
+ logger.log(formatBlockingError(`Deploy JSON write failed for ${item.appKey}`));
128
+ (item.deploy?.validation?.errors || []).forEach((e) => logger.log(` • ${e}`));
129
+ }
130
+
131
+ /**
132
+ * @param {Array<object>} items
133
+ * @param {string} outFormat
134
+ */
135
+ function displayBatchSummary(items, outFormat) {
136
+ if (outFormat === 'json') {
137
+ logger.log(JSON.stringify({
138
+ batch: true,
139
+ valid: items.every((i) => i.valid),
140
+ results: items.map((i) => ({
141
+ appKey: i.appKey,
142
+ valid: i.valid,
143
+ path: i.deploy?.path || null,
144
+ error: i.error || null
145
+ }))
146
+ }, null, 2));
147
+ return;
148
+ }
149
+ const passed = items.filter((i) => i.valid);
150
+ const failed = items.filter((i) => !i.valid);
151
+ logger.log('');
152
+ logger.log(chalk.bold('validate-json summary'));
153
+ logger.log(` Passed: ${passed.length}`);
154
+ logger.log(` Failed: ${failed.length}`);
155
+ failed.forEach((i) => {
156
+ logger.log(formatBlockingError(`${i.appKey}${i.error ? `: ${i.error}` : ''}`));
157
+ });
158
+ }
159
+
160
+ /**
161
+ * @param {object} opts
162
+ * @param {string} outFormat
163
+ * @returns {Promise<{ exitCode: number }>}
164
+ */
165
+ async function runValidateJsonBatch(opts, outFormat) {
166
+ const names = collectBatchAppNames(opts);
167
+ const items = [];
168
+ for (const name of names) {
169
+ const item = await validateThenWriteDeploy(name, opts);
170
+ displayOneHumanResult(item, outFormat);
171
+ items.push(item);
172
+ }
173
+ displayBatchSummary(items, outFormat);
174
+ return { exitCode: items.every((i) => i.valid) ? 0 : 1 };
175
+ }
176
+
177
+ /**
178
+ * @param {string} appKey
179
+ * @param {object} opts
180
+ * @param {string} outFormat
181
+ * @returns {Promise<{ exitCode: number }>}
182
+ */
183
+ async function runValidateJsonSingle(appKey, opts, outFormat) {
184
+ if (looksLikeExistingFilePath(appKey)) {
185
+ logger.log(
186
+ chalk.red(
187
+ 'validate-json requires an appKey or systemKey (not a file path). Use aifabrix validate <file> for files.'
188
+ )
189
+ );
190
+ return { exitCode: 1 };
191
+ }
192
+ const item = await validateThenWriteDeploy(appKey, opts);
193
+ if (outFormat === 'json') {
194
+ logger.log(JSON.stringify({
195
+ appKey: item.appKey,
196
+ valid: item.valid,
197
+ path: item.deploy?.path || null,
198
+ validation: item.validation,
199
+ error: item.error || null
200
+ }, null, 2));
201
+ } else {
202
+ displayOneHumanResult(item, outFormat);
203
+ }
204
+ return { exitCode: item.valid ? 0 : 1 };
205
+ }
206
+
207
+ /**
208
+ * @param {string|undefined} appKey
209
+ * @param {object} options - Commander options or opts()
210
+ * @returns {Promise<{ exitCode: number }>}
211
+ */
212
+ async function runValidateJsonCommand(appKey, options) {
213
+ const opts = options.opts ? options.opts() : options;
214
+ const outFormat = (opts.format || 'default').toLowerCase();
215
+ if (opts.integration === true || opts.builder === true) {
216
+ return runValidateJsonBatch(opts, outFormat);
217
+ }
218
+ if (!appKey || typeof appKey !== 'string') {
219
+ logger.log(chalk.red('App or system key is required, or use --integration / --builder'));
220
+ return { exitCode: 1 };
221
+ }
222
+ return runValidateJsonSingle(appKey, opts, outFormat);
223
+ }
224
+
225
+ module.exports = {
226
+ runValidateJsonCommand,
227
+ validateThenWriteDeploy,
228
+ collectBatchAppNames,
229
+ looksLikeExistingFilePath
230
+ };
@@ -8,58 +8,96 @@ const { validateExternalSystemComplete } = require('../validation/validate');
8
8
  const { testExternalSystem, testExternalSystemIntegration } = require('../external-system/test');
9
9
  const { runTestE2EForExternalSystem } = require('./test-e2e-external');
10
10
 
11
+ /**
12
+ * @param {string} systemKey
13
+ * @returns {Array}
14
+ */
15
+ function buildBeforeSyncSteps(systemKey) {
16
+ return [
17
+ {
18
+ key: 'validation',
19
+ run: () => validateExternalSystemComplete(systemKey, { type: 'external' }),
20
+ ok: (result) => result.valid === true
21
+ },
22
+ {
23
+ key: 'unit',
24
+ run: () => testExternalSystem(systemKey, { verbose: false }),
25
+ ok: (result) => result.valid === true
26
+ }
27
+ ];
28
+ }
29
+
11
30
  /**
12
31
  * @param {string} systemKey
13
32
  * @param {Object} options
14
- * @returns {{ beforeSync: Array, afterSync: Array, e2e: Object }}
33
+ * @returns {Array}
15
34
  */
16
- function buildOperationsVerifyStepPlan(systemKey, options) {
17
- return {
18
- beforeSync: [
19
- {
20
- key: 'validation',
21
- run: () => validateExternalSystemComplete(systemKey, { type: 'external' }),
22
- ok: (result) => result.valid === true
23
- },
24
- {
25
- key: 'unit',
26
- run: () => testExternalSystem(systemKey, { verbose: false }),
27
- ok: (result) => result.valid === true
28
- }
29
- ],
30
- afterSync: [
31
- {
32
- key: 'integration',
33
- run: () =>
34
- testExternalSystemIntegration(systemKey, {
35
- environment: options.env,
36
- noPublish: true,
37
- verbose: false,
38
- debug: options.debug === true
39
- }),
40
- ok: (result) => result.success === true
41
- }
42
- ],
43
- e2e: {
44
- key: 'e2e',
45
- run: (progress) =>
46
- runTestE2EForExternalSystem(systemKey, {
47
- env: options.env,
35
+ function buildAfterSyncSteps(systemKey, options) {
36
+ return [
37
+ {
38
+ key: 'integration',
39
+ run: () =>
40
+ testExternalSystemIntegration(systemKey, {
41
+ environment: options.env || options.environment,
48
42
  noPublish: true,
49
43
  verbose: false,
50
44
  debug: options.debug === true,
51
- async: options.async !== false,
45
+ // Token-forward (builder-api certify): same auth as E2E step — do not
46
+ // fall back to getDeploymentAuth inside the container.
52
47
  authConfig: options.authConfig,
53
48
  dataplaneUrl: options.dataplaneUrl,
54
- silentResolve: options.silentResolve === true,
55
- quietPollUi: true,
56
- onDatasourceProgress: progress?.setLabel
57
- ? ({ key, index, total }) =>
58
- progress.setLabel(`Running E2E: ${key} (${index + 1}/${total})`)
59
- : undefined
49
+ silentResolve: options.silentResolve === true
60
50
  }),
61
51
  ok: (result) => result.success === true
62
52
  }
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * @param {string} systemKey
58
+ * @param {Object} options
59
+ * @returns {Object}
60
+ */
61
+ function buildE2eStep(systemKey, options) {
62
+ if (options.skipE2e === true || options.minimal === true) {
63
+ return {
64
+ key: 'e2e',
65
+ run: async() => ({ success: true, skipped: true, reason: 'minimal' }),
66
+ ok: () => true
67
+ };
68
+ }
69
+ return {
70
+ key: 'e2e',
71
+ run: (progress) =>
72
+ runTestE2EForExternalSystem(systemKey, {
73
+ env: options.env,
74
+ noPublish: true,
75
+ verbose: false,
76
+ debug: options.debug === true,
77
+ async: options.async !== false,
78
+ authConfig: options.authConfig,
79
+ dataplaneUrl: options.dataplaneUrl,
80
+ silentResolve: options.silentResolve === true,
81
+ quietPollUi: true,
82
+ onDatasourceProgress: progress?.setLabel
83
+ ? ({ key, index, total }) =>
84
+ progress.setLabel(`Running E2E: ${key} (${index + 1}/${total})`)
85
+ : undefined
86
+ }),
87
+ ok: (result) => result.success === true
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {string} systemKey
93
+ * @param {Object} options
94
+ * @returns {{ beforeSync: Array, afterSync: Array, e2e: Object }}
95
+ */
96
+ function buildOperationsVerifyStepPlan(systemKey, options) {
97
+ return {
98
+ beforeSync: buildBeforeSyncSteps(systemKey),
99
+ afterSync: buildAfterSyncSteps(systemKey, options),
100
+ e2e: buildE2eStep(systemKey, options)
63
101
  };
64
102
  }
65
103
 
@@ -238,7 +238,7 @@ async function logApiCall({ url, options, statusCode, duration, success, errorIn
238
238
  const path = extractPathFromUrl(url);
239
239
 
240
240
  // Extract controller URL from full URL
241
- let controllerUrl = 'unknown';
241
+ let controllerUrl;
242
242
  try {
243
243
  const urlObj = new URL(url);
244
244
  controllerUrl = `${urlObj.protocol}//${urlObj.host}`;
@@ -133,9 +133,7 @@ function mergeUserWithConfigFile(userSecrets, resolvedConfigPath, keySources) {
133
133
  for (const key of Object.keys(configSecrets)) {
134
134
  if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
135
135
  merged[key] = configSecrets[key];
136
- if (keySources) {
137
- keySources[key] = resolvedConfigPath;
138
- }
136
+ keySources[key] = resolvedConfigPath;
139
137
  }
140
138
  }
141
139
  return merged;
@@ -23,7 +23,7 @@ function _asObject(x) {
23
23
  function _uniqStrings(list) {
24
24
  const out = [];
25
25
  const seen = new Set();
26
- for (const x of list || []) {
26
+ for (const x of list) {
27
27
  const s = String(x || '').trim();
28
28
  if (!s || seen.has(s)) continue;
29
29
  seen.add(s);
@@ -156,10 +156,10 @@ function validatePortalInput(portalInput, variableName) {
156
156
  * @throws {Error} If portalInput structure is invalid
157
157
  */
158
158
  /**
159
- * Creates a map of portalInput configurations from variables config
159
+ * Creates a map of portal overlay metadata from variables config
160
160
  * @function createPortalInputMap
161
161
  * @param {Object|null} variablesConfig - Configuration from application.yaml
162
- * @returns {Map} Map of variable names to portalInput configurations
162
+ * @returns {Map} Map of variable names to { portalInput, category? }
163
163
  */
164
164
  function createPortalInputMap(variablesConfig) {
165
165
  const portalInputMap = new Map();
@@ -168,7 +168,11 @@ function createPortalInputMap(variablesConfig) {
168
168
  if (configItem.name && configItem.portalInput) {
169
169
  // Validate portalInput before adding to map
170
170
  validatePortalInput(configItem.portalInput, configItem.name);
171
- portalInputMap.set(configItem.name, configItem.portalInput);
171
+ const overlay = { portalInput: configItem.portalInput };
172
+ if (typeof configItem.category === 'string' && configItem.category.trim().length > 0) {
173
+ overlay.category = configItem.category.trim();
174
+ }
175
+ portalInputMap.set(configItem.name, overlay);
172
176
  }
173
177
  }
174
178
  }
@@ -291,9 +295,13 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
291
295
  required
292
296
  };
293
297
 
294
- // Merge portalInput if it exists in application config
298
+ // Merge portalInput / category if present in application config
295
299
  if (portalInputMap.has(key)) {
296
- configItem.portalInput = portalInputMap.get(key);
300
+ const overlay = portalInputMap.get(key);
301
+ configItem.portalInput = overlay.portalInput;
302
+ if (typeof overlay.category === 'string' && overlay.category.length > 0) {
303
+ configItem.category = overlay.category;
304
+ }
297
305
  }
298
306
 
299
307
  return configItem;
@@ -66,15 +66,21 @@ function extractOptionalSections(deployment) {
66
66
  }
67
67
 
68
68
  /**
69
- * Portal-only configuration for application.yaml (name + portalInput per entry).
69
+ * Portal-only configuration for application.yaml (name + optional category + portalInput).
70
70
  * @param {Array} configuration - Configuration array from deployment JSON
71
- * @returns {Array<{ name: string, portalInput: Object }>}
71
+ * @returns {Array<{ name: string, category?: string, portalInput: Object }>}
72
72
  */
73
73
  function extractPortalInputConfiguration(configuration) {
74
74
  if (!Array.isArray(configuration) || configuration.length === 0) return [];
75
75
  return configuration
76
76
  .filter(item => item && item.portalInput && typeof item.name === 'string')
77
- .map(item => ({ name: item.name, portalInput: item.portalInput }));
77
+ .map(item => {
78
+ const entry = { name: item.name, portalInput: item.portalInput };
79
+ if (typeof item.category === 'string' && item.category.trim().length > 0) {
80
+ entry.category = item.category.trim();
81
+ }
82
+ return entry;
83
+ });
78
84
  }
79
85
 
80
86
  /**
@@ -53,6 +53,12 @@ function formatDockerInfrastructureFailure(detail) {
53
53
  */
54
54
  async function checkDockerAvailability() {
55
55
  await ensureDevCertsIfNeededForRemoteDocker();
56
+ try {
57
+ const { ensureDevCertFreshIfDue } = require('../commands/dev-refresh');
58
+ await ensureDevCertFreshIfDue({ quiet: true });
59
+ } catch {
60
+ // Best-effort renew for remote Docker TLS; docker check reports real failures
61
+ }
56
62
  try {
57
63
  await dockerUtils.ensureDockerAndCompose();
58
64
  } catch (error) {
@@ -181,7 +181,7 @@ function generatePgAdminConfig(infraDir, postgresPassword) {
181
181
  * @returns {string} Escaped string
182
182
  */
183
183
  function escapePgString(s) {
184
- if (s === null || s === undefined || typeof s !== 'string') return '';
184
+ if (typeof s !== 'string') return '';
185
185
  return s.replace(/'/g, '\'\'');
186
186
  }
187
187
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  /**
12
12
  * @param {string} bearerToken
13
- * @returns {{ type: 'bearer', token: string }}
13
+ * @returns {{ type: 'bearer', token: string, preserveBearer: true }}
14
14
  */
15
15
  function buildBearerAuthConfig(bearerToken) {
16
16
  if (!bearerToken || typeof bearerToken !== 'string') {
@@ -20,7 +20,8 @@ function buildBearerAuthConfig(bearerToken) {
20
20
  if (!token) {
21
21
  throw new Error('Bearer token is required');
22
22
  }
23
- return { type: 'bearer', token };
23
+ // Token-forward: caller JWT must reach dataplane CIP/pipeline unchanged (no API_KEY swap).
24
+ return { type: 'bearer', token, preserveBearer: true };
24
25
  }
25
26
 
26
27
  /**
@@ -20,7 +20,7 @@ function extractBearerToken(req) {
20
20
  if (!auth || typeof auth !== 'string') {
21
21
  return null;
22
22
  }
23
- const match = auth.match(/^Bearer\s+(.+)$/i);
23
+ const match = auth.match(/^Bearer\s+([^\s]+)\s*$/i);
24
24
  return match ? match[1].trim() : null;
25
25
  }
26
26
 
@@ -19,6 +19,21 @@ function resolveEnterpriseMcpSystemKey() {
19
19
  return (process.env.ENTERPRISE_MCP_SYSTEM_KEY || 'enterprise-integration-lifecycle').trim();
20
20
  }
21
21
 
22
+ /**
23
+ * COM-generated Enterprise MCP tools sometimes use OpenAPI operationId forms
24
+ * (e.g. repository.getrepositorystatus) instead of canonical capability keys
25
+ * (repository.status). Map canonical → published name when present.
26
+ *
27
+ * @type {Record<string, string>}
28
+ */
29
+ const ENTERPRISE_MCP_TOOL_ALIASES = {
30
+ 'repository.status': 'repository.getrepositorystatus',
31
+ 'repository.sync': 'repository.runrepositorysync',
32
+ 'repository.createReview': 'repository.runrepositorycreatereview',
33
+ 'repository.dispatchPipeline': 'repository.runrepositorydispatchpipeline',
34
+ 'template.materializeWorkspace': 'template.runwizard'
35
+ };
36
+
22
37
  /**
23
38
  * @param {string} capabilityKey
24
39
  * @returns {{ resourceType: string, toolName: string }}
@@ -29,9 +44,10 @@ function parseCapabilityKey(capabilityKey) {
29
44
  if (dotIndex <= 0) {
30
45
  throw new Error('capabilityKey must be resourceType.operation form');
31
46
  }
47
+ const toolName = ENTERPRISE_MCP_TOOL_ALIASES[key] || key;
32
48
  return {
33
49
  resourceType: key.slice(0, dotIndex),
34
- toolName: key
50
+ toolName
35
51
  };
36
52
  }
37
53
 
@@ -42,7 +58,7 @@ function parseCapabilityKey(capabilityKey) {
42
58
  * @returns {string}
43
59
  */
44
60
  function buildMcpExecutePath(systemKey, resourceType, toolName) {
45
- return `/api/v1/mcp/${encodeURIComponent(systemKey)}/${encodeURIComponent(resourceType)}/tools/${encodeURIComponent(toolName)}/execute`;
61
+ return `/api/v1/mcp/servers/${encodeURIComponent(systemKey)}/${encodeURIComponent(resourceType)}/tools/${encodeURIComponent(toolName)}/execute`;
46
62
  }
47
63
 
48
64
  /**
@@ -22,6 +22,15 @@ const SCHEMA_DIR = path.join(__dirname, '..', 'schema');
22
22
  let systemSchemaDoc = null;
23
23
  /** @type {Object|null} */
24
24
  let datasourceSchemaDoc = null;
25
+ const PROTOTYPE_POLLUTION_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
26
+
27
+ /**
28
+ * @param {string} part
29
+ * @returns {boolean}
30
+ */
31
+ function isSafePathPart(part) {
32
+ return typeof part === 'string' && part.length > 0 && !PROTOTYPE_POLLUTION_KEYS.has(part);
33
+ }
25
34
 
26
35
  function getSystemSchemaDoc() {
27
36
  if (!systemSchemaDoc) {
@@ -90,10 +99,18 @@ function getPathValue(doc, dottedPath) {
90
99
  */
91
100
  function setPathValue(doc, dottedPath, value) {
92
101
  const parts = dottedPath.split('.');
102
+ for (const part of parts) {
103
+ if (!isSafePathPart(part)) {
104
+ const err = new Error(`Unsafe path segment: ${part}`);
105
+ err.statusCode = 400;
106
+ throw err;
107
+ }
108
+ }
93
109
  let current = doc;
94
110
  for (let i = 0; i < parts.length - 1; i += 1) {
95
111
  const part = parts[i];
96
- if (current[part] === null || current[part] === undefined || typeof current[part] !== 'object') {
112
+ const next = Object.prototype.hasOwnProperty.call(current, part) ? current[part] : undefined;
113
+ if (next === null || next === undefined || typeof next !== 'object' || Array.isArray(next)) {
97
114
  current[part] = {};
98
115
  }
99
116
  current = current[part];
@@ -12,18 +12,35 @@ const { frozenObject, schemaRef, stringArraySchema } = require('./openapi-schema
12
12
 
13
13
  /** @type {Record<string, Object>} */
14
14
  const OPENAPI_RESULT_SCHEMAS = {
15
- ProblemDetails: frozenObject(
16
- {
17
- type: { type: 'string', format: 'uri-reference' },
18
- title: { type: 'string' },
19
- status: { type: 'integer', minimum: 100, maximum: 599 },
20
- detail: { type: 'string' },
21
- instance: { type: 'string' },
22
- correlationId: { type: 'string' },
23
- errors: { type: 'array', items: schemaRef('ProblemValidationError') }
24
- },
25
- ['type', 'title', 'status', 'detail']
15
+ ProblemDetail: Object.assign(
16
+ frozenObject(
17
+ {
18
+ type: { type: 'string', format: 'uri-reference' },
19
+ title: { type: 'string' },
20
+ status: { type: 'integer', minimum: 100, maximum: 599 },
21
+ detail: { type: 'string' },
22
+ instance: { type: 'string' },
23
+ correlationId: { type: 'string' },
24
+ errors: { type: 'array', items: schemaRef('ProblemValidationError') }
25
+ },
26
+ ['type', 'title', 'status', 'detail', 'correlationId', 'instance', 'errors']
27
+ ),
28
+ {
29
+ example: {
30
+ type: '/Errors/Unauthorized',
31
+ title: 'Unauthorized',
32
+ status: 401,
33
+ detail: 'Missing or invalid Bearer token.',
34
+ instance: '/api/v1/builder/workspaces/me/systems',
35
+ correlationId: '00000000-0000-4000-8000-000000000003',
36
+ errors: []
37
+ }
38
+ }
26
39
  ),
40
+ /** Prefer ProblemDetail; allOf alias for published REST lint (OAPI-148). */
41
+ ProblemDetails: {
42
+ allOf: [schemaRef('ProblemDetail')]
43
+ },
27
44
  ProblemValidationError: frozenObject(
28
45
  {
29
46
  field: { type: 'string' },
@@ -22,7 +22,7 @@ const CORE_SCHEMA_DESCRIPTIONS = {
22
22
  RepositorySyncDirection: {
23
23
  _schema: 'Repository sync direction: pull remote into workspace or push workspace to remote.'
24
24
  },
25
- ProblemDetails: {
25
+ ProblemDetail: {
26
26
  _schema: 'RFC 7807 problem response for builder-api errors (application/problem+json).',
27
27
  type: 'Problem type URI or identifier.',
28
28
  title: 'Short human-readable error title.',
@@ -32,8 +32,11 @@ const CORE_SCHEMA_DESCRIPTIONS = {
32
32
  correlationId: 'Correlation id for log lookup across services.',
33
33
  errors: 'Field-level validation errors when status is 400.'
34
34
  },
35
+ ProblemDetails: {
36
+ _schema: 'Alias of ProblemDetail (allOf) for backward-compatible $refs.'
37
+ },
35
38
  ProblemValidationError: {
36
- _schema: 'Single field validation error inside ProblemDetails.errors.',
39
+ _schema: 'Single field validation error inside ProblemDetail.errors.',
37
40
  field: 'JSON pointer or field name that failed validation.',
38
41
  message: 'Validation failure message.',
39
42
  value: 'Rejected value when safe to echo (may be omitted for secrets).'