@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
@@ -24,6 +24,17 @@ function expandConfiguredSecretsTilde(filePath) {
24
24
  return filePath;
25
25
  }
26
26
 
27
+ /** One auto-renew attempt per CLI process (shared by concurrent getRemoteDevAuth callers). */
28
+ let autoRenewPromise = null;
29
+
30
+ /**
31
+ * Reset process-level auto-renew gate (tests only).
32
+ * @returns {void}
33
+ */
34
+ function resetDevCertAutoRenewGateForTests() {
35
+ autoRenewPromise = null;
36
+ }
37
+
27
38
  /**
28
39
  * Single API object so resolveSharedSecretsEndpoint and callers share one getRemoteDevAuth
29
40
  * (Jest spies and partial mocks work reliably).
@@ -39,12 +50,11 @@ const remoteDevAuth = {
39
50
  },
40
51
 
41
52
  /**
42
- * Get Builder Server URL, client cert PEM, and optional dev root CA when remote is configured; otherwise null.
43
- * serverCaPem comes from ca.pem (saved at dev init); required for Node TLS to private-CA servers.
44
- * Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
53
+ * Read remote server URL + client cert from disk without auto-renew.
54
+ * Use when renewing (avoids recursion) or when only a local presence check is needed.
45
55
  * @returns {Promise<{ serverUrl: string, clientCertPem: string, serverCaPem: string|null }|null>}
46
56
  */
47
- async getRemoteDevAuth() {
57
+ async readRemoteDevAuth() {
48
58
  const serverUrl = await config.getRemoteServer();
49
59
  if (!serverUrl) return null;
50
60
  const devId = await config.getDeveloperId();
@@ -55,6 +65,30 @@ const remoteDevAuth = {
55
65
  return { serverUrl, clientCertPem, serverCaPem };
56
66
  },
57
67
 
68
+ /**
69
+ * Get Builder Server URL, client cert PEM, and optional dev root CA when remote is configured; otherwise null.
70
+ * When the client cert is still valid but expires within 14 days, renews it once per process before returning.
71
+ * serverCaPem comes from ca.pem (saved at dev init); required for Node TLS to private-CA servers.
72
+ * Use for cert-authenticated dev API calls (settings, users, ssh-keys, secrets).
73
+ * @returns {Promise<{ serverUrl: string, clientCertPem: string, serverCaPem: string|null }|null>}
74
+ */
75
+ async getRemoteDevAuth() {
76
+ const existing = await remoteDevAuth.readRemoteDevAuth();
77
+ if (!existing) return null;
78
+ if (!autoRenewPromise) {
79
+ autoRenewPromise = (async() => {
80
+ const { ensureDevCertFreshIfDue } = require('../commands/dev-refresh');
81
+ return ensureDevCertFreshIfDue({ quiet: false });
82
+ })();
83
+ }
84
+ try {
85
+ await autoRenewPromise;
86
+ } catch {
87
+ // ensureDevCertFreshIfDue already handles failures; keep using current cert
88
+ }
89
+ return remoteDevAuth.readRemoteDevAuth();
90
+ },
91
+
58
92
  /**
59
93
  * Resolve where shared secrets read/write should go. After dev init, config may still hold a
60
94
  * server-side filesystem path from GET /api/dev/settings (not an http URL) if merge did not rewrite it.
@@ -148,3 +182,4 @@ function normalizeSharedSecretsFilePath(configured) {
148
182
  }
149
183
 
150
184
  module.exports = remoteDevAuth;
185
+ module.exports.resetDevCertAutoRenewGateForTests = resetDevCertAutoRenewGateForTests;
@@ -12,6 +12,32 @@
12
12
  'use strict';
13
13
 
14
14
  const { parseImageOverride } = require('./parse-image-ref');
15
+ const DOCKER_REPOSITORY_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
16
+ const DOCKER_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
+
18
+ /**
19
+ * @param {string} repository
20
+ * @returns {string}
21
+ */
22
+ function assertSafeRepository(repository) {
23
+ const trimmed = String(repository || '').trim();
24
+ if (!trimmed || !DOCKER_REPOSITORY_RE.test(trimmed)) {
25
+ throw new Error(`Invalid Docker repository path: ${repository}`);
26
+ }
27
+ return trimmed;
28
+ }
29
+
30
+ /**
31
+ * @param {string} tag
32
+ * @returns {string}
33
+ */
34
+ function assertSafeTag(tag) {
35
+ const trimmed = String(tag || '').trim();
36
+ if (!trimmed || !DOCKER_TAG_RE.test(trimmed)) {
37
+ throw new Error(`Invalid Docker image tag: ${tag}`);
38
+ }
39
+ return trimmed;
40
+ }
15
41
 
16
42
  /**
17
43
  * Repository path without host (same rules as compose-generator getImageName).
@@ -59,7 +85,11 @@ function normalizeDockerRegistryPrefix(registry) {
59
85
  if (!t) {
60
86
  return '';
61
87
  }
62
- return t.replace(/\/+$/, '');
88
+ let end = t.length;
89
+ while (end > 0 && t[end - 1] === '/') {
90
+ end -= 1;
91
+ }
92
+ return t.slice(0, end);
63
93
  }
64
94
 
65
95
  /**
@@ -81,18 +111,18 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
81
111
  if (opts.image) {
82
112
  const parsed = parseImageOverride(opts.image);
83
113
  return {
84
- imageName: parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName),
85
- imageTag: parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig)
114
+ imageName: assertSafeRepository(parsed ? parsed.name : getRepositoryPathFromConfig(appConfig, appName)),
115
+ imageTag: assertSafeTag(parsed ? parsed.tag : tagFromOpts || imageTagFromConfig(appConfig))
86
116
  };
87
117
  }
88
118
 
89
- const baseRepo = getRepositoryPathFromConfig(appConfig, appName);
90
- const imageTag = tagFromOpts || imageTagFromConfig(appConfig);
119
+ const baseRepo = assertSafeRepository(getRepositoryPathFromConfig(appConfig, appName));
120
+ const imageTag = assertSafeTag(tagFromOpts || imageTagFromConfig(appConfig));
91
121
  const prefix =
92
122
  normalizeDockerRegistryPrefix(opts.registry) ||
93
123
  normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
94
124
  if (prefix) {
95
- return { imageName: `${prefix}/${baseRepo}`, imageTag };
125
+ return { imageName: assertSafeRepository(`${prefix}/${baseRepo}`), imageTag };
96
126
  }
97
127
  return { imageName: baseRepo, imageTag };
98
128
  }
@@ -409,13 +409,29 @@ function applyLabDataplaneApiKeyBearer(authConfig) {
409
409
  }
410
410
 
411
411
  function requireBearerForDataplanePipeline(authConfig) {
412
+ if (!authConfig || typeof authConfig !== 'object') {
413
+ return;
414
+ }
415
+
412
416
  if (authConfig.type === 'client-token' && authConfig.token) {
413
417
  return;
414
418
  }
415
419
 
420
+ // Token-forward (builder-api / bearerForward): never substitute lab API_KEY for the caller JWT.
421
+ if (authConfig.preserveBearer && authConfig.token) {
422
+ return;
423
+ }
424
+
416
425
  const preferAppClientToken =
417
426
  String(process.env.AIFABRIX_DEPLOYMENT_AUTH || '').toLowerCase() === 'client-credentials';
418
427
 
428
+ const existing = authConfig.token ? String(authConfig.token) : '';
429
+ const looksLikeJwt = existing.split('.').length === 3 && existing.length > 80;
430
+ // Enterprise path: keep a real OAuth JWT; do not overwrite with process.env.API_KEY.
431
+ if (looksLikeJwt) {
432
+ return;
433
+ }
434
+
419
435
  if (!preferAppClientToken && process.env.API_KEY) {
420
436
  const apiKey = String(process.env.API_KEY);
421
437
  if (authConfig.type === 'bearer' && authConfig.token !== apiKey) {
@@ -10,9 +10,10 @@
10
10
 
11
11
  /**
12
12
  * @param {Object} [options] - CLI options from test / sync callers
13
- * @returns {{ minimal: true, verbose: boolean, silentResolve: true, syncMode: true, force?: true }}
13
+ * @returns {{ minimal: true, verbose: boolean, silentResolve: true, syncMode: true, force?: true, authConfig?: Object, dataplaneUrl?: string, environment?: string }}
14
14
  */
15
15
  function buildMinimalUploadSyncOptions(options = {}) {
16
+ const deployPreflight = options.uploadContext === 'deploy-preflight';
16
17
  const out = {
17
18
  minimal: true,
18
19
  // CLI -v on verify-* / test-* is for run output, not preflight upload banners.
@@ -20,14 +21,27 @@ function buildMinimalUploadSyncOptions(options = {}) {
20
21
  silentResolve: true,
21
22
  syncMode: true,
22
23
  skipDataSync: true,
23
- suppressPipelineSpinner: options.suppressPipelineSpinner === true
24
+ suppressPipelineSpinner:
25
+ options.suppressPipelineSpinner === true || deployPreflight === true
24
26
  };
25
27
  if (options.force === true) {
26
28
  out.force = true;
27
29
  }
28
- if (options.suppressSummary === true) {
30
+ if (options.suppressSummary === true || deployPreflight === true) {
29
31
  out.suppressSummary = true;
30
32
  }
33
+ if (deployPreflight) {
34
+ out.uploadContext = 'deploy-preflight';
35
+ }
36
+ // Builder-api / programmatic callers: keep request Bearer — do not fall back to
37
+ // getDeploymentAuth against a stale mount AIFABRIX_CONFIG device token.
38
+ if (options.authConfig && options.dataplaneUrl) {
39
+ out.authConfig = options.authConfig;
40
+ out.dataplaneUrl = options.dataplaneUrl;
41
+ if (options.environment) {
42
+ out.environment = options.environment;
43
+ }
44
+ }
31
45
  return out;
32
46
  }
33
47
 
@@ -307,6 +307,10 @@ function resolveCachedVendorSpec(documentKey, prefix, appPath, systemKey, vendor
307
307
  function validateServiceContractVendorOpenApi(ds, prefix, appPath, systemKey, vendorCache) {
308
308
  const errors = [];
309
309
  const warnings = [];
310
+ // Python/CIP handlers without a vendor OpenAPI binding are valid serviceContracts.
311
+ if (!hasVendorOpenapiOnManifest(ds)) {
312
+ return { errors, warnings };
313
+ }
310
314
  const documentKey =
311
315
  typeof ds.openapi?.documentKey === 'string' ? ds.openapi.documentKey.trim() : '';
312
316
  if (!documentKey) {
@@ -223,10 +223,11 @@ async function validateRbac(appName, options = {}) {
223
223
  throw new Error(`Invalid syntax in ${basename}: ${error.message}`);
224
224
  }
225
225
 
226
- const errors = [
227
- ...validateRoles(rbac.roles),
228
- ...validatePermissions(rbac.permissions)
229
- ];
226
+ const [roleErrors, permissionErrors] = await Promise.all([
227
+ Promise.resolve(validateRoles(rbac.roles)),
228
+ Promise.resolve(validatePermissions(rbac.permissions))
229
+ ]);
230
+ const errors = [...roleErrors, ...permissionErrors];
230
231
 
231
232
  return {
232
233
  valid: errors.length === 0,
@@ -275,7 +276,7 @@ async function validateEnvTemplate(appName, options = {}) {
275
276
  if (!trimmed.includes('=')) {
276
277
  errors.push(`Line ${index + 1}: Invalid environment variable format (missing =)`);
277
278
  } else {
278
- const [key, _value] = trimmed.split('=', 2);
279
+ const [key] = trimmed.split('=', 2);
279
280
  // Trim key to handle whitespace issues
280
281
  // Empty values are allowed (_value can be empty string or undefined)
281
282
  const trimmedKey = key ? key.trim() : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.50.0",
3
+ "version": "2.51.0",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -85,36 +85,58 @@
85
85
  "cache": true,
86
86
  "temp-dir": ".nyc_output"
87
87
  },
88
+ "overrides": {
89
+ "@babel/core": "^7.29.7",
90
+ "@babel/plugin-transform-modules-systemjs": "^7.29.7",
91
+ "fast-uri": "^4.1.0",
92
+ "flatted": "^3.4.2",
93
+ "follow-redirects": "^1.16.0",
94
+ "form-data": "^4.0.6",
95
+ "lodash": "^4.18.1",
96
+ "minimatch": "^10.2.5",
97
+ "picomatch": "^4.0.5",
98
+ "qs": "^6.15.3"
99
+ },
88
100
  "pnpm": {
89
101
  "overrides": {
90
- "@bcoe/v8-coverage": "^1.0.2"
102
+ "@bcoe/v8-coverage": "^1.0.2",
103
+ "@babel/core": "^7.29.7",
104
+ "@babel/plugin-transform-modules-systemjs": "^7.29.7",
105
+ "fast-uri": "^4.1.0",
106
+ "flatted": "^3.4.2",
107
+ "follow-redirects": "^1.16.0",
108
+ "form-data": "^4.0.6",
109
+ "lodash": "^4.18.1",
110
+ "minimatch": "^10.2.5",
111
+ "picomatch": "^4.0.5",
112
+ "qs": "^6.15.3"
91
113
  }
92
114
  },
93
115
  "dependencies": {
94
116
  "@aifabrix/miso-client": "^4.19.0",
95
- "ajv": "^8.12.0",
117
+ "ajv": "^8.20.0",
96
118
  "ajv-formats": "^3.0.1",
97
- "axios": "^1.6.0",
119
+ "axios": "^1.18.1",
98
120
  "chalk": "^4.1.2",
99
121
  "commander": "^11.1.0",
100
- "handlebars": "^4.7.8",
122
+ "handlebars": "^4.7.9",
101
123
  "inquirer": "^8.2.5",
102
124
  "inquirer-autocomplete-prompt": "^2.0.0",
103
- "js-yaml": "^4.1.0",
125
+ "js-yaml": "^4.3.0",
104
126
  "ora": "^5.4.1",
105
- "yaml": "^2.4.0"
127
+ "yaml": "^2.9.0"
106
128
  },
107
129
  "devDependencies": {
108
130
  "@babel/plugin-syntax-optional-chaining": "^7.8.3",
109
- "@babel/preset-env": "^7.29.0",
131
+ "@babel/preset-env": "^7.29.7",
110
132
  "@bcoe/v8-coverage": "^1.0.2",
111
- "@nestjs/common": "^10.4.15",
112
- "@nestjs/core": "^10.4.15",
113
- "@nestjs/platform-express": "^10.4.15",
133
+ "@nestjs/common": "^11.1.28",
134
+ "@nestjs/core": "^11.1.28",
135
+ "@nestjs/platform-express": "^11.1.28",
114
136
  "@types/node": "^20.10.0",
115
137
  "cross-env": "^10.1.0",
116
138
  "eslint": "^8.55.0",
117
- "jest": "^30.2.0",
139
+ "jest": "^30.4.2",
118
140
  "markdownlint-cli": "^0.49.0",
119
141
  "nyc": "^18.0.0",
120
142
  "reflect-metadata": "^0.2.2",
@@ -18,8 +18,8 @@ const DEFAULT_OUT = path.join(
18
18
  '..',
19
19
  'aifabrix-dataplane',
20
20
  'integration',
21
- 'aifabrix-builder-api',
22
- 'aifabrix-builder-api-evidence.json'
21
+ 'builder-api',
22
+ 'builder-api-evidence.json'
23
23
  );
24
24
 
25
25
  const EVIDENCE_PATTERNS = path.join(REPO_ROOT, 'docs', 'builder-help', 'evidence-patterns.json');
@@ -80,9 +80,9 @@ function loadActor() {
80
80
  function saveActor(displayName, email) {
81
81
  const actorPath = resolveActorPath();
82
82
  fs.mkdirSync(path.dirname(actorPath), { recursive: true });
83
- const lines = [`displayName: "${displayName.replace(/"/g, '\\"')}"`];
83
+ const lines = [`displayName: ${JSON.stringify(displayName)}`];
84
84
  if (email) {
85
- lines.push(`email: "${email.replace(/"/g, '\\"')}"`);
85
+ lines.push(`email: ${JSON.stringify(email)}`);
86
86
  }
87
87
  fs.writeFileSync(actorPath, `${lines.join('\n')}\n`, 'utf8');
88
88
  return actorPath;
@@ -87,7 +87,7 @@ function patchPlanMeta(planPath, key, value) {
87
87
  die('Plan has no frontmatter to patch');
88
88
  }
89
89
  let yaml = match[1];
90
- const quoted = `"${String(value).replace(/"/g, '\\"')}"`;
90
+ const quoted = JSON.stringify(String(value));
91
91
  const re = new RegExp(`^${key}:.*$`, 'm');
92
92
  if (re.test(yaml)) {
93
93
  yaml = yaml.replace(re, `${key}: ${quoted}`);
@@ -111,7 +111,7 @@ function loadPlanFile(planPath) {
111
111
  if (!match) {
112
112
  return null;
113
113
  }
114
- const meta = parseSimpleYaml(match[1]) || {};
114
+ const meta = parseSimpleYaml(match[1]);
115
115
  if (Array.isArray(meta.todos)) {
116
116
  meta.todos = normalizeTodos(meta.todos);
117
117
  }
@@ -17,6 +17,17 @@ const { loadPlanFile, hasSurface } = require('./plan-parse');
17
17
  const REPO_ROOT = path.resolve(__dirname, '../..');
18
18
  const PREVALIDATE_REPORT_DIR = path.join(REPO_ROOT, 'temp/plan-prevalidation');
19
19
 
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {string}
23
+ */
24
+ function escapeMarkdownCell(value) {
25
+ return String(value ?? '')
26
+ .replace(/\r?\n+/g, ' ')
27
+ .replace(/\|/g, '\\|')
28
+ .trim();
29
+ }
30
+
20
31
  /**
21
32
  * @typedef {{ label: string, status: 'pass' | 'fail' | 'warn' | 'skip', detail?: string }} GateResult
22
33
  */
@@ -299,7 +310,7 @@ function buildPrevalidateReport(params) {
299
310
  '| --- | --- | --- |'
300
311
  ];
301
312
  for (const g of gates) {
302
- lines.push(`| ${g.label} | ${g.status} | ${(g.detail || '').replace(/\|/g, '\\|')} |`);
313
+ lines.push(`| ${escapeMarkdownCell(g.label)} | ${escapeMarkdownCell(g.status)} | ${escapeMarkdownCell(g.detail || '')} |`);
303
314
  }
304
315
  return `${lines.join('\n')}\n`;
305
316
  }
@@ -8,6 +8,17 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { REPORT_DIR } = require('./plan-config');
10
10
 
11
+ /**
12
+ * @param {unknown} value
13
+ * @returns {string}
14
+ */
15
+ function escapeMarkdownCell(value) {
16
+ return String(value ?? '')
17
+ .replace(/\r?\n+/g, ' ')
18
+ .replace(/\|/g, '\\|')
19
+ .trim();
20
+ }
21
+
11
22
  /**
12
23
  * @param {object} params
13
24
  * @returns {string}
@@ -44,13 +55,13 @@ function buildSummaryMarkdown(params) {
44
55
 
45
56
  for (const g of gates) {
46
57
  const icon = g.status === 'pass' ? 'pass' : g.status === 'warn' ? 'warn' : 'FAIL';
47
- lines.push(`| ${g.label} | ${icon} | ${(g.detail || '').replace(/\|/g, '\\|')} |`);
58
+ lines.push(`| ${escapeMarkdownCell(g.label)} | ${icon} | ${escapeMarkdownCell(g.detail || '')} |`);
48
59
  }
49
60
 
50
61
  lines.push('', '## Todos', '', '| ID | Status | Content |', '| --- | --- | --- |');
51
62
  for (const t of todos) {
52
63
  lines.push(
53
- `| ${t.id} | ${t.status} | ${String(t.content).replace(/\|/g, '\\|').slice(0, 100)} |`
64
+ `| ${escapeMarkdownCell(t.id)} | ${escapeMarkdownCell(t.status)} | ${escapeMarkdownCell(t.content).slice(0, 100)} |`
54
65
  );
55
66
  }
56
67
 
@@ -10,8 +10,8 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../
10
10
  const MIN_AIFABRIX_VERSION = '2.45.0';
11
11
 
12
12
  /**
13
- * @param {string} version
14
- * @returns {number[]}
13
+ * @param {string} version - Semver string.
14
+ * @returns {number[]} Parsed numeric semver segments.
15
15
  */
16
16
  function parseSemver(version) {
17
17
  return String(version)
@@ -21,9 +21,9 @@ function parseSemver(version) {
21
21
  }
22
22
 
23
23
  /**
24
- * @param {string} left
25
- * @param {string} right
26
- * @returns {number}
24
+ * @param {string} left - Left semver.
25
+ * @param {string} right - Right semver.
26
+ * @returns {number} Compare result (1, 0, -1).
27
27
  */
28
28
  function compareSemver(left, right) {
29
29
  const a = parseSemver(left);
@@ -38,8 +38,8 @@ function compareSemver(left, right) {
38
38
  }
39
39
 
40
40
  /**
41
- * @param {string} bin
42
- * @returns {string|null}
41
+ * @param {string} bin - Candidate executable path.
42
+ * @returns {string|null} CLI version output or null.
43
43
  */
44
44
  function readAifabrixVersion(bin) {
45
45
  try {
@@ -50,7 +50,7 @@ function readAifabrixVersion(bin) {
50
50
  }
51
51
 
52
52
  /**
53
- * @returns {string[]}
53
+ * @returns {string[]} Candidate aifabrix binary paths.
54
54
  */
55
55
  function collectAifabrixCandidates() {
56
56
  const candidates = [];
@@ -85,14 +85,14 @@ function collectAifabrixCandidates() {
85
85
  /**
86
86
  * Prefer this repo's bin/aifabrix.js when developing the builder package locally.
87
87
  *
88
- * @returns {string}
88
+ * @returns {string} Resolved aifabrix executable path.
89
89
  */
90
90
  export function resolveAifabrixBin() {
91
91
  let bestBin = null;
92
92
  let bestVersion = null;
93
93
 
94
94
  for (const candidate of collectAifabrixCandidates()) {
95
- let resolved = candidate;
95
+ let resolved;
96
96
  try {
97
97
  if (fs.existsSync(candidate)) {
98
98
  resolved = fs.realpathSync(candidate);
@@ -5,8 +5,7 @@ app:
5
5
  description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
6
6
  type: webapp
7
7
  language: python # Explicitly specify Python language
8
- version: 2.0.0
9
-
8
+ version: 2.0.10
10
9
  # Image Configuration
11
10
  # Set tag to match your build (e.g. aifabrix build dataplane -t 1.0.0 then tag: 1.0.0)
12
11
  # Registry is required so the controller can pull the image (avoids "docker: not found" on the controller host).
@@ -69,7 +68,7 @@ build:
69
68
  # Pulled-image local compose (no bind-mount): drives `command` in generated docker-compose. Override for non-standard images (e.g. stub ACR tags).
70
69
  imageRun: exec python -m uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-3001}
71
70
  # Image policy (442.0): explicit Dockerfile COPY; worker/beat via python -m app.core.*
72
- # integration/ in image: <systemKey>-deploy.json + application.json|yaml only (no test-*, wizard-configs).
71
+ # integration/ in image: only onboarding-catalog.json keys (+ deploy/application per key).
73
72
  worker: true
74
73
  imageRunWorker: exec python -m app.core.celery_worker_main
75
74
  imageRunBeat: exec python -m app.core.celery_beat_main
@@ -83,7 +82,9 @@ build:
83
82
  # NOT included (auto-configured):
84
83
  # - Database URLs, Redis → auto-generated during Azure install
85
84
  # - MISO_*, KEYCLOAK_* → auto-generated during Azure install
86
- # - AI params (OPENAI_*, AZURE_OPENAI_*) → from controller config
85
+ # - AI params / provider secrets → LLM external systems + secret store
86
+ # (never product OPENAI_* / AZURE_OPENAI_*). ≥2 providers →
87
+ # ENTERPRISE_LLM_DEFAULT_SELECTOR (portal LLM category).
87
88
  # - ENVIRONMENT → set by deployment target
88
89
 
89
90
  configuration:
@@ -91,6 +92,7 @@ configuration:
91
92
  # Operations & Debugging
92
93
  # -------------------------------------------------------------------------
93
94
  - name: LOG_LEVEL
95
+ category: Operations & Debugging
94
96
  portalInput:
95
97
  field: select
96
98
  label: "Log Level"
@@ -101,6 +103,7 @@ configuration:
101
103
  - ERROR
102
104
 
103
105
  - name: ABAC_AUDIT_DETAIL_LEVEL
106
+ category: Operations & Debugging
104
107
  portalInput:
105
108
  field: select
106
109
  label: "ABAC Audit Detail Level"
@@ -109,6 +112,7 @@ configuration:
109
112
  - detailed
110
113
 
111
114
  - name: RBAC_AUDIT_DETAIL_LEVEL
115
+ category: Operations & Debugging
112
116
  portalInput:
113
117
  field: select
114
118
  label: "RBAC Audit Detail Level"
@@ -118,6 +122,7 @@ configuration:
118
122
  - explain
119
123
 
120
124
  - name: TRUST_CUSTOMER_POLICY_LEVEL
125
+ category: Trust
121
126
  portalInput:
122
127
  field: select
123
128
  label: "Trust policy level (no-policy defaults)"
@@ -127,6 +132,7 @@ configuration:
127
132
  - relaxed
128
133
 
129
134
  - name: TRUST_PUBLISH_GATE_ENABLED
135
+ category: Trust
130
136
  portalInput:
131
137
  field: select
132
138
  label: "Enforce trust gate on datasource publish"
@@ -135,6 +141,7 @@ configuration:
135
141
  - "true"
136
142
 
137
143
  - name: TRUST_PROMOTE_GATE_ENABLED
144
+ category: Trust
138
145
  portalInput:
139
146
  field: select
140
147
  label: "Enforce trust gate on external system publish (promote scope)"
@@ -143,6 +150,7 @@ configuration:
143
150
  - "true"
144
151
 
145
152
  - name: TRUST_RUNTIME_GATE_ENABLED
153
+ category: Trust
146
154
  portalInput:
147
155
  field: select
148
156
  label: "Enforce trust gate on CIP execution and workforce generation inputs (runtime scope)"
@@ -151,6 +159,7 @@ configuration:
151
159
  - "true"
152
160
 
153
161
  - name: TRUST_AI_EXPOSURE_GATE_ENABLED
162
+ category: Trust
154
163
  portalInput:
155
164
  field: select
156
165
  label: "Enforce trust gate on AI document-storage prompt generation"
@@ -164,24 +173,28 @@ configuration:
164
173
  # Tune based on customer data volumes and external API characteristics
165
174
 
166
175
  - name: CIP_EXECUTION_MAX_RECORDS
176
+ category: CIP Execution
167
177
  portalInput:
168
178
  field: text
169
179
  label: "Max Records per CIP Execution"
170
180
  placeholder: "100000"
171
181
 
172
182
  - name: CIP_EXECUTION_MAX_RESPONSE_SIZE_MB
183
+ category: CIP Execution
173
184
  portalInput:
174
185
  field: text
175
186
  label: "Max Response Size (MB)"
176
187
  placeholder: "100.0"
177
188
 
178
189
  - name: CIP_EXECUTION_OPERATION_TIMEOUT
190
+ category: CIP Execution
179
191
  portalInput:
180
192
  field: text
181
193
  label: "Operation Timeout (seconds)"
182
194
  placeholder: "300.0"
183
195
 
184
196
  - name: CIP_EXECUTION_HTTP_TIMEOUT
197
+ category: CIP Execution
185
198
  portalInput:
186
199
  field: text
187
200
  label: "HTTP Request Timeout (seconds)"
@@ -193,13 +206,22 @@ configuration:
193
206
  # Tune to match external API rate limits
194
207
 
195
208
  - name: CIP_EXECUTION_RATE_LIMIT_REQUESTS_PER_SECOND
209
+ category: CIP Rate Limiting
196
210
  portalInput:
197
211
  field: text
198
212
  label: "Rate Limit (requests/second)"
199
213
  placeholder: "10.0"
200
214
 
201
215
  - name: CIP_EXECUTION_RATE_LIMIT_BURST_SIZE
216
+ category: CIP Rate Limiting
202
217
  portalInput:
203
218
  field: text
204
219
  label: "Rate Limit Burst Size"
205
220
  placeholder: "20"
221
+
222
+ - name: ENTERPRISE_LLM_DEFAULT_SELECTOR
223
+ category: LLM
224
+ portalInput:
225
+ field: text
226
+ label: "Default LLM provider (system key when two or more providers)"
227
+ placeholder: "openai"