@aifabrix/builder 2.52.0 → 2.52.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.
- package/jest.isolated-projects.js +25 -2
- package/jest.projects.js +4 -1
- package/lib/api/datasources-extended.api.js +8 -8
- package/lib/api/keycloak.api.js +92 -0
- package/lib/api/token-providers.api.js +77 -0
- package/lib/api/types/keycloak.types.js +23 -0
- package/lib/api/types/token-providers.types.js +65 -0
- package/lib/app/down.js +16 -1
- package/lib/channels/add-channel.js +126 -19
- package/lib/channels/approval-guides/chatgpt.js +1 -1
- package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
- package/lib/channels/channel-add-ensure.js +128 -0
- package/lib/channels/channel-artifact-generator.js +11 -1
- package/lib/channels/channel-branding.js +94 -0
- package/lib/channels/channel-credential.js +8 -0
- package/lib/channels/channel-discovery.js +330 -0
- package/lib/channels/channel-external-system.js +1 -1
- package/lib/channels/channel-runtime-resolver.js +103 -14
- package/lib/channels/channel-status-display.js +7 -1
- package/lib/channels/microsoft-copilot-channel-add.js +138 -0
- package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
- package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
- package/lib/channels/microsoft-copilot-icons.js +74 -0
- package/lib/channels/microsoft-copilot-limits.js +84 -0
- package/lib/channels/microsoft-copilot-package.js +130 -49
- package/lib/channels/microsoft-copilot-validate.js +197 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/infra-guided.js +13 -6
- package/lib/cli/setup-infra.js +31 -4
- package/lib/commands/channel-add-setup.js +28 -9
- package/lib/commands/channel.js +14 -10
- package/lib/commands/integration-definition.js +158 -0
- package/lib/commands/repair-auth-apply.js +19 -5
- package/lib/commands/repair-datasource-sync-operations.js +4 -1
- package/lib/commands/role-assistant-test-output.js +200 -0
- package/lib/commands/role-assistant.js +125 -166
- package/lib/datasource/capability/capability-resolve.js +15 -5
- package/lib/datasource/capability/copy-operations.js +14 -4
- package/lib/infrastructure/index.js +19 -4
- package/lib/integration-definition/apply.js +478 -0
- package/lib/integration-definition/canonical-json.js +51 -0
- package/lib/integration-definition/emit-suite-bid.js +94 -0
- package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
- package/lib/integration-definition/enrich-openapi-operations.js +434 -0
- package/lib/integration-definition/from-discovery.js +339 -0
- package/lib/integration-definition/hash.js +37 -0
- package/lib/integration-definition/index.js +37 -0
- package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
- package/lib/integration-definition/resource-type-catalog.js +59 -0
- package/lib/integration-definition/system-kind-enum.js +40 -0
- package/lib/integration-definition/validate.js +148 -0
- package/lib/programmatic/builder-api-request.js +5 -1
- package/lib/programmatic/builder-help-governance.js +3 -2
- package/lib/programmatic/capability-registry.js +19 -0
- package/lib/programmatic/openapi-component-schemas.js +6 -1
- package/lib/programmatic/openapi-descriptions.js +40 -0
- package/lib/programmatic/openapi-envelope-examples.js +269 -0
- package/lib/programmatic/openapi-operation-bindings.js +45 -0
- package/lib/programmatic/openapi-request-schemas.js +14 -0
- package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
- package/lib/programmatic/openapi-result-schemas.js +46 -99
- package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
- package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
- package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
- package/lib/programmatic/openapi-schema-helpers.js +9 -9
- package/lib/programmatic/openapi-spec.js +11 -1
- package/lib/programmatic/orchestration-openapi-meta.js +10 -0
- package/lib/programmatic/route-handler-map.js +29 -1
- package/lib/programmatic/run-channel-package.js +236 -0
- package/lib/programmatic/run-integration-definition.js +69 -0
- package/lib/role-assistant/knowledge-sync-logging.js +58 -0
- package/lib/role-assistant/knowledge-sync.js +88 -93
- package/lib/role-assistant/test-cases-bid.js +155 -0
- package/lib/role-assistant/test-cases-evaluate.js +9 -1
- package/lib/role-assistant/test-cases-honesty.js +24 -10
- package/lib/role-assistant/test-cases.js +28 -0
- package/lib/role-assistant/test-runner-bid-apply.js +122 -0
- package/lib/role-assistant/test-runner-bid-document.js +278 -0
- package/lib/role-assistant/test-runner-workhub-answers.js +245 -20
- package/lib/role-assistant/test-runner-workhub.js +100 -18
- package/lib/role-assistant/test-suite.js +119 -0
- package/lib/schema/application-schema.json +56 -6
- package/lib/schema/external-datasource.schema.json +103 -17
- package/lib/schema/external-system.schema.json +54 -3
- package/lib/schema/integration-definition.schema.json +195 -0
- package/lib/utils/authentication-shape.js +6 -0
- package/lib/utils/docker-build.js +3 -3
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/promote-dev-docker-image.js +97 -18
- package/lib/utils/transient-developer-id.js +28 -0
- package/package.json +1 -1
- package/scripts/pnpm/af.mjs +24 -9
- package/scripts/pnpm/help.mjs +1 -1
- package/templates/applications/dataplane/application.yaml +3 -2
- package/templates/applications/dataplane/env.template +7 -6
- package/templates/applications/miso-controller/application.yaml +23 -9
- package/templates/applications/miso-controller/env.template +8 -2
- package/templates/channels/README.md +11 -1
- package/templates/channels/brand/color-192.png +0 -0
- package/templates/channels/brand/dark/128.png +0 -0
- package/templates/channels/brand/dark/192.png +0 -0
- package/templates/channels/brand/dark/256.png +0 -0
- package/templates/channels/brand/dark/32.png +0 -0
- package/templates/channels/brand/dark/512.png +0 -0
- package/templates/channels/brand/dark/64.png +0 -0
- package/templates/channels/brand/light/128.png +0 -0
- package/templates/channels/brand/light/192.png +0 -0
- package/templates/channels/brand/light/256.png +0 -0
- package/templates/channels/brand/light/32.png +0 -0
- package/templates/channels/brand/light/512.png +0 -0
- package/templates/channels/brand/light/64.png +0 -0
- package/templates/channels/brand/mono-white/128.png +0 -0
- package/templates/channels/brand/mono-white/192.png +0 -0
- package/templates/channels/brand/mono-white/256.png +0 -0
- package/templates/channels/brand/mono-white/32.png +0 -0
- package/templates/channels/brand/mono-white/512.png +0 -0
- package/templates/channels/brand/mono-white/64.png +0 -0
- package/templates/channels/brand/outline-32.png +0 -0
- package/templates/channels/chatgpt/install.template.md.hbs +6 -4
- package/templates/channels/chatgpt/integration/README.md +4 -5
- package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
- package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
- package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
- package/templates/channels/microsoft-copilot/integration/README.md +2 -3
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
- package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
- package/templates/channels/slack/integration/README.md +2 -4
- package/templates/channels/teams-bot/install.template.md.hbs +4 -0
- package/templates/channels/teams-bot/integration/README.md +3 -5
- package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
- /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Microsoft Copilot channel
|
|
2
|
+
* Microsoft Copilot channel install / approval steps.
|
|
3
3
|
*
|
|
4
|
-
* @fileoverview
|
|
4
|
+
* @fileoverview Teams custom-app upload guide aligned with install templates
|
|
5
5
|
* @author AI Fabrix Team
|
|
6
6
|
* @version 1.0.0
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
ENTRA_SSO_REGISTRATION_URL
|
|
13
|
+
} = require('../microsoft-copilot-entra-sso-form');
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @param {Object} ctx
|
|
@@ -16,16 +18,18 @@ const { formatCredentialPortalStep } = require('./credential-portal');
|
|
|
16
18
|
*/
|
|
17
19
|
function buildMicrosoftCopilotApprovalGuide(ctx) {
|
|
18
20
|
return {
|
|
19
|
-
portalUrl:
|
|
21
|
+
portalUrl: ENTRA_SSO_REGISTRATION_URL,
|
|
20
22
|
steps: [
|
|
21
|
-
'
|
|
22
|
-
'Upload
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
|
|
26
|
-
'
|
|
27
|
-
`
|
|
28
|
-
'
|
|
23
|
+
'Open Microsoft Teams with your target Microsoft 365 account.',
|
|
24
|
+
'Go to Apps → Manage your apps → Upload an app → Upload a custom app.',
|
|
25
|
+
'Upload only appPackage.<env>.zip (do not upload individual manifest files or channel-microsoft-copilot.json).',
|
|
26
|
+
'Review permissions, select Add, and open the installed app. Users stay on the Microsoft 365 Entra session — no separate AI Fabrix sign-in.',
|
|
27
|
+
'If prompted, complete Entra admin consent for the Runtime MCP delegated scope.',
|
|
28
|
+
'If Upload a custom app is unavailable, ask a Teams admin to enable custom app uploads or deploy the package centrally.',
|
|
29
|
+
`Register Entra SSO first at ${ENTRA_SSO_REGISTRATION_URL} using the Base URL / Client ID / Scope values printed by the CLI (or INSTALL.md). Pass the registration id as --oauth-plugin-vault-reference-id.`,
|
|
30
|
+
'Do not configure AI Fabrix OAuth client secrets under Connected Systems → Authentication for this Entra SSO flow.',
|
|
31
|
+
'Confirm Runtime MCP and governed tools using INSTALL.md, then re-run with --probe after Teams install.',
|
|
32
|
+
`After the app is available in Teams, re-run: ${ctx.resumeCommand}`
|
|
29
33
|
]
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure local scaffold + dataplane upload before channel add install.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview One-command channel add prelude (scaffold → upload if needed)
|
|
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 chalk = require('chalk');
|
|
14
|
+
const logger = require('../utils/logger');
|
|
15
|
+
const { formatSuccessLine, metadata } = require('../utils/cli-test-layout-chalk');
|
|
16
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
17
|
+
const { getExternalSystem } = require('../api/external-systems.api');
|
|
18
|
+
const { unwrapApiData } = require('../utils/external-system-readiness-core');
|
|
19
|
+
const { scaffoldChannelIntegration } = require('./channel-integration-generator');
|
|
20
|
+
const { resolveChannelAuthContext } = require('./channel-auth-context');
|
|
21
|
+
const { uploadExternalSystem } = require('../commands/upload');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} channelTarget
|
|
25
|
+
* @param {string} [projectRoot]
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function resolveIntegrationDir(channelTarget, projectRoot) {
|
|
29
|
+
if (projectRoot) {
|
|
30
|
+
return path.join(path.resolve(projectRoot), 'integration', channelTarget);
|
|
31
|
+
}
|
|
32
|
+
return getIntegrationPath(channelTarget);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} ctx
|
|
37
|
+
* @param {string} channelTarget
|
|
38
|
+
* @returns {Promise<boolean>}
|
|
39
|
+
*/
|
|
40
|
+
async function isCommunicationSystemUploaded(ctx, channelTarget) {
|
|
41
|
+
const res = await getExternalSystem(ctx.dataplaneUrl, channelTarget, ctx.authConfig);
|
|
42
|
+
const system = unwrapApiData(res);
|
|
43
|
+
return !!(system && typeof system === 'object');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scaffold integration/<target>/ when missing (does not overwrite).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} channelTarget
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @returns {Promise<{ scaffolded: boolean, integrationDir: string }>}
|
|
52
|
+
*/
|
|
53
|
+
async function ensureChannelScaffold(channelTarget, options = {}) {
|
|
54
|
+
const integrationDir = resolveIntegrationDir(channelTarget, options.projectRoot);
|
|
55
|
+
if (fs.existsSync(integrationDir)) {
|
|
56
|
+
return { scaffolded: false, integrationDir };
|
|
57
|
+
}
|
|
58
|
+
const result = await scaffoldChannelIntegration(channelTarget, {
|
|
59
|
+
projectRoot: options.projectRoot,
|
|
60
|
+
allowExperimental: options.allowExperimental === true
|
|
61
|
+
});
|
|
62
|
+
if (options.quiet !== true) {
|
|
63
|
+
logger.log(formatSuccessLine(`Scaffolded integration at ${result.integrationDir}`));
|
|
64
|
+
for (const file of result.files) {
|
|
65
|
+
logger.log(metadata(file));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { scaffolded: true, integrationDir: result.integrationDir };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Upload when missing on dataplane; with --force upload every time.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} channelTarget
|
|
75
|
+
* @param {Object} options
|
|
76
|
+
* @returns {Promise<{ uploaded: boolean, skipped: boolean, ctx: Object|null }>}
|
|
77
|
+
*/
|
|
78
|
+
async function ensureChannelUploaded(channelTarget, options = {}) {
|
|
79
|
+
if (options.dryRun === true) {
|
|
80
|
+
return { uploaded: false, skipped: true, ctx: null };
|
|
81
|
+
}
|
|
82
|
+
const ctx = await resolveChannelAuthContext(channelTarget, { silent: options.quiet === true });
|
|
83
|
+
const force = options.force === true;
|
|
84
|
+
const alreadyUploaded = force ? false : await isCommunicationSystemUploaded(ctx, channelTarget);
|
|
85
|
+
if (alreadyUploaded) {
|
|
86
|
+
if (options.quiet !== true) {
|
|
87
|
+
logger.log(chalk.gray(` Communication system "${channelTarget}" already on dataplane`));
|
|
88
|
+
}
|
|
89
|
+
return { uploaded: false, skipped: true, ctx };
|
|
90
|
+
}
|
|
91
|
+
if (options.quiet !== true) {
|
|
92
|
+
const reason = force ? 'force upload' : 'not found on dataplane';
|
|
93
|
+
logger.log(chalk.white(`Uploading ${channelTarget} (${reason})…`));
|
|
94
|
+
}
|
|
95
|
+
await uploadExternalSystem(channelTarget, {
|
|
96
|
+
force: true,
|
|
97
|
+
minimal: options.quiet === true || options.minimal !== false
|
|
98
|
+
});
|
|
99
|
+
if (options.quiet !== true) {
|
|
100
|
+
logger.log(formatSuccessLine(`Uploaded ${channelTarget}`));
|
|
101
|
+
}
|
|
102
|
+
return { uploaded: true, skipped: false, ctx };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scaffold if needed, then upload if missing (or always when --force).
|
|
107
|
+
*
|
|
108
|
+
* @param {string} channelTarget
|
|
109
|
+
* @param {Object} options
|
|
110
|
+
* @returns {Promise<{ scaffolded: boolean, uploaded: boolean, integrationDir: string }>}
|
|
111
|
+
*/
|
|
112
|
+
async function ensureChannelReadyForAdd(channelTarget, options = {}) {
|
|
113
|
+
const scaffold = await ensureChannelScaffold(channelTarget, options);
|
|
114
|
+
const upload = await ensureChannelUploaded(channelTarget, options);
|
|
115
|
+
return {
|
|
116
|
+
scaffolded: scaffold.scaffolded,
|
|
117
|
+
uploaded: upload.uploaded,
|
|
118
|
+
integrationDir: scaffold.integrationDir
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
resolveIntegrationDir,
|
|
124
|
+
isCommunicationSystemUploaded,
|
|
125
|
+
ensureChannelScaffold,
|
|
126
|
+
ensureChannelUploaded,
|
|
127
|
+
ensureChannelReadyForAdd
|
|
128
|
+
};
|
|
@@ -147,11 +147,21 @@ function generateChannelArtifact(params) {
|
|
|
147
147
|
params.dryRun === true
|
|
148
148
|
);
|
|
149
149
|
|
|
150
|
+
const installReady = params.runtimeValues?.installReady === true;
|
|
151
|
+
const blocker = params.runtimeValues?.discoveryBlocker || null;
|
|
150
152
|
return {
|
|
151
153
|
outputPath,
|
|
152
154
|
artifactHash,
|
|
153
155
|
content,
|
|
154
|
-
installPath: install?.installPath
|
|
156
|
+
installPath: install?.installPath,
|
|
157
|
+
zipPath: null,
|
|
158
|
+
links: {
|
|
159
|
+
readme: install?.installPath || null,
|
|
160
|
+
zip: null,
|
|
161
|
+
filesRoot: outputDir
|
|
162
|
+
},
|
|
163
|
+
installReady,
|
|
164
|
+
blocker
|
|
155
165
|
};
|
|
156
166
|
}
|
|
157
167
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional channel package branding in Builder workspace (plan 167.0).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Read/write channel-branding.json under integration/<systemKey>/
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('../internal/fs-real-sync');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const BRANDING_FILENAME = 'channel-branding.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} workspaceSystemDir
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function brandingFilePath(workspaceSystemDir) {
|
|
21
|
+
return path.join(workspaceSystemDir, BRANDING_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} workspaceSystemDir
|
|
26
|
+
* @returns {{ displayName?: string, description?: string, iconColorPath?: string, iconOutlinePath?: string }}
|
|
27
|
+
*/
|
|
28
|
+
function readChannelBranding(workspaceSystemDir) {
|
|
29
|
+
const filePath = brandingFilePath(workspaceSystemDir);
|
|
30
|
+
if (!fs.existsSync(filePath)) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
34
|
+
if (!raw || typeof raw !== 'object') {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
/** @type {{ displayName?: string, description?: string, iconColorPath?: string, iconOutlinePath?: string }} */
|
|
38
|
+
const out = {};
|
|
39
|
+
if (typeof raw.displayName === 'string' && raw.displayName.trim()) {
|
|
40
|
+
out.displayName = raw.displayName.trim();
|
|
41
|
+
}
|
|
42
|
+
if (typeof raw.description === 'string' && raw.description.trim()) {
|
|
43
|
+
out.description = raw.description.trim();
|
|
44
|
+
}
|
|
45
|
+
if (typeof raw.iconColorPath === 'string' && raw.iconColorPath.trim()) {
|
|
46
|
+
out.iconColorPath = raw.iconColorPath.trim();
|
|
47
|
+
}
|
|
48
|
+
if (typeof raw.iconOutlinePath === 'string' && raw.iconOutlinePath.trim()) {
|
|
49
|
+
out.iconOutlinePath = raw.iconOutlinePath.trim();
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} workspaceSystemDir
|
|
56
|
+
* @param {Object} branding
|
|
57
|
+
* @returns {Object}
|
|
58
|
+
*/
|
|
59
|
+
function writeChannelBranding(workspaceSystemDir, branding) {
|
|
60
|
+
fs.mkdirSync(workspaceSystemDir, { recursive: true });
|
|
61
|
+
const next = {
|
|
62
|
+
...readChannelBranding(workspaceSystemDir),
|
|
63
|
+
...(branding && typeof branding === 'object' ? branding : {})
|
|
64
|
+
};
|
|
65
|
+
const cleaned = {};
|
|
66
|
+
for (const key of ['displayName', 'description', 'iconColorPath', 'iconOutlinePath']) {
|
|
67
|
+
if (typeof next[key] === 'string' && next[key].trim()) {
|
|
68
|
+
cleaned[key] = next[key].trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(brandingFilePath(workspaceSystemDir), `${JSON.stringify(cleaned, null, 2)}\n`, 'utf8');
|
|
72
|
+
return cleaned;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Merge CLI/API overrides over workspace branding.
|
|
77
|
+
* @param {Object} [workspaceBranding]
|
|
78
|
+
* @param {Object} [overrides]
|
|
79
|
+
* @returns {Object}
|
|
80
|
+
*/
|
|
81
|
+
function mergeChannelBranding(workspaceBranding, overrides) {
|
|
82
|
+
return {
|
|
83
|
+
...(workspaceBranding && typeof workspaceBranding === 'object' ? workspaceBranding : {}),
|
|
84
|
+
...(overrides && typeof overrides === 'object' ? overrides : {})
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
BRANDING_FILENAME,
|
|
90
|
+
brandingFilePath,
|
|
91
|
+
readChannelBranding,
|
|
92
|
+
writeChannelBranding,
|
|
93
|
+
mergeChannelBranding
|
|
94
|
+
};
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
auditCredentialUpdated,
|
|
19
19
|
auditFailed
|
|
20
20
|
} = require('./channel-audit');
|
|
21
|
+
const { skipsChannelOauthCredential } = require('./microsoft-copilot-entra-sso');
|
|
21
22
|
|
|
22
23
|
/** @type {Record<string, { method: string, displayName: string, credentialConfig: Object }>} */
|
|
23
24
|
const CREDENTIAL_SPECS = {
|
|
@@ -171,6 +172,13 @@ async function pushIntegrationSecrets(ctx, channelTarget) {
|
|
|
171
172
|
*/
|
|
172
173
|
async function ensureChannelCredential(params) {
|
|
173
174
|
const { ctx, channelTarget, target, force = false } = params;
|
|
175
|
+
if (skipsChannelOauthCredential(channelTarget)) {
|
|
176
|
+
return {
|
|
177
|
+
skipped: true,
|
|
178
|
+
reason: 'entraSsoTrustedIssuer',
|
|
179
|
+
credentialKey: target.credentialKey
|
|
180
|
+
};
|
|
181
|
+
}
|
|
174
182
|
const existing = await findCredentialByKey(ctx, target.credentialKey);
|
|
175
183
|
if (existing) {
|
|
176
184
|
auditCredentialUpdated(channelTarget, {
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External AI Discovery mapping for channel packaging (plan 167.0).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Pure discovery → package context; optional Dataplane fetch
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { ApiClient } = require('../api');
|
|
12
|
+
const { RUNTIME_ORCHESTRATION_SYSTEM_KEY } = require('./chatgpt-runtime-contract');
|
|
13
|
+
const { normalizeBaseUrl, joinUrl } = require('./channel-runtime-resolver');
|
|
14
|
+
|
|
15
|
+
const GATEWAY_TOOLS_CATALOG_MARKER = '/api/v1/mcp/servers/tools';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} pathOrUrl
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isGatewayToolsCatalogUrl(pathOrUrl) {
|
|
22
|
+
const value = String(pathOrUrl || '');
|
|
23
|
+
return value.includes(GATEWAY_TOOLS_CATALOG_MARKER);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize discovery paths that include a public `/data` prefix.
|
|
28
|
+
* @param {string} dataplaneBaseUrl
|
|
29
|
+
* @param {string|null|undefined} pathOrUrl
|
|
30
|
+
* @returns {string|null}
|
|
31
|
+
*/
|
|
32
|
+
function resolveDiscoveryUrl(dataplaneBaseUrl, pathOrUrl) {
|
|
33
|
+
if (!pathOrUrl || typeof pathOrUrl !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const trimmed = pathOrUrl.trim();
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (isGatewayToolsCatalogUrl(trimmed)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Gateway tools catalog must not be used as MCP server URL for channel packages'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
46
|
+
return trimmed;
|
|
47
|
+
}
|
|
48
|
+
let relative = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
49
|
+
if (relative.startsWith('/data/api/')) {
|
|
50
|
+
relative = relative.slice('/data'.length);
|
|
51
|
+
}
|
|
52
|
+
return joinUrl(normalizeBaseUrl(dataplaneBaseUrl), relative);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} discovery
|
|
57
|
+
* @param {string} key
|
|
58
|
+
* @returns {Object|null}
|
|
59
|
+
*/
|
|
60
|
+
function findEntryPoint(discovery, key) {
|
|
61
|
+
const entries = Array.isArray(discovery?.entryPoints) ? discovery.entryPoints : [];
|
|
62
|
+
return entries.find((ep) => ep && ep.key === key) || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {Object} discovery
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function extractGovernedToolNames(discovery) {
|
|
70
|
+
const fromRuntime = discovery?.runtimeDiscovery?.governedToolNames;
|
|
71
|
+
if (Array.isArray(fromRuntime) && fromRuntime.length > 0) {
|
|
72
|
+
return fromRuntime.map((name) => String(name)).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
const tools = Array.isArray(discovery?.tools) ? discovery.tools : [];
|
|
75
|
+
return tools.map((tool) => (tool && tool.name ? String(tool.name) : '')).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Object} discovery
|
|
80
|
+
* @param {Object|null} adapter
|
|
81
|
+
* @param {string} dataplaneBaseUrl
|
|
82
|
+
* @returns {{ mcpServerUrl: string|null, installReady: boolean, blocker: string|null }}
|
|
83
|
+
*/
|
|
84
|
+
function resolveMcpConnection(discovery, adapter, dataplaneBaseUrl) {
|
|
85
|
+
const adapterUrl = adapter?.connection?.mcpServerUrl || null;
|
|
86
|
+
const remote = findEntryPoint(discovery, 'remoteMcp');
|
|
87
|
+
const fromEntry =
|
|
88
|
+
resolveDiscoveryUrl(dataplaneBaseUrl, remote?.absoluteUrl) ||
|
|
89
|
+
resolveDiscoveryUrl(dataplaneBaseUrl, remote?.path);
|
|
90
|
+
const mcpServerUrl = resolveDiscoveryUrl(dataplaneBaseUrl, adapterUrl) || fromEntry;
|
|
91
|
+
if (mcpServerUrl && isGatewayToolsCatalogUrl(mcpServerUrl)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'Gateway tools catalog must not be used as MCP server URL for channel packages'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const mcpReady = discovery?.publication?.mcpReady === true;
|
|
97
|
+
const connectable = adapter ? adapter.connectable === true : mcpReady;
|
|
98
|
+
const remoteAvailable = discovery?.transport?.remoteMcp?.status === 'available';
|
|
99
|
+
const installReady = Boolean(mcpReady && connectable && remoteAvailable && mcpServerUrl);
|
|
100
|
+
const blocker =
|
|
101
|
+
adapter?.blocker ||
|
|
102
|
+
(!installReady ? 'remoteMcpUnavailable' : null);
|
|
103
|
+
return { mcpServerUrl, installReady, blocker };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Object} branding
|
|
108
|
+
* @param {Object} identity
|
|
109
|
+
* @param {Object|null} adapter
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function resolvePackageDisplayName(branding, identity, adapter) {
|
|
113
|
+
if (typeof branding.displayName === 'string' && branding.displayName.trim()) {
|
|
114
|
+
return branding.displayName.trim();
|
|
115
|
+
}
|
|
116
|
+
return (
|
|
117
|
+
identity.displayName ||
|
|
118
|
+
adapter?.nameForHuman ||
|
|
119
|
+
adapter?.displayName ||
|
|
120
|
+
identity.systemKey ||
|
|
121
|
+
RUNTIME_ORCHESTRATION_SYSTEM_KEY
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Object} branding
|
|
127
|
+
* @param {Object} identity
|
|
128
|
+
* @param {Object|null} adapter
|
|
129
|
+
* @param {Object} discovery
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
function resolvePackageDescription(branding, identity, adapter, discovery) {
|
|
133
|
+
if (typeof branding.description === 'string' && branding.description.trim()) {
|
|
134
|
+
return branding.description.trim();
|
|
135
|
+
}
|
|
136
|
+
return (
|
|
137
|
+
identity.description ||
|
|
138
|
+
adapter?.descriptionForHuman ||
|
|
139
|
+
adapter?.description ||
|
|
140
|
+
discovery.operatingModel?.summary ||
|
|
141
|
+
''
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {Object|null} auth
|
|
147
|
+
* @returns {Object|null}
|
|
148
|
+
*/
|
|
149
|
+
function mapAdapterAuthentication(auth) {
|
|
150
|
+
if (!auth) return null;
|
|
151
|
+
return {
|
|
152
|
+
recommendedCapability: auth.recommendedCapability || null,
|
|
153
|
+
issuer: auth.issuer || null,
|
|
154
|
+
openidConfigurationUrl: auth.openidConfigurationUrl || null,
|
|
155
|
+
authorizationServerMetadataUrl: auth.authorizationServerMetadataUrl || null,
|
|
156
|
+
protectedResourceMetadataUrl: auth.protectedResourceMetadataUrl || null,
|
|
157
|
+
resource: auth.resource || auth.apiResource || null,
|
|
158
|
+
audience: auth.audience !== undefined && auth.audience !== null ? auth.audience : null,
|
|
159
|
+
scopes: Array.isArray(auth.scopes)
|
|
160
|
+
? [...auth.scopes]
|
|
161
|
+
: Array.isArray(auth.allowedScopes)
|
|
162
|
+
? [...auth.allowedScopes]
|
|
163
|
+
: [],
|
|
164
|
+
operatorNote: auth.operatorNote || null,
|
|
165
|
+
pkceRequired: auth.pkceRequired === true,
|
|
166
|
+
clientRegistrationModes: Array.isArray(auth.clientRegistrationModes)
|
|
167
|
+
? [...auth.clientRegistrationModes]
|
|
168
|
+
: []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {Object} branding
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
function isBrandingApplied(branding) {
|
|
177
|
+
return Boolean(
|
|
178
|
+
(branding.displayName && String(branding.displayName).trim()) ||
|
|
179
|
+
(branding.description && String(branding.description).trim())
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Map discovery (+ optional adapter) into packaging fields.
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} params
|
|
187
|
+
* @param {Object} params.discovery
|
|
188
|
+
* @param {Object} [params.adapter]
|
|
189
|
+
* @param {string} params.dataplaneBaseUrl
|
|
190
|
+
* @param {Object} [params.branding]
|
|
191
|
+
* @returns {Object}
|
|
192
|
+
*/
|
|
193
|
+
function mapDiscoveryToPackageContext(params) {
|
|
194
|
+
const discovery = params.discovery;
|
|
195
|
+
if (!discovery || typeof discovery !== 'object') {
|
|
196
|
+
throw new Error('discovery document is required');
|
|
197
|
+
}
|
|
198
|
+
const identity = discovery.identity || {};
|
|
199
|
+
const branding = params.branding && typeof params.branding === 'object' ? params.branding : {};
|
|
200
|
+
const adapter = params.adapter || null;
|
|
201
|
+
const connection = resolveMcpConnection(discovery, adapter, params.dataplaneBaseUrl);
|
|
202
|
+
const authCaps = Array.isArray(discovery.authenticationCapabilities)
|
|
203
|
+
? discovery.authenticationCapabilities.map((c) => c.capability || c).filter(Boolean)
|
|
204
|
+
: [];
|
|
205
|
+
return {
|
|
206
|
+
systemKey: identity.systemKey || RUNTIME_ORCHESTRATION_SYSTEM_KEY,
|
|
207
|
+
displayName: resolvePackageDisplayName(branding, identity, adapter),
|
|
208
|
+
description: resolvePackageDescription(branding, identity, adapter, discovery),
|
|
209
|
+
governedToolNames: extractGovernedToolNames(discovery),
|
|
210
|
+
mcpServerUrl: connection.mcpServerUrl,
|
|
211
|
+
installReady: connection.installReady,
|
|
212
|
+
blocker: connection.blocker,
|
|
213
|
+
authentication: mapAdapterAuthentication(adapter?.authentication || null),
|
|
214
|
+
authenticationCapabilities: authCaps,
|
|
215
|
+
publication: {
|
|
216
|
+
mcpReady: discovery.publication?.mcpReady === true,
|
|
217
|
+
environment: discovery.publication?.environment || null
|
|
218
|
+
},
|
|
219
|
+
brandingApplied: isBrandingApplied(branding)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {Object} response
|
|
225
|
+
* @returns {Object}
|
|
226
|
+
*/
|
|
227
|
+
function unwrapApiData(response) {
|
|
228
|
+
if (response && typeof response === 'object' && response.data && typeof response.data === 'object') {
|
|
229
|
+
if (response.data.kind || response.data.identity || response.data.adapter) {
|
|
230
|
+
return response.data;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return response;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @param {string} dataplaneBaseUrl
|
|
238
|
+
* @param {Object} authConfig
|
|
239
|
+
* @param {string} [systemKey]
|
|
240
|
+
* @returns {Promise<Object>}
|
|
241
|
+
*/
|
|
242
|
+
async function fetchExternalAiDiscovery(
|
|
243
|
+
dataplaneBaseUrl,
|
|
244
|
+
authConfig,
|
|
245
|
+
systemKey = RUNTIME_ORCHESTRATION_SYSTEM_KEY
|
|
246
|
+
) {
|
|
247
|
+
const client = new ApiClient(normalizeBaseUrl(dataplaneBaseUrl), authConfig || {});
|
|
248
|
+
const response = await client.get(`/api/v1/external/systems/${systemKey}/discovery`);
|
|
249
|
+
const discovery = unwrapApiData(response);
|
|
250
|
+
if (!discovery || !discovery.kind) {
|
|
251
|
+
throw new Error(`Discovery document missing for systemKey=${systemKey}`);
|
|
252
|
+
}
|
|
253
|
+
return discovery;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} dataplaneBaseUrl
|
|
258
|
+
* @param {Object} authConfig
|
|
259
|
+
* @param {string} adapterKey microsoft-plugin | chatgpt-setup
|
|
260
|
+
* @param {string} [systemKey]
|
|
261
|
+
* @returns {Promise<Object>}
|
|
262
|
+
*/
|
|
263
|
+
async function fetchExternalAiDiscoveryAdapter(
|
|
264
|
+
dataplaneBaseUrl,
|
|
265
|
+
authConfig,
|
|
266
|
+
adapterKey,
|
|
267
|
+
systemKey = RUNTIME_ORCHESTRATION_SYSTEM_KEY
|
|
268
|
+
) {
|
|
269
|
+
const client = new ApiClient(normalizeBaseUrl(dataplaneBaseUrl), authConfig || {});
|
|
270
|
+
const response = await client.get(
|
|
271
|
+
`/api/v1/external/systems/${systemKey}/discovery/adapters/${adapterKey}`
|
|
272
|
+
);
|
|
273
|
+
return unwrapApiData(response);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {string} channelTarget
|
|
278
|
+
* @returns {string|null}
|
|
279
|
+
*/
|
|
280
|
+
function adapterKeyForChannel(channelTarget) {
|
|
281
|
+
if (channelTarget === 'microsoft-copilot') return 'microsoft-plugin';
|
|
282
|
+
if (channelTarget === 'chatgpt') return 'chatgpt-setup';
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Fetch discovery (+ adapter when known) and map package context.
|
|
288
|
+
*
|
|
289
|
+
* @param {Object} params
|
|
290
|
+
* @returns {Promise<Object>}
|
|
291
|
+
*/
|
|
292
|
+
async function loadChannelPackageContext(params) {
|
|
293
|
+
const systemKey = params.runtimeSystemKey || RUNTIME_ORCHESTRATION_SYSTEM_KEY;
|
|
294
|
+
const discovery = await fetchExternalAiDiscovery(
|
|
295
|
+
params.dataplaneBaseUrl,
|
|
296
|
+
params.authConfig,
|
|
297
|
+
systemKey
|
|
298
|
+
);
|
|
299
|
+
const adapterKey = adapterKeyForChannel(params.channelTarget);
|
|
300
|
+
let adapter = null;
|
|
301
|
+
if (adapterKey) {
|
|
302
|
+
try {
|
|
303
|
+
adapter = await fetchExternalAiDiscoveryAdapter(
|
|
304
|
+
params.dataplaneBaseUrl,
|
|
305
|
+
params.authConfig,
|
|
306
|
+
adapterKey,
|
|
307
|
+
systemKey
|
|
308
|
+
);
|
|
309
|
+
} catch {
|
|
310
|
+
adapter = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return mapDiscoveryToPackageContext({
|
|
314
|
+
discovery,
|
|
315
|
+
adapter,
|
|
316
|
+
dataplaneBaseUrl: params.dataplaneBaseUrl,
|
|
317
|
+
branding: params.branding
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
GATEWAY_TOOLS_CATALOG_MARKER,
|
|
323
|
+
isGatewayToolsCatalogUrl,
|
|
324
|
+
resolveDiscoveryUrl,
|
|
325
|
+
mapDiscoveryToPackageContext,
|
|
326
|
+
fetchExternalAiDiscovery,
|
|
327
|
+
fetchExternalAiDiscoveryAdapter,
|
|
328
|
+
adapterKeyForChannel,
|
|
329
|
+
loadChannelPackageContext
|
|
330
|
+
};
|
|
@@ -72,7 +72,7 @@ async function fetchCommunicationSystem(ctx) {
|
|
|
72
72
|
if (!system || typeof system !== 'object') {
|
|
73
73
|
throw new Error(
|
|
74
74
|
`Communication external system "${ctx.channelTarget}" was not found. ` +
|
|
75
|
-
`Run: aifabrix
|
|
75
|
+
`Run: aifabrix channel add ${ctx.channelTarget} --name "…" --force`
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
const kind = system.systemKind || system.systemKindKey;
|