@aifabrix/builder 2.51.1 → 2.52.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.
- package/lib/app/run-container-start.js +4 -0
- package/lib/app/run-deploy-job.js +135 -27
- package/lib/app/run-env-compose.js +32 -0
- package/lib/app/run-env-recovery.js +35 -0
- package/lib/app/run-helpers.js +5 -6
- package/lib/app/run.js +11 -3
- package/lib/channels/approval-guides/chatgpt.js +12 -9
- package/lib/channels/channel-targets.js +18 -1
- package/lib/channels/chatgpt-runtime-contract.js +1 -1
- package/lib/cli/infra-guided-footers.js +1 -1
- package/lib/cli/infra-guided-public-url-warn.js +55 -0
- package/lib/cli/infra-guided.js +65 -56
- package/lib/cli/setup-app.help.js +4 -0
- package/lib/cli/setup-app.test-commands.js +59 -28
- package/lib/cli/setup-external-system.js +50 -12
- package/lib/commands/channel-add-setup.js +8 -2
- package/lib/commands/channel.js +16 -5
- package/lib/commands/deploy-gate-orchestrator.js +3 -2
- package/lib/commands/governance-verify-external.js +2 -5
- package/lib/commands/lifecycle-external.js +14 -7
- package/lib/commands/role-assistant.js +66 -8
- package/lib/commands/test-governance-external.js +2 -5
- package/lib/commands/up-miso.js +61 -24
- package/lib/commands/upload-data-sync-finalize.js +20 -3
- package/lib/commands/verify-governance-external.js +2 -5
- package/lib/commands/verify-operations-external.js +2 -5
- package/lib/commands/verify-operations-step-plan.js +31 -0
- package/lib/commands/verify-trust-command-action.js +2 -5
- package/lib/datasource/datasource-identifier.js +4 -3
- package/lib/datasource/integration-context.js +3 -3
- package/lib/datasource/resolve-app.js +90 -8
- package/lib/datasource/test-e2e.js +2 -2
- package/lib/datasource/unified-validation-run-body.js +15 -1
- package/lib/datasource/unified-validation-run-resolve.js +2 -2
- package/lib/external-system/deploy-helpers.js +9 -5
- package/lib/external-system/deploy.js +2 -1
- package/lib/external-system/test-system-level.js +21 -3
- package/lib/external-system/test.js +4 -4
- package/lib/generator/builders.js +9 -15
- package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
- package/lib/role-assistant/knowledge-sync.js +25 -5
- package/lib/role-assistant/test-runner-workhub-answers.js +44 -26
- package/lib/role-assistant/test-runner-workhub.js +21 -7
- package/lib/schema/application-schema.json +10 -28
- package/lib/schema/infra.parameter.yaml +0 -20
- package/lib/utils/certify-external-workspace.js +40 -0
- package/lib/utils/docker-run-ephemeral.js +77 -17
- package/lib/utils/health-check-public-warn.js +31 -13
- package/lib/utils/health-check.js +11 -1
- package/lib/utils/paths.js +13 -15
- package/lib/utils/role-assistant-paths.js +76 -14
- package/lib/utils/upload-sync-options.js +7 -4
- package/lib/validation/reserved-system-key-prefixes.js +56 -0
- package/lib/validation/validate.js +54 -19
- package/package.json +1 -1
- package/templates/applications/builder-api/application.yaml +3 -3
- package/templates/applications/dataplane/application.yaml +3 -4
- package/templates/applications/dataplane/env.template +2 -2
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/env.template +5 -2
- package/templates/applications/miso-controller/application.yaml +3 -3
- package/templates/applications/miso-controller/env.template +16 -3
- package/templates/channels/chatgpt/install.template.md.hbs +75 -37
- package/templates/channels/chatgpt/integration/README.md +3 -2
- package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
- package/templates/channels/slack/install.template.md.hbs +6 -1
- package/templates/python/docker-compose.hbs +10 -0
- package/templates/typescript/docker-compose.hbs +10 -0
|
@@ -27,24 +27,18 @@ function sanitizeAuthType(authType) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Historically (incorrectly) stripped all non-DOCKER_REGISTRY_* vars when
|
|
31
|
+
* registryMode was "external", which wiped platform app env/portal config on
|
|
32
|
+
* `aifabrix json`. External only means image pull source — keep full configuration.
|
|
33
|
+
* DOCKER_REGISTRY_SERVER_* may still appear when present in env.template.
|
|
34
|
+
*
|
|
32
35
|
* @function filterConfigurationByRegistryMode
|
|
33
36
|
* @param {Array} configuration - Environment configuration
|
|
34
|
-
* @param {string}
|
|
35
|
-
* @returns {Array}
|
|
37
|
+
* @param {string} _registryMode - Registry mode ('external' or 'acr'); unused
|
|
38
|
+
* @returns {Array} Configuration unchanged
|
|
36
39
|
*/
|
|
37
|
-
function filterConfigurationByRegistryMode(configuration,
|
|
38
|
-
|
|
39
|
-
return configuration;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const allowedDockerRegistryVars = [
|
|
43
|
-
'DOCKER_REGISTRY_SERVER_URL',
|
|
44
|
-
'DOCKER_REGISTRY_SERVER_USERNAME',
|
|
45
|
-
'DOCKER_REGISTRY_SERVER_PASSWORD'
|
|
46
|
-
];
|
|
47
|
-
return configuration.filter(config => allowedDockerRegistryVars.includes(config.name));
|
|
40
|
+
function filterConfigurationByRegistryMode(configuration, _registryMode) {
|
|
41
|
+
return Array.isArray(configuration) ? configuration : [];
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
function buildImageReference(variables) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse optional YAML frontmatter from Role Assistant knowledge markdown.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview knowledge-markdown-frontmatter
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
|
|
13
|
+
const SEARCH_META_KEYS = [
|
|
14
|
+
'aspect',
|
|
15
|
+
'knowledgeType',
|
|
16
|
+
'knowledgeKey',
|
|
17
|
+
'title',
|
|
18
|
+
'resourceType',
|
|
19
|
+
'roleKey'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Split markdown into optional YAML frontmatter and body.
|
|
24
|
+
* @param {string} content
|
|
25
|
+
* @returns {{ frontmatter: Object|null, body: string }}
|
|
26
|
+
*/
|
|
27
|
+
function splitMarkdownFrontmatter(content) {
|
|
28
|
+
const text = typeof content === 'string' ? content : '';
|
|
29
|
+
if (!text.startsWith('---')) {
|
|
30
|
+
return { frontmatter: null, body: text };
|
|
31
|
+
}
|
|
32
|
+
const end = text.indexOf('\n---', 3);
|
|
33
|
+
if (end < 0) {
|
|
34
|
+
return { frontmatter: null, body: text };
|
|
35
|
+
}
|
|
36
|
+
const raw = text.slice(3, end).trim();
|
|
37
|
+
const body = text.slice(end + 4).replace(/^(?:\r?\n)+/, '');
|
|
38
|
+
if (!raw) {
|
|
39
|
+
return { frontmatter: {}, body };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const parsed = yaml.load(raw);
|
|
43
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
44
|
+
return { frontmatter: null, body: text };
|
|
45
|
+
}
|
|
46
|
+
return { frontmatter: parsed, body };
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
return { frontmatter: null, body: text };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Project searchable metadata fields from frontmatter.
|
|
54
|
+
* @param {Object|null|undefined} frontmatter
|
|
55
|
+
* @returns {Object}
|
|
56
|
+
*/
|
|
57
|
+
function knowledgeSearchMetadataFromFrontmatter(frontmatter) {
|
|
58
|
+
const out = {};
|
|
59
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
for (const key of SEARCH_META_KEYS) {
|
|
63
|
+
const value = frontmatter[key];
|
|
64
|
+
if (typeof value === 'string' && value.trim()) {
|
|
65
|
+
out[key] = value.trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
SEARCH_META_KEYS,
|
|
73
|
+
splitMarkdownFrontmatter,
|
|
74
|
+
knowledgeSearchMetadataFromFrontmatter
|
|
75
|
+
};
|
|
@@ -28,6 +28,10 @@ const {
|
|
|
28
28
|
resolveDocumentStorageKeyFromRoleAssistantSettings,
|
|
29
29
|
resolveRoleAssistantEnvironmentSettings
|
|
30
30
|
} = require('./settings');
|
|
31
|
+
const {
|
|
32
|
+
knowledgeSearchMetadataFromFrontmatter,
|
|
33
|
+
splitMarkdownFrontmatter
|
|
34
|
+
} = require('./knowledge-markdown-frontmatter');
|
|
31
35
|
|
|
32
36
|
function resolveSystemNode(input) {
|
|
33
37
|
if (!input || typeof input !== 'object') {
|
|
@@ -108,6 +112,15 @@ async function pushRoleAssistantDocumentStorageSettings(params) {
|
|
|
108
112
|
documentStorageKey: documentStorageKey || null
|
|
109
113
|
};
|
|
110
114
|
}
|
|
115
|
+
if (documentStorageKey === '') {
|
|
116
|
+
// No Knowledge binding in role-assistant.yaml — skip Assistants settings write.
|
|
117
|
+
// Draft / not-yet-catalogued digital workers would 404 on PATCH otherwise.
|
|
118
|
+
return {
|
|
119
|
+
skipped: true,
|
|
120
|
+
reason: 'no-document-storage-key',
|
|
121
|
+
documentStorageKey: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
111
124
|
if (!dataplaneUrl || !authConfig) {
|
|
112
125
|
return { skipped: true, reason: 'missing-dataplane-auth' };
|
|
113
126
|
}
|
|
@@ -182,20 +195,27 @@ function normalizeDocumentExternalId(knowledgeRoot, filePath) {
|
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
function buildDocumentRecord(knowledgeRoot, filePath) {
|
|
185
|
-
const
|
|
198
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
199
|
+
const { frontmatter, body } = splitMarkdownFrontmatter(raw);
|
|
200
|
+
const searchMeta = knowledgeSearchMetadataFromFrontmatter(frontmatter);
|
|
186
201
|
const externalId = normalizeDocumentExternalId(knowledgeRoot, filePath);
|
|
187
|
-
const title =
|
|
202
|
+
const title =
|
|
203
|
+
(typeof searchMeta.title === 'string' && searchMeta.title) || path.basename(externalId);
|
|
204
|
+
const content = typeof body === 'string' ? body : raw;
|
|
188
205
|
const contentHash = crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
189
206
|
const metadata = {
|
|
190
207
|
externalId,
|
|
191
|
-
title,
|
|
192
208
|
mimeType: 'text/markdown',
|
|
193
209
|
sourcePath: path.relative(knowledgeRoot, filePath).replace(/\\/g, '/'),
|
|
194
|
-
sourceType: 'role-assistant-markdown'
|
|
210
|
+
sourceType: 'role-assistant-markdown',
|
|
211
|
+
...searchMeta,
|
|
212
|
+
title
|
|
195
213
|
};
|
|
196
214
|
return {
|
|
197
215
|
key: externalId,
|
|
198
|
-
|
|
216
|
+
// Prefer sourcePath (*.md) so binary storage uses a known markdown extension
|
|
217
|
+
// instead of inventing ".guide" from path-like keys or titles.
|
|
218
|
+
name: metadata.sourcePath || `${externalId}.md`,
|
|
199
219
|
contentHash,
|
|
200
220
|
metadata,
|
|
201
221
|
binaryBase64: Buffer.from(content, 'utf8').toString('base64'),
|
|
@@ -88,40 +88,58 @@ function fillFieldsFromHints(fields, hints) {
|
|
|
88
88
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function
|
|
91
|
+
function _synthContactFields(hints) {
|
|
92
|
+
const out = {};
|
|
93
|
+
if (hints.email) {
|
|
94
|
+
out.email = hints.email;
|
|
95
|
+
}
|
|
96
|
+
if (hints.name || hints.email) {
|
|
97
|
+
out.firstname = String(hints.email || 'user').split('@')[0];
|
|
98
|
+
out.lastname = hints.name || 'Contact';
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _synthCustomerFields(hints) {
|
|
104
|
+
const out = {};
|
|
105
|
+
if (hints.name) {
|
|
106
|
+
out.name = hints.name;
|
|
107
|
+
}
|
|
108
|
+
if (hints.domain) {
|
|
109
|
+
out.domain = hints.domain;
|
|
110
|
+
}
|
|
111
|
+
if (hints.address) {
|
|
112
|
+
out.address = hints.address;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _synthDefaultFields(hints) {
|
|
92
118
|
const out = {};
|
|
119
|
+
if (hints.name) {
|
|
120
|
+
out.name = hints.name;
|
|
121
|
+
}
|
|
122
|
+
if (hints.email) {
|
|
123
|
+
out.email = hints.email;
|
|
124
|
+
}
|
|
125
|
+
if (hints.domain) {
|
|
126
|
+
out.domain = hints.domain;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function synthesizeFromCapability(capability, hints) {
|
|
132
|
+
let out = {};
|
|
93
133
|
if (capability.includes('contact')) {
|
|
94
|
-
|
|
95
|
-
out.email = hints.email;
|
|
96
|
-
}
|
|
97
|
-
if (hints.name || hints.email) {
|
|
98
|
-
out.firstname = String(hints.email || 'user').split('@')[0];
|
|
99
|
-
out.lastname = hints.name || 'Contact';
|
|
100
|
-
}
|
|
134
|
+
out = _synthContactFields(hints);
|
|
101
135
|
} else if (capability.includes('deal')) {
|
|
102
136
|
if (hints.name) {
|
|
103
137
|
out.dealname = hints.name;
|
|
104
138
|
}
|
|
105
139
|
} else if (capability.includes('customer') || capability.includes('compan')) {
|
|
106
|
-
|
|
107
|
-
out.name = hints.name;
|
|
108
|
-
}
|
|
109
|
-
if (hints.domain) {
|
|
110
|
-
out.domain = hints.domain;
|
|
111
|
-
}
|
|
112
|
-
if (hints.address) {
|
|
113
|
-
out.address = hints.address;
|
|
114
|
-
}
|
|
140
|
+
out = _synthCustomerFields(hints);
|
|
115
141
|
} else {
|
|
116
|
-
|
|
117
|
-
out.name = hints.name;
|
|
118
|
-
}
|
|
119
|
-
if (hints.email) {
|
|
120
|
-
out.email = hints.email;
|
|
121
|
-
}
|
|
122
|
-
if (hints.domain) {
|
|
123
|
-
out.domain = hints.domain;
|
|
124
|
-
}
|
|
142
|
+
out = _synthDefaultFields(hints);
|
|
125
143
|
}
|
|
126
144
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
127
145
|
}
|
|
@@ -180,23 +180,25 @@ async function createActivateAcceptPlan(dataplaneUrl, authConfig, planBody, assi
|
|
|
180
180
|
throw lastError || new Error('Work Hub plan create/activate/accept failed.');
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
function _approveGateDecision(ctx) {
|
|
184
184
|
if (ctx.approveStopped) {
|
|
185
|
-
return;
|
|
185
|
+
return 'skip';
|
|
186
186
|
}
|
|
187
187
|
const status = String(ctx.status || '').trim().toLowerCase();
|
|
188
188
|
if (status !== 'pendingapproval' && status !== 'pending_approval') {
|
|
189
|
-
return;
|
|
189
|
+
return 'skip';
|
|
190
190
|
}
|
|
191
191
|
const approvalState = String(ctx.taskRow?.approvalState || '').trim().toLowerCase();
|
|
192
192
|
if (approvalState && approvalState !== 'pending') {
|
|
193
|
-
|
|
194
|
-
return;
|
|
193
|
+
return 'stop';
|
|
195
194
|
}
|
|
196
195
|
if ((Number(ctx.approveAttempts) || 0) >= 8) {
|
|
197
|
-
|
|
198
|
-
return;
|
|
196
|
+
return 'stop';
|
|
199
197
|
}
|
|
198
|
+
return 'run';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function _attemptApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
|
|
200
202
|
try {
|
|
201
203
|
await approveWorkTask(dataplaneUrl, authConfig, taskId, {});
|
|
202
204
|
ctx.interactions.push({ type: 'approve', taskId, attempt: ctx.approveAttempts });
|
|
@@ -211,6 +213,18 @@ async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
|
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
|
|
217
|
+
const gate = _approveGateDecision(ctx);
|
|
218
|
+
if (gate === 'skip') {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (gate === 'stop') {
|
|
222
|
+
ctx.approveStopped = true;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await _attemptApproveTask(dataplaneUrl, authConfig, taskId, ctx);
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
async function maybeRespondWaiting(dataplaneUrl, authConfig, taskRow, testCase, interactions) {
|
|
215
229
|
const peId = extractPeFromTask(taskRow);
|
|
216
230
|
if (!peId) {
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"key": "application-schema",
|
|
8
8
|
"name": "Application Configuration Schema",
|
|
9
9
|
"description": "JSON schema for validating AI Fabrix application configuration files",
|
|
10
|
-
"version": "1.6.
|
|
10
|
+
"version": "1.6.1",
|
|
11
11
|
"type": "schema",
|
|
12
12
|
"category": "infrastructure",
|
|
13
13
|
"author": "AI Fabrix Team",
|
|
14
14
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
15
|
-
"updatedAt": "2026-
|
|
15
|
+
"updatedAt": "2026-08-03T00:00:00Z",
|
|
16
16
|
"compatibility": {
|
|
17
17
|
"minVersion": "1.0.0",
|
|
18
18
|
"maxVersion": "2.0.0",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"external-datasource.schema.json"
|
|
31
31
|
],
|
|
32
32
|
"changelog": [
|
|
33
|
+
{
|
|
34
|
+
"version": "1.6.1",
|
|
35
|
+
"date": "2026-08-03T00:00:00Z",
|
|
36
|
+
"changes": [
|
|
37
|
+
"registryMode external no longer restricts configuration item names to DOCKER_REGISTRY_SERVER_*; external is image pull source only and full env/portal configuration remains valid."
|
|
38
|
+
],
|
|
39
|
+
"breaking": false
|
|
40
|
+
},
|
|
33
41
|
{
|
|
34
42
|
"version": "1.6.0",
|
|
35
43
|
"date": "2026-07-28T00:00:00Z",
|
|
@@ -974,32 +982,6 @@
|
|
|
974
982
|
"databases"
|
|
975
983
|
]
|
|
976
984
|
}
|
|
977
|
-
},
|
|
978
|
-
{
|
|
979
|
-
"if": {
|
|
980
|
-
"properties": {
|
|
981
|
-
"registryMode": {
|
|
982
|
-
"const": "external"
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
},
|
|
986
|
-
"then": {
|
|
987
|
-
"properties": {
|
|
988
|
-
"configuration": {
|
|
989
|
-
"items": {
|
|
990
|
-
"properties": {
|
|
991
|
-
"name": {
|
|
992
|
-
"enum": [
|
|
993
|
-
"DOCKER_REGISTRY_SERVER_URL",
|
|
994
|
-
"DOCKER_REGISTRY_SERVER_USERNAME",
|
|
995
|
-
"DOCKER_REGISTRY_SERVER_PASSWORD"
|
|
996
|
-
]
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
985
|
}
|
|
1004
986
|
]
|
|
1005
987
|
}
|
|
@@ -340,26 +340,6 @@ parameters:
|
|
|
340
340
|
vaultSecretName: dataplane-client-secretKeyVault
|
|
341
341
|
notes: Per-app OAuth client secret for dataplane; generated on first ensure.
|
|
342
342
|
|
|
343
|
-
- key: mori-controller-client-idKeyVault
|
|
344
|
-
scope: app
|
|
345
|
-
generator:
|
|
346
|
-
type: literal
|
|
347
|
-
value: 'miso-controller-dev-mori-controller'
|
|
348
|
-
ensureOn: [upInfra, resolveApp]
|
|
349
|
-
azure:
|
|
350
|
-
notes: >-
|
|
351
|
-
Per-app OAuth client id from controller registration; literal default matches
|
|
352
|
-
{controllerId}-{environmentKey}-{applicationKey} (override per environment).
|
|
353
|
-
|
|
354
|
-
- key: mori-controller-client-secretKeyVault
|
|
355
|
-
scope: app
|
|
356
|
-
generator:
|
|
357
|
-
type: randomBytes32
|
|
358
|
-
ensureOn: [upInfra, resolveApp]
|
|
359
|
-
azure:
|
|
360
|
-
vaultSecretName: mori-controller-client-secretKeyVault
|
|
361
|
-
notes: Per-app OAuth client secret for mori-controller; generated on first ensure.
|
|
362
|
-
|
|
363
343
|
# Mori / telemetry: optional locally (LICENSE_JWT via miso-controller-jwt-licenseKeyVault=DEVELOPMENT skips Mori).
|
|
364
344
|
# MORI_AUTH_METHOD=kv://mori-controller-auth-methodKeyVault — local default apiKey.
|
|
365
345
|
- key: mori-controller-auth-methodKeyVault
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Workspace roots allowed for external certify / verify CLI commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const CERTIFY_EXTERNAL_BASE_DIRS = new Set([
|
|
8
|
+
'integration',
|
|
9
|
+
'role-assistant',
|
|
10
|
+
'enterprise-knowledge'
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string|null|undefined} baseDir
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isCertifyExternalWorkspace(baseDir) {
|
|
18
|
+
return CERTIFY_EXTERNAL_BASE_DIRS.has(baseDir);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string|null|undefined} baseDir
|
|
23
|
+
* @param {string} commandLabel - e.g. verify-operations
|
|
24
|
+
* @returns {void}
|
|
25
|
+
* @throws {Error}
|
|
26
|
+
*/
|
|
27
|
+
function assertCertifyExternalWorkspace(baseDir, commandLabel) {
|
|
28
|
+
if (!isCertifyExternalWorkspace(baseDir)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${commandLabel} applies to external authoring folders only ` +
|
|
31
|
+
'(integration/<systemKey>/, role-assistant/<roleKey>/, or enterprise-knowledge/<key>/).'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
CERTIFY_EXTERNAL_BASE_DIRS,
|
|
38
|
+
isCertifyExternalWorkspace,
|
|
39
|
+
assertCertifyExternalWorkspace
|
|
40
|
+
};
|
|
@@ -16,8 +16,12 @@ const { getDockerExecEnv } = require('./remote-docker-env');
|
|
|
16
16
|
* @param {string} fullImage - Image:tag
|
|
17
17
|
* @param {string} cmd - Shell command (passed to sh -c)
|
|
18
18
|
* @param {string|null} [envFilePath] - Path for docker run --env-file
|
|
19
|
-
* @param {{
|
|
20
|
-
*
|
|
19
|
+
* @param {{
|
|
20
|
+
* network?: string|null,
|
|
21
|
+
* volumeMounts?: Array<{ host: string, container: string }>,
|
|
22
|
+
* quiet?: boolean
|
|
23
|
+
* }} [options] - quiet=true pipes stdio (installer-friendly; dump on failure)
|
|
24
|
+
* @returns {Promise<number|{ code: number, output: string }>} Exit code, or code+output when quiet
|
|
21
25
|
*/
|
|
22
26
|
async function runDockerRunEphemeral(fullImage, cmd, envFilePath = null, options = {}) {
|
|
23
27
|
const dockerEnv = await getDockerExecEnv();
|
|
@@ -26,22 +30,48 @@ async function runDockerRunEphemeral(fullImage, cmd, envFilePath = null, options
|
|
|
26
30
|
? options.network.trim()
|
|
27
31
|
: null;
|
|
28
32
|
const volumeMounts = Array.isArray(options?.volumeMounts) ? options.volumeMounts : [];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const quiet = options?.quiet === true;
|
|
34
|
+
const args = _buildDockerRunArgs(fullImage, cmd, envFilePath, network, volumeMounts);
|
|
35
|
+
if (quiet) {
|
|
36
|
+
return _spawnDockerQuiet(args, dockerEnv);
|
|
37
|
+
}
|
|
38
|
+
return _spawnDockerInherit(args, dockerEnv);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} fullImage
|
|
43
|
+
* @param {string} cmd
|
|
44
|
+
* @param {string|null} envFilePath
|
|
45
|
+
* @param {string|null} network
|
|
46
|
+
* @param {Array<{ host: string, container: string }>} volumeMounts
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
function _buildDockerRunArgs(fullImage, cmd, envFilePath, network, volumeMounts) {
|
|
50
|
+
const args = ['run', '--rm'];
|
|
51
|
+
if (network) {
|
|
52
|
+
args.push('--network', network);
|
|
53
|
+
}
|
|
54
|
+
if (envFilePath) {
|
|
55
|
+
args.push('--env-file', envFilePath);
|
|
56
|
+
}
|
|
57
|
+
for (const mount of volumeMounts) {
|
|
58
|
+
const host = typeof mount?.host === 'string' ? mount.host.trim() : '';
|
|
59
|
+
const container = typeof mount?.container === 'string' ? mount.container.trim() : '';
|
|
60
|
+
if (host && container) {
|
|
61
|
+
args.push('-v', `${host}:${container}`);
|
|
43
62
|
}
|
|
44
|
-
|
|
63
|
+
}
|
|
64
|
+
args.push(fullImage, 'sh', '-c', cmd);
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string[]} args
|
|
70
|
+
* @param {NodeJS.ProcessEnv} dockerEnv
|
|
71
|
+
* @returns {Promise<number>}
|
|
72
|
+
*/
|
|
73
|
+
function _spawnDockerInherit(args, dockerEnv) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
45
75
|
const proc = spawn('docker', args, {
|
|
46
76
|
stdio: 'inherit',
|
|
47
77
|
shell: false,
|
|
@@ -52,4 +82,34 @@ async function runDockerRunEphemeral(fullImage, cmd, envFilePath = null, options
|
|
|
52
82
|
});
|
|
53
83
|
}
|
|
54
84
|
|
|
85
|
+
/**
|
|
86
|
+
* @param {string[]} args
|
|
87
|
+
* @param {NodeJS.ProcessEnv} dockerEnv
|
|
88
|
+
* @returns {Promise<{ code: number, output: string }>}
|
|
89
|
+
*/
|
|
90
|
+
function _spawnDockerQuiet(args, dockerEnv) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
const proc = spawn('docker', args, {
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
shell: false,
|
|
95
|
+
env: dockerEnv
|
|
96
|
+
});
|
|
97
|
+
const chunks = [];
|
|
98
|
+
const push = (buf) => {
|
|
99
|
+
if (buf && buf.length) {
|
|
100
|
+
chunks.push(buf);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
proc.stdout.on('data', push);
|
|
104
|
+
proc.stderr.on('data', push);
|
|
105
|
+
proc.on('close', (code) => {
|
|
106
|
+
resolve({
|
|
107
|
+
code: code !== null ? code : 1,
|
|
108
|
+
output: Buffer.concat(chunks).toString('utf8')
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
proc.on('error', () => resolve({ code: 1, output: '' }));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
55
115
|
module.exports = { runDockerRunEphemeral };
|
|
@@ -38,32 +38,50 @@ async function filterTraefikUrlByDns(traefikUrl, debug, isHostnameResolvableFn)
|
|
|
38
38
|
* @param {string} [p.skippedPublicHealthUrl]
|
|
39
39
|
* @param {string[]} [p.urlsToTry]
|
|
40
40
|
* @param {number} p.resolvedIndex
|
|
41
|
+
* @returns {string|null} Warning message without leading glyph, or null
|
|
41
42
|
*/
|
|
42
|
-
function
|
|
43
|
+
function buildPublicHealthUrlWarningMessage(p) {
|
|
43
44
|
const { skippedPublicHealthUrl, urlsToTry, resolvedIndex } = p;
|
|
44
|
-
if (typeof resolvedIndex !== 'number' || resolvedIndex < 0) return;
|
|
45
|
+
if (typeof resolvedIndex !== 'number' || resolvedIndex < 0) return null;
|
|
45
46
|
|
|
46
47
|
if (skippedPublicHealthUrl) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'The application reported healthy via localhost only. Validate DNS names and Traefik routing for this host.'
|
|
51
|
-
)
|
|
48
|
+
return (
|
|
49
|
+
`Public URL was not verified (DNS): ${skippedPublicHealthUrl}. ` +
|
|
50
|
+
'The application reported healthy via localhost only. Validate DNS names and Traefik routing for this host.'
|
|
52
51
|
);
|
|
53
|
-
return;
|
|
54
52
|
}
|
|
55
53
|
if (Array.isArray(urlsToTry) && urlsToTry.length > 1 && resolvedIndex > 0) {
|
|
56
54
|
const pub = urlsToTry[0];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'Health checks succeeded via localhost only. Validate Traefik routing, TLS, and DNS.'
|
|
61
|
-
)
|
|
55
|
+
return (
|
|
56
|
+
`Public URL was not verified: ${pub}. ` +
|
|
57
|
+
'Health checks succeeded via localhost only. Validate Traefik routing, TLS, and DNS.'
|
|
62
58
|
);
|
|
63
59
|
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {Object} p
|
|
65
|
+
* @param {string} [p.skippedPublicHealthUrl]
|
|
66
|
+
* @param {string[]} [p.urlsToTry]
|
|
67
|
+
* @param {number} p.resolvedIndex
|
|
68
|
+
* @param {boolean} [p.defer]
|
|
69
|
+
* @param {(message: string) => void} [p.onDeferred]
|
|
70
|
+
*/
|
|
71
|
+
function logPublicHealthUrlWarningIfNeeded(p) {
|
|
72
|
+
const message = buildPublicHealthUrlWarningMessage(p);
|
|
73
|
+
if (!message) return;
|
|
74
|
+
if (p && p.defer === true) {
|
|
75
|
+
if (typeof p.onDeferred === 'function') {
|
|
76
|
+
p.onDeferred(message);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
logger.log(chalk.yellow(`⚠ ${message}`));
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
module.exports = {
|
|
67
84
|
filterTraefikUrlByDns,
|
|
85
|
+
buildPublicHealthUrlWarningMessage,
|
|
68
86
|
logPublicHealthUrlWarningIfNeeded
|
|
69
87
|
};
|
|
@@ -339,6 +339,14 @@ function resolveHealthCheckPollOptions(runOptions = {}) {
|
|
|
339
339
|
if (typeof runOptions.onPollText === 'function') {
|
|
340
340
|
pollOptions.onPollText = runOptions.onPollText;
|
|
341
341
|
}
|
|
342
|
+
const defer =
|
|
343
|
+
runOptions.deferPublicUrlWarning === true || typeof runOptions.onPollText === 'function';
|
|
344
|
+
if (defer) {
|
|
345
|
+
pollOptions.deferPublicUrlWarning = true;
|
|
346
|
+
if (typeof runOptions.onDeferredPublicUrlWarning === 'function') {
|
|
347
|
+
pollOptions.onDeferredPublicUrlWarning = runOptions.onDeferredPublicUrlWarning;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
342
350
|
return pollOptions;
|
|
343
351
|
}
|
|
344
352
|
|
|
@@ -367,7 +375,9 @@ async function pollUntilHealthCheckPasses({
|
|
|
367
375
|
logPublicHealthUrlWarningIfNeeded({
|
|
368
376
|
skippedPublicHealthUrl,
|
|
369
377
|
urlsToTry,
|
|
370
|
-
resolvedIndex: attemptResult.resolvedIndex
|
|
378
|
+
resolvedIndex: attemptResult.resolvedIndex,
|
|
379
|
+
defer: pollOptions.deferPublicUrlWarning === true,
|
|
380
|
+
onDeferred: pollOptions.onDeferredPublicUrlWarning
|
|
371
381
|
});
|
|
372
382
|
return;
|
|
373
383
|
}
|