@aifabrix/builder 2.51.0 → 2.51.1

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 (162) hide show
  1. package/jest.isolated-projects.js +77 -1
  2. package/jest.projects.js +5 -426
  3. package/lib/api/assistants.api.js +67 -0
  4. package/lib/api/datasources-extended.api.js +6 -6
  5. package/lib/api/evidence-authoring-helpers.js +267 -0
  6. package/lib/api/evidence-authoring.api.js +337 -0
  7. package/lib/api/evidence.api.js +57 -1
  8. package/lib/api/index.js +24 -7
  9. package/lib/api/pipeline.api.js +43 -3
  10. package/lib/api/records-bulk.api.js +28 -3
  11. package/lib/api/types/evidence.types.js +156 -8
  12. package/lib/api/work.api.js +283 -0
  13. package/lib/app/run-deploy-job.js +108 -0
  14. package/lib/app/run-helpers.js +10 -3
  15. package/lib/app/run.js +8 -0
  16. package/lib/channels/add-channel.js +54 -23
  17. package/lib/channels/approval-guides/chatgpt.js +8 -7
  18. package/lib/channels/approval-guides/microsoft-copilot.js +7 -8
  19. package/lib/channels/channel-artifact-generator.js +14 -2
  20. package/lib/channels/channel-credential.js +3 -1
  21. package/lib/channels/channel-probe-evidence.js +79 -0
  22. package/lib/channels/channel-probe.js +265 -13
  23. package/lib/channels/channel-runtime-resolver.js +120 -29
  24. package/lib/channels/channel-status-display.js +44 -0
  25. package/lib/channels/channel-targets.js +7 -0
  26. package/lib/channels/chatgpt-runtime-contract.js +29 -0
  27. package/lib/channels/microsoft-copilot-package.js +280 -0
  28. package/lib/channels/runtime-tool-catalog.js +165 -0
  29. package/lib/cli/index.js +2 -0
  30. package/lib/cli/setup-app.js +4 -0
  31. package/lib/cli/setup-external-system-help.js +8 -2
  32. package/lib/cli/setup-external-system.js +12 -0
  33. package/lib/commands/app-test.js +2 -14
  34. package/lib/commands/datasource-load-export-cli.js +6 -1
  35. package/lib/commands/deploy-gate-failure-details.js +19 -17
  36. package/lib/commands/dev-down.js +2 -6
  37. package/lib/commands/evidence-kind.js +65 -15
  38. package/lib/commands/governance-generate-external.js +56 -21
  39. package/lib/commands/lifecycle-external.js +10 -4
  40. package/lib/commands/repair-datasource-apply.js +10 -1
  41. package/lib/commands/repair-datasource-run.js +4 -2
  42. package/lib/commands/repair-datasource-test-payload.js +80 -19
  43. package/lib/commands/repair-datasource-webhooks-sync-bridge.js +42 -3
  44. package/lib/commands/repair-internal.js +9 -1
  45. package/lib/commands/repair-openapi-sync.js +34 -17
  46. package/lib/commands/role-assistant.js +313 -0
  47. package/lib/commands/upload-data-sync-finalize.js +5 -4
  48. package/lib/commands/upload-sidecar-followups.js +144 -47
  49. package/lib/commands/upload.js +43 -6
  50. package/lib/commands/verify-governance-combined-external.js +73 -9
  51. package/lib/commands/verify-governance-db.js +80 -25
  52. package/lib/commands/verify-governance-external.js +29 -5
  53. package/lib/commands/wizard-core-helpers.js +7 -3
  54. package/lib/commands/wizard-core.js +78 -9
  55. package/lib/datasource/bulk-loader-run-batches.js +4 -2
  56. package/lib/datasource/bulk-loader-service.js +5 -1
  57. package/lib/datasource/datasource-validate-summary.js +2 -2
  58. package/lib/datasource/validate.js +13 -6
  59. package/lib/external-system/deploy-helpers.js +35 -1
  60. package/lib/external-system/deploy.js +23 -10
  61. package/lib/external-system/download-helpers.js +111 -2
  62. package/lib/external-system/download-role-assistant.js +55 -0
  63. package/lib/external-system/download.js +45 -26
  64. package/lib/external-system/test-helpers.js +1 -1
  65. package/lib/external-system/test.js +39 -3
  66. package/lib/generator/builders.js +3 -0
  67. package/lib/generator/wizard.js +51 -4
  68. package/lib/lifecycle/access-control-abac.js +121 -0
  69. package/lib/lifecycle/lifecycle-governance-options.js +4 -1
  70. package/lib/lifecycle/product-model.js +3 -2
  71. package/lib/lifecycle/report-display.js +14 -2
  72. package/lib/programmatic/capability-registry.js +20 -1
  73. package/lib/programmatic/command-options.js +1 -0
  74. package/lib/programmatic/index.js +4 -0
  75. package/lib/programmatic/manifest-patch-section.js +48 -9
  76. package/lib/programmatic/manifest-section-registry-data.js +3 -2
  77. package/lib/programmatic/openapi-apply-result-schemas.js +44 -0
  78. package/lib/programmatic/openapi-component-schemas.js +16 -1
  79. package/lib/programmatic/openapi-description-enricher.js +3 -1
  80. package/lib/programmatic/openapi-descriptions.js +55 -5
  81. package/lib/programmatic/openapi-evidence-schemas.js +161 -0
  82. package/lib/programmatic/openapi-manifest-schemas.js +6 -2
  83. package/lib/programmatic/openapi-operation-bindings.js +99 -0
  84. package/lib/programmatic/openapi-request-schemas.js +36 -1
  85. package/lib/programmatic/openapi-result-schemas.js +1 -1
  86. package/lib/programmatic/openapi-schema-descriptions-certify.js +5 -1
  87. package/lib/programmatic/openapi-schema-descriptions-core.js +1 -1
  88. package/lib/programmatic/openapi-schema-descriptions-evidence.js +156 -0
  89. package/lib/programmatic/openapi-schema-descriptions-manifest.js +4 -3
  90. package/lib/programmatic/openapi-schema-descriptions-operations.js +70 -0
  91. package/lib/programmatic/openapi-spec.js +47 -28
  92. package/lib/programmatic/orchestration-openapi-meta.js +5 -0
  93. package/lib/programmatic/request-timeout.js +3 -0
  94. package/lib/programmatic/route-handler-map.js +89 -2
  95. package/lib/programmatic/run-certify.js +15 -2
  96. package/lib/programmatic/run-deploy.js +74 -0
  97. package/lib/programmatic/run-evidence-authoring.js +80 -0
  98. package/lib/programmatic/run-protection.js +124 -0
  99. package/lib/programmatic/run-template-workspace.js +28 -1
  100. package/lib/programmatic/workspace-context.js +6 -0
  101. package/lib/protection/auth-context.js +48 -6
  102. package/lib/protection/paths.js +22 -0
  103. package/lib/protection/run-protection-create-helpers.js +22 -1
  104. package/lib/protection/run-protection-create.js +1 -1
  105. package/lib/protection/sync-after-upload.js +1 -1
  106. package/lib/role-assistant/evidence-sync.js +450 -0
  107. package/lib/role-assistant/github-workflow.js +96 -0
  108. package/lib/role-assistant/knowledge-sync.js +465 -0
  109. package/lib/role-assistant/settings.js +126 -0
  110. package/lib/role-assistant/test-cases-evaluate.js +170 -0
  111. package/lib/role-assistant/test-cases-honesty.js +184 -0
  112. package/lib/role-assistant/test-cases.js +364 -0
  113. package/lib/role-assistant/test-runner-workhub-answers.js +203 -0
  114. package/lib/role-assistant/test-runner-workhub.js +385 -0
  115. package/lib/role-assistant/test-runner.js +500 -0
  116. package/lib/schema/application-schema.json +16 -2
  117. package/lib/schema/deployment-rules.yaml +1 -0
  118. package/lib/schema/external-datasource.schema.json +38 -4
  119. package/lib/schema/external-system.schema.json +4 -4
  120. package/lib/schema/infra.parameter.yaml +20 -0
  121. package/lib/schema/role-assistant-settings.schema.json +108 -0
  122. package/lib/utils/api.js +52 -27
  123. package/lib/utils/app-path-resolve.js +72 -24
  124. package/lib/utils/authentication-shape.js +3 -1
  125. package/lib/utils/deployment-validation.js +28 -3
  126. package/lib/utils/docker-run-ephemeral.js +55 -0
  127. package/lib/utils/enterprise-knowledge-paths.js +29 -12
  128. package/lib/utils/env-copy-keycloak.js +196 -0
  129. package/lib/utils/env-copy.js +34 -1
  130. package/lib/utils/external-system-readiness-compact-display.js +3 -0
  131. package/lib/utils/help-builder.js +1 -0
  132. package/lib/utils/paths.js +67 -23
  133. package/lib/utils/pipeline-upload-auth-retry.js +15 -3
  134. package/lib/utils/role-assistant-paths.js +98 -0
  135. package/lib/utils/schema-loader.js +41 -0
  136. package/lib/utils/schema-resolver.js +36 -4
  137. package/lib/utils/token-manager.js +38 -29
  138. package/lib/utils/url-declarative-resolve-load-doc.js +38 -1
  139. package/lib/validation/external-manifest-validator.js +31 -8
  140. package/lib/validation/validate-external-file.js +8 -1
  141. package/package.json +2 -1
  142. package/scripts/export-builder-api-evidence.js +1 -1
  143. package/templates/applications/dataplane/application.yaml +23 -15
  144. package/templates/applications/dataplane/env.template +1 -1
  145. package/templates/applications/dataplane/rbac.yaml +14 -0
  146. package/templates/applications/miso-controller/application.yaml +1 -1
  147. package/templates/applications/miso-controller/env.template +3 -1
  148. package/templates/channels/chatgpt/install.template.md.hbs +42 -24
  149. package/templates/channels/chatgpt/integration/README.md +10 -7
  150. package/templates/channels/chatgpt/integration/application.json +2 -2
  151. package/templates/channels/chatgpt/integration/chatgpt-system.json +3 -3
  152. package/templates/channels/chatgpt/integration/env.template +1 -1
  153. package/templates/channels/chatgpt/manifest.template.json.hbs +36 -5
  154. package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +28 -0
  155. package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +22 -0
  156. package/templates/channels/microsoft-copilot/install.template.md.hbs +32 -22
  157. package/templates/channels/microsoft-copilot/integration/README.md +9 -7
  158. package/templates/channels/microsoft-copilot/integration/application.json +2 -2
  159. package/templates/channels/microsoft-copilot/integration/env.template +1 -1
  160. package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +7 -6
  161. package/templates/channels/microsoft-copilot/manifest.template.json.hbs +34 -6
  162. /package/.cursor/plans/{157.1-security-quality-violations-by-type.csv → Done/157.1-security-quality-violations-by-type.csv} +0 -0
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Host .env.local Keycloak loopback URL helpers for env-copy.
3
+ *
4
+ * @fileoverview Resolve developer-scoped Keycloak ports for host-side .env.local
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const yaml = require('js-yaml');
14
+ const config = require('../core/config');
15
+
16
+ /**
17
+ * Read unquoted env value from content.
18
+ * @param {string} content
19
+ * @param {string} key
20
+ * @returns {string}
21
+ */
22
+ function readEnvLineValue(content, key) {
23
+ const re = new RegExp(`^${key}\\s*=\\s*(.*)$`, 'm');
24
+ const m = content.match(re);
25
+ if (!m) return '';
26
+ return m[1].trim().replace(/^["']|["']$/g, '');
27
+ }
28
+
29
+ /**
30
+ * @param {Object} cfgModule - Config module with CONFIG_FILE
31
+ * @returns {number|null}
32
+ */
33
+ function readDeveloperIdFromConfig(cfgModule) {
34
+ const configPath = cfgModule && cfgModule.CONFIG_FILE ? cfgModule.CONFIG_FILE : null;
35
+ if (!configPath || !fs.existsSync(configPath)) {
36
+ return null;
37
+ }
38
+ try {
39
+ const cfgContent = fs.readFileSync(configPath, 'utf8');
40
+ const cfg = yaml.load(cfgContent) || {};
41
+ const raw = cfg['developer-id'];
42
+ if (typeof raw === 'number') {
43
+ return raw;
44
+ }
45
+ if (typeof raw === 'string' && /^[0-9]+$/.test(raw)) {
46
+ return parseInt(raw, 10);
47
+ }
48
+ } catch {
49
+ // ignore
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Base Keycloak published port from urls.local.yaml (before developer offset).
56
+ * @returns {number|null}
57
+ */
58
+ function readKeycloakBasePortFromUrlsLocal() {
59
+ try {
60
+ const urlsPath = path.join(path.dirname(config.CONFIG_FILE || ''), 'urls.local.yaml');
61
+ if (!fs.existsSync(urlsPath)) return null;
62
+ const doc = yaml.load(fs.readFileSync(urlsPath, 'utf8')) || {};
63
+ const raw = doc['keycloak-port'];
64
+ const n = typeof raw === 'number' ? raw : parseInt(String(raw || ''), 10);
65
+ return Number.isFinite(n) && n > 0 ? n : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {string} host
73
+ * @returns {string|null} Normalized loopback host or null when not loopback
74
+ */
75
+ function normalizeKeycloakLoopbackHost(host) {
76
+ let next = host;
77
+ if (next === 'keycloak') {
78
+ next = 'localhost';
79
+ }
80
+ const isLoopback =
81
+ next === 'localhost' || next === '127.0.0.1' || next === '::1' || next === '[::1]';
82
+ if (!isLoopback) {
83
+ return null;
84
+ }
85
+ return next === '::1' ? '[::1]' : next;
86
+ }
87
+
88
+ /**
89
+ * @param {string} content
90
+ * @returns {number|null}
91
+ */
92
+ function resolveExplicitKeycloakPort(content) {
93
+ const publicPortRaw = readEnvLineValue(content, 'KEYCLOAK_PUBLIC_PORT');
94
+ if (/^\d+$/.test(publicPortRaw)) {
95
+ return parseInt(publicPortRaw, 10);
96
+ }
97
+ const portRaw = readEnvLineValue(content, 'KEYCLOAK_PORT');
98
+ if (!/^\d+$/.test(portRaw)) {
99
+ return null;
100
+ }
101
+ const portNum = parseInt(portRaw, 10);
102
+ // 8080 is the in-container listen port — not the host publish port
103
+ return portNum !== 8080 ? portNum : null;
104
+ }
105
+
106
+ /**
107
+ * @returns {number|null}
108
+ */
109
+ function resolveDeveloperIdForKeycloakPort() {
110
+ try {
111
+ const raw = process.env.AIFABRIX_DEVELOPERID;
112
+ if (raw && /^[0-9]+$/.test(String(raw).trim())) {
113
+ return parseInt(String(raw).trim(), 10);
114
+ }
115
+ return readDeveloperIdFromConfig(config);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Published Keycloak host port for this developer (never a fixed 8182).
123
+ * Prefer KEYCLOAK_PUBLIC_PORT, then loopback KEYCLOAK_PORT (if not container 8080),
124
+ * else base from urls.local.yaml / 8082 + developer-id × 100.
125
+ *
126
+ * @param {string} content - Env file content
127
+ * @returns {{ host: string, port: number }|null}
128
+ */
129
+ function resolveHostKeycloakLoopbackEndpoint(content) {
130
+ const { publishedHostPort, parseDeveloperIdNum } = require('./declarative-url-ports');
131
+ const host = normalizeKeycloakLoopbackHost(
132
+ readEnvLineValue(content, 'KEYCLOAK_HOST') || 'localhost'
133
+ );
134
+ if (!host) {
135
+ return null;
136
+ }
137
+ const explicit = resolveExplicitKeycloakPort(content);
138
+ if (explicit !== null) {
139
+ return { host, port: explicit };
140
+ }
141
+ const base = readKeycloakBasePortFromUrlsLocal() || 8082;
142
+ const port = publishedHostPort(base, parseDeveloperIdNum(resolveDeveloperIdForKeycloakPort()));
143
+ return { host, port };
144
+ }
145
+
146
+ /**
147
+ * Upsert KEYCLOAK_PORT and KEYCLOAK_INTERNAL_SERVER_URL for host .env.local.
148
+ * @param {string} content
149
+ * @param {string} host
150
+ * @param {number} port
151
+ * @returns {string}
152
+ */
153
+ function patchKeycloakHostInternalLines(content, host, port) {
154
+ const internalUrl = `http://${host}:${port}/auth`;
155
+ let next = content;
156
+ if (readEnvLineValue(content, 'KEYCLOAK_HOST') === 'keycloak') {
157
+ next = next.replace(/^KEYCLOAK_HOST\s*=.*$/m, 'KEYCLOAK_HOST=localhost');
158
+ }
159
+ if (/^KEYCLOAK_PORT\s*=/m.test(next)) {
160
+ next = next.replace(/^KEYCLOAK_PORT\s*=.*$/m, `KEYCLOAK_PORT=${port}`);
161
+ } else {
162
+ next = `${next.trimEnd()}\nKEYCLOAK_PORT=${port}\n`;
163
+ }
164
+ if (/^KEYCLOAK_INTERNAL_SERVER_URL\s*=/m.test(next)) {
165
+ return next.replace(
166
+ /^KEYCLOAK_INTERNAL_SERVER_URL\s*=.*$/m,
167
+ `KEYCLOAK_INTERNAL_SERVER_URL=${internalUrl}`
168
+ );
169
+ }
170
+ return `${next.trimEnd()}\nKEYCLOAK_INTERNAL_SERVER_URL=${internalUrl}\n`;
171
+ }
172
+
173
+ /**
174
+ * For host `.env.local`, force Keycloak admin/internal base to this developer's
175
+ * published loopback port (`base + developerId × 100`). Never hardcodes a single
176
+ * developer’s port (e.g. 8182).
177
+ *
178
+ * @param {string} content - Env file content
179
+ * @returns {string}
180
+ */
181
+ function applyHostLocalKeycloakInternalUrl(content) {
182
+ if (typeof content !== 'string' || content.length === 0) {
183
+ return content;
184
+ }
185
+ const endpoint = resolveHostKeycloakLoopbackEndpoint(content);
186
+ if (!endpoint) {
187
+ return content;
188
+ }
189
+ return patchKeycloakHostInternalLines(content, endpoint.host, endpoint.port);
190
+ }
191
+
192
+ module.exports = {
193
+ applyHostLocalKeycloakInternalUrl,
194
+ readEnvLineValue,
195
+ resolveHostKeycloakLoopbackEndpoint
196
+ };
@@ -21,6 +21,7 @@ const { rewriteInfraEndpoints } = require('./env-endpoints');
21
21
  const { buildEnvVarMap } = require('./env-map');
22
22
  const { interpolateEnvVars } = require('./secrets-helpers');
23
23
  const { getLocalPort } = require('./port-resolver');
24
+ const { applyHostLocalKeycloakInternalUrl } = require('./env-copy-keycloak');
24
25
 
25
26
  /**
26
27
  * Read developer ID from config file synchronously
@@ -98,6 +99,26 @@ function resolveEnvOutputPath(rawOutputPath, variablesPath) {
98
99
  return outputPath;
99
100
  }
100
101
 
102
+ /**
103
+ * Sibling host-only env path next to `build.envOutputPath` (`.env` → `.env.local`).
104
+ * Used for host CLI / manual tests; must not be mounted into the container.
105
+ *
106
+ * @param {string} envOutputPath - Resolved path ending in `.env`
107
+ * @returns {string} Path ending in `.env.local`
108
+ */
109
+ function resolveEnvLocalOutputPath(envOutputPath) {
110
+ if (typeof envOutputPath !== 'string' || envOutputPath.trim() === '') {
111
+ throw new Error('envOutputPath is required to resolve .env.local');
112
+ }
113
+ if (envOutputPath.endsWith('.env.local')) {
114
+ return envOutputPath;
115
+ }
116
+ if (envOutputPath.endsWith('.env')) {
117
+ return `${envOutputPath}.local`;
118
+ }
119
+ return path.join(path.dirname(envOutputPath), '.env.local');
120
+ }
121
+
101
122
  /**
102
123
  * Writes .env to envOutputPath for `--reload`: merge container `.env.run` into the host file.
103
124
  * Preserves comments when a file already exists (e.g. after `aifabrix resolve`). Keys present only
@@ -149,8 +170,18 @@ async function writeEnvOutputForLocal(appName, outputPath) {
149
170
  const localMap = parseEnvContentToMap(localContent);
150
171
  toWrite = mergeEnvMapIntoContent(existingContent, localMap);
151
172
  }
173
+ const isLocalSibling = path.basename(outputPath) === '.env.local';
174
+ if (isLocalSibling) {
175
+ toWrite = applyHostLocalKeycloakInternalUrl(toWrite);
176
+ }
152
177
  await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
153
- logger.log(formatSuccessLine(`Wrote .env to envOutputPath (localPort): ${outputPath}`));
178
+ logger.log(
179
+ formatSuccessLine(
180
+ isLocalSibling
181
+ ? `Wrote host .env.local (localhost ports for CLI/tests): ${outputPath}`
182
+ : `Wrote .env to envOutputPath (localPort): ${outputPath}`
183
+ )
184
+ );
154
185
  }
155
186
 
156
187
  /**
@@ -408,6 +439,8 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath,
408
439
  module.exports = {
409
440
  processEnvVariables,
410
441
  resolveEnvOutputPath,
442
+ resolveEnvLocalOutputPath,
443
+ applyHostLocalKeycloakInternalUrl,
411
444
  substituteMntDataForLocal,
412
445
  syncWrittenBuilderEnvToOutputPath,
413
446
  writeEnvOutputForReload,
@@ -248,6 +248,9 @@ function formatGatePillarLine(opts = {}) {
248
248
  if (verdict === VERDICT.FAILED) {
249
249
  return formatBlockingError(`${label} · FAILED${suffix}`);
250
250
  }
251
+ if (verdict === VERDICT.NOT_APPLICABLE) {
252
+ return formatSuccessLine(`${label} · NOT APPLICABLE${suffix}`);
253
+ }
251
254
  return formatWarningLine(`${label} · NOT VERIFIED${suffix}`);
252
255
  }
253
256
 
@@ -79,6 +79,7 @@ const CATEGORIES = [
79
79
  { name: 'dimension value', term: 'dimension value <command>' },
80
80
  { name: 'resource-type' },
81
81
  { name: 'role' },
82
+ { name: 'role-assistant' },
82
83
  { name: 'evidence-kind' },
83
84
  { name: 'enterprise-knowledge' },
84
85
  { name: 'identity' },
@@ -382,15 +382,41 @@ function getIntegrationBuilderBaseDir() {
382
382
  }
383
383
 
384
384
  /**
385
- * Returns the default integration root under {@link getAppsMaterializationParent} (listing may also
386
- * scan {@link getCwdIntegrationRoot}; see {@link listIntegrationAppNames}).
385
+ * Returns the default integration root: {@link getCwdIntegrationRoot} when present, else
386
+ * under {@link getAppsMaterializationParent}. Listing may still scan both trees
387
+ * (see {@link listIntegrationAppNames}).
387
388
  *
388
389
  * @returns {string} Absolute path to integration/ directory
389
390
  */
390
391
  function getIntegrationRoot() {
392
+ const cwdRoot = getCwdIntegrationRoot();
393
+ if (cwdRoot) {
394
+ return cwdRoot;
395
+ }
391
396
  return path.join(getAppsMaterializationParent(), 'integration');
392
397
  }
393
398
 
399
+ /**
400
+ * True when path exists and is a directory (best-effort; false on error).
401
+ * @param {string} dirPath
402
+ * @returns {boolean}
403
+ */
404
+ function isExistingDirectoryPath(dirPath) {
405
+ if (!dirPath || typeof dirPath !== 'string') {
406
+ return false;
407
+ }
408
+ try {
409
+ const disk = nodeFs();
410
+ if (!disk.existsSync(dirPath)) {
411
+ return false;
412
+ }
413
+ const st = disk.statSync(dirPath);
414
+ return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+
394
420
  /**
395
421
  * Absolute `integration/` next to {@link process.cwd} when that directory exists.
396
422
  *
@@ -574,8 +600,10 @@ function listIntegrationAppNames() {
574
600
  if (cwdRoot) {
575
601
  addBuilderSubdirNamesToSet(disk, cwdRoot, names, withAppConfig(cwdRoot));
576
602
  }
577
- const matInt = getIntegrationRoot();
578
- addBuilderSubdirNamesToSet(disk, matInt, names, withAppConfig(matInt));
603
+ const matInt = path.join(getAppsMaterializationParent(), 'integration');
604
+ if (!cwdRoot || path.resolve(matInt) !== path.resolve(cwdRoot)) {
605
+ addBuilderSubdirNamesToSet(disk, matInt, names, withAppConfig(matInt));
606
+ }
579
607
  return [...names].sort();
580
608
  }
581
609
 
@@ -629,8 +657,11 @@ function listBuilderAppNames() {
629
657
  }
630
658
 
631
659
  /**
632
- * Gets the integration folder path: **`cwd/integration/<appName>`** when that directory exists, else
633
- * **`aifabrix-work` or `aifabrix-home` + `/integration/<appName>`**.
660
+ * Gets the integration folder path.
661
+ * Resolution: existing `cwd/integration/<app>` → existing materialization copy →
662
+ * `cwd/integration/<app>` when `cwd/integration/` exists (new download/create) →
663
+ * materialization parent. Download should use {@link getAppCreatePath} when it must
664
+ * always land under cwd even if a materialization orphan exists.
634
665
  *
635
666
  * @param {string} appName - Application name
636
667
  * @returns {string} Absolute path to integration directory
@@ -648,18 +679,21 @@ function getIntegrationPath(appName) {
648
679
  } catch {
649
680
  // programmatic module optional for CLI-only usage
650
681
  }
651
- const cwdInt = path.join(path.resolve(process.cwd()), 'integration', appName);
652
- try {
653
- if (nodeFs().existsSync(cwdInt)) {
654
- const st = nodeFs().statSync(cwdInt);
655
- if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
656
- return cwdInt;
657
- }
682
+ const cwdRoot = getCwdIntegrationRoot();
683
+ const matApp = path.join(getAppsMaterializationParent(), 'integration', appName);
684
+ if (cwdRoot) {
685
+ const cwdApp = path.join(cwdRoot, appName);
686
+ if (isExistingDirectoryPath(cwdApp)) {
687
+ return cwdApp;
658
688
  }
659
- } catch {
660
- // ignore
661
689
  }
662
- return path.join(getAppsMaterializationParent(), 'integration', appName);
690
+ if (isExistingDirectoryPath(matApp)) {
691
+ return matApp;
692
+ }
693
+ if (cwdRoot) {
694
+ return path.join(cwdRoot, appName);
695
+ }
696
+ return matApp;
663
697
  }
664
698
 
665
699
  /**
@@ -797,13 +831,15 @@ function resolveExistingWorkspacePath(appName, options) {
797
831
  if (isExistingAppDirectory(appPath)) {
798
832
  return { appPath, baseDir };
799
833
  }
800
- const altWorkspace =
801
- baseDir === 'enterprise-knowledge' ? 'integration' : 'enterprise-knowledge';
802
- const alt = resolveAppWorkspace(appName, { ...options, workspace: altWorkspace });
803
- if (!isExistingAppDirectory(alt.appPath)) {
804
- return null;
834
+ const alternates = ['integration', 'enterprise-knowledge', 'role-assistant']
835
+ .filter((workspaceName) => workspaceName !== baseDir);
836
+ for (const workspaceName of alternates) {
837
+ const alt = resolveAppWorkspace(appName, { ...options, workspace: workspaceName });
838
+ if (isExistingAppDirectory(alt.appPath)) {
839
+ return { appPath: alt.appPath, baseDir: alt.baseDir };
840
+ }
805
841
  }
806
- return { appPath: alt.appPath, baseDir: alt.baseDir };
842
+ return null;
807
843
  }
808
844
 
809
845
  function externalWorkspaceFolderFromConfig(appPath, baseDir, scopedWorkspace) {
@@ -926,7 +962,7 @@ async function detectAppType(appName, options = {}) {
926
962
  const builderAfterTemplate = checkBuilderFolder(appName);
927
963
  if (builderAfterTemplate) return builderAfterTemplate;
928
964
  throw new Error(
929
- `App '${appName}' not found in integration/${appName}, enterprise-knowledge/${appName}, or builder/${appName}`
965
+ `App '${appName}' not found in integration/${appName}, enterprise-knowledge/${appName}, role-assistant/${appName}, or builder/${appName}`
930
966
  );
931
967
  }
932
968
 
@@ -964,6 +1000,11 @@ async function getResolveAppPath(appName, options = {}) {
964
1000
  if (workspaceHit) {
965
1001
  return workspaceHit;
966
1002
  }
1003
+ const { getRoleAssistantPath } = require('./role-assistant-paths');
1004
+ const roleAssistantHit = tryResolveEnvOnlyPath(getRoleAssistantPath(appName));
1005
+ if (roleAssistantHit) {
1006
+ return roleAssistantHit;
1007
+ }
967
1008
  const integrationHit = tryResolveEnvOnlyPath(getIntegrationPath(appName));
968
1009
  if (integrationHit) {
969
1010
  return integrationHit;
@@ -1073,6 +1114,9 @@ module.exports = {
1073
1114
  getEnterpriseKnowledgePath: require('./enterprise-knowledge-paths').getEnterpriseKnowledgePath,
1074
1115
  getEnterpriseKnowledgeRoot: require('./enterprise-knowledge-paths').getEnterpriseKnowledgeRoot,
1075
1116
  listEnterpriseKnowledgeAnchors: require('./enterprise-knowledge-paths').listEnterpriseKnowledgeAnchors,
1117
+ getRoleAssistantPath: require('./role-assistant-paths').getRoleAssistantPath,
1118
+ getRoleAssistantRoot: require('./role-assistant-paths').getRoleAssistantRoot,
1119
+ getCwdRoleAssistantRoot: require('./role-assistant-paths').getCwdRoleAssistantRoot,
1076
1120
  resolveAppWorkspace: require('./app-path-resolve').resolveAppWorkspace,
1077
1121
  resolveExternalSystemPath: require('./app-path-resolve').resolveExternalSystemPath
1078
1122
  };
@@ -57,10 +57,22 @@ async function resolveClientTokenAuthForPipelineRetry(systemKey) {
57
57
  * @param {Object} authConfig
58
58
  * @param {Object} payload
59
59
  * @param {string} systemKey - External system key for credential lookup on retry
60
+ * @param {Object} [options] - Forwarded to uploadApplicationViaPipeline (e.g. timeoutMs)
60
61
  * @returns {Promise<Object>}
61
62
  */
62
- async function uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfig, payload, systemKey) {
63
- const first = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
63
+ async function uploadApplicationViaPipelineWithAuthRetry(
64
+ dataplaneUrl,
65
+ authConfig,
66
+ payload,
67
+ systemKey,
68
+ options = {}
69
+ ) {
70
+ const first = await uploadApplicationViaPipeline(
71
+ dataplaneUrl,
72
+ authConfig,
73
+ payload,
74
+ options
75
+ );
64
76
  if (!isPipelineAuthFailure(first) || authConfig.type !== 'bearer') {
65
77
  return first;
66
78
  }
@@ -77,7 +89,7 @@ async function uploadApplicationViaPipelineWithAuthRetry(dataplaneUrl, authConfi
77
89
  'Device login token was rejected for pipeline publish; retrying with application client token…'
78
90
  )
79
91
  );
80
- return uploadApplicationViaPipeline(dataplaneUrl, clientAuth, payload);
92
+ return uploadApplicationViaPipeline(dataplaneUrl, clientAuth, payload, options);
81
93
  }
82
94
 
83
95
  module.exports = {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Role Assistant workspace path helpers.
3
+ *
4
+ * @fileoverview Paths under role-assistant/ for Role Assistant packages
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const { nodeFs } = require('../internal/node-fs');
14
+
15
+ const WORKSPACE_ROLE_ASSISTANT = 'role-assistant';
16
+ const SYSTEM_KIND_DIGITAL_WORKER = 'digitalWorker';
17
+
18
+ function getMaterializationParent() {
19
+ const work = process.env.AIFABRIX_WORK;
20
+ if (work && typeof work === 'string' && work.trim()) {
21
+ return path.resolve(work.trim());
22
+ }
23
+ if (process.env.AIFABRIX_HOME && typeof process.env.AIFABRIX_HOME === 'string') {
24
+ return path.resolve(process.env.AIFABRIX_HOME.trim());
25
+ }
26
+ return path.join(os.homedir(), '.aifabrix');
27
+ }
28
+
29
+ function getCwdRoleAssistantRoot() {
30
+ const p = path.join(path.resolve(process.cwd()), WORKSPACE_ROLE_ASSISTANT);
31
+ try {
32
+ const disk = nodeFs();
33
+ if (disk.existsSync(p)) {
34
+ const st = disk.statSync(p);
35
+ if (st && typeof st.isDirectory === 'function' && st.isDirectory()) {
36
+ return p;
37
+ }
38
+ }
39
+ } catch {
40
+ // ignore
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function getRoleAssistantRoot() {
46
+ const cwdRoot = getCwdRoleAssistantRoot();
47
+ if (cwdRoot) {
48
+ return cwdRoot;
49
+ }
50
+ return path.join(getMaterializationParent(), WORKSPACE_ROLE_ASSISTANT);
51
+ }
52
+
53
+ function isExistingDirectory(disk, dirPath) {
54
+ try {
55
+ if (!dirPath || !disk.existsSync(dirPath)) {
56
+ return false;
57
+ }
58
+ const st = disk.statSync(dirPath);
59
+ return Boolean(st && typeof st.isDirectory === 'function' && st.isDirectory());
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Resolves role-assistant/<roleKey> with cwd-first behavior.
67
+ * @param {string} roleKey
68
+ * @returns {string}
69
+ */
70
+ function getRoleAssistantPath(roleKey) {
71
+ if (!roleKey || typeof roleKey !== 'string') {
72
+ throw new Error('Role key is required and must be a string');
73
+ }
74
+ const disk = nodeFs();
75
+ const cwdRoot = getCwdRoleAssistantRoot();
76
+ const matPath = path.join(getMaterializationParent(), WORKSPACE_ROLE_ASSISTANT, roleKey);
77
+ if (cwdRoot) {
78
+ const cwdPath = path.join(cwdRoot, roleKey);
79
+ if (isExistingDirectory(disk, cwdPath)) {
80
+ return cwdPath;
81
+ }
82
+ }
83
+ if (isExistingDirectory(disk, matPath)) {
84
+ return matPath;
85
+ }
86
+ if (cwdRoot) {
87
+ return path.join(cwdRoot, roleKey);
88
+ }
89
+ return matPath;
90
+ }
91
+
92
+ module.exports = {
93
+ WORKSPACE_ROLE_ASSISTANT,
94
+ SYSTEM_KIND_DIGITAL_WORKER,
95
+ getCwdRoleAssistantRoot,
96
+ getRoleAssistantRoot,
97
+ getRoleAssistantPath
98
+ };
@@ -18,6 +18,7 @@ const addFormats = require('ajv-formats');
18
18
  // These are reset when module is reloaded (for testing)
19
19
  let externalSystemValidator = null;
20
20
  let externalDataSourceValidator = null;
21
+ let roleAssistantSettingsValidator = null;
21
22
 
22
23
  /**
23
24
  * Reset validators cache (for testing)
@@ -26,6 +27,7 @@ let externalDataSourceValidator = null;
26
27
  function resetValidators() {
27
28
  externalSystemValidator = null;
28
29
  externalDataSourceValidator = null;
30
+ roleAssistantSettingsValidator = null;
29
31
  }
30
32
 
31
33
  /**
@@ -138,6 +140,38 @@ function loadExternalDataSourceSchema() {
138
140
  return externalDataSourceValidator;
139
141
  }
140
142
 
143
+ /**
144
+ * Loads and compiles role-assistant settings schema.
145
+ * @function loadRoleAssistantSettingsSchema
146
+ * @returns {Function}
147
+ */
148
+ function loadRoleAssistantSettingsSchema() {
149
+ if (roleAssistantSettingsValidator) {
150
+ return roleAssistantSettingsValidator;
151
+ }
152
+
153
+ const schemaPath = path.join(__dirname, '..', 'schema', 'role-assistant-settings.schema.json');
154
+ let schema;
155
+ try {
156
+ const schemaContent = fsRealSync.readFileSync(schemaPath, 'utf8');
157
+ schema = JSON.parse(schemaContent);
158
+ } catch (error) {
159
+ try {
160
+ schema = require('../schema/role-assistant-settings.schema.json');
161
+ } catch {
162
+ if (error && error.code === 'ENOENT') {
163
+ throw new Error(`Role assistant settings schema not found: ${schemaPath}`);
164
+ }
165
+ throw new Error(`Invalid JSON in role-assistant-settings.schema.json: ${error.message}`);
166
+ }
167
+ }
168
+
169
+ const ajv = new Ajv({ allErrors: true, strict: false, strictSchema: false });
170
+ addFormats(ajv);
171
+ roleAssistantSettingsValidator = ajv.compile(schema);
172
+ return roleAssistantSettingsValidator;
173
+ }
174
+
141
175
  /**
142
176
  * Detect schema type from $id field
143
177
  * @param {Object} parsed - Parsed JSON object
@@ -157,6 +191,9 @@ function detectFromId(parsed) {
157
191
  if (parsed.$id.includes('application-schema')) {
158
192
  return 'application';
159
193
  }
194
+ if (parsed.$id.includes('role-assistant-settings')) {
195
+ return 'role-assistant-settings';
196
+ }
160
197
 
161
198
  return null;
162
199
  }
@@ -239,6 +276,9 @@ function detectFromApplicationFields(parsed) {
239
276
  */
240
277
  function detectFromFilename(filePath) {
241
278
  const fileName = path.basename(filePath).toLowerCase();
279
+ if (fileName === 'role-assistant.yaml' || fileName === 'role-assistant.yml') {
280
+ return 'role-assistant-settings';
281
+ }
242
282
  if (fileName.includes('external-system') || fileName.includes('external_system')) {
243
283
  return 'external-system';
244
284
  }
@@ -347,6 +387,7 @@ function detectSchemaType(filePath, content) {
347
387
  module.exports = {
348
388
  loadExternalSystemSchema,
349
389
  loadExternalDataSourceSchema,
390
+ loadRoleAssistantSettingsSchema,
350
391
  registerExternalDatasourceAjvDependencies,
351
392
  detectSchemaType,
352
393
  detectSchemaTypeFromParsed,