@aifabrix/builder 2.60.0 → 2.61.2
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/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +2 -2
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/repair-datasource-apply.js +2 -0
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-modes.js +1 -8
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +7 -4
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/secrets-env-content.js +5 -1
- package/lib/core/secrets-env-write.js +10 -3
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/generator/index.js +3 -0
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +388 -38
- package/lib/utils/compose-generate-docker-compose.js +7 -12
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/secrets-helpers.js +0 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +4 -3
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/miso-controller/application.yaml +74 -2
- package/templates/applications/miso-controller/env.template +42 -3
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/main.json +4 -4
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const fsSync = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const buildCopy = require('./build-copy');
|
|
14
|
+
const { prepareLocalDataMount } = require('./prepare-local-data-mount');
|
|
14
15
|
const paths = require('./paths');
|
|
15
16
|
const { isSystemBuilderAppName } = require('./paths-system-builder-keys');
|
|
16
17
|
const {
|
|
@@ -187,14 +188,14 @@ function resolveStorageMountSourceRoot(appName, buildContext) {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
|
-
* Host `mount
|
|
191
|
+
* Host `mount/data/<appName>/` directory bind-mounted to `/mnt/data` when storage is required.
|
|
191
192
|
*
|
|
192
193
|
* @param {object} ctx - Compose template context (`devMountPath`, `appName`, `appConfig`, `serviceConfig`)
|
|
193
194
|
* @returns {string|null}
|
|
194
195
|
*/
|
|
195
196
|
function resolveLocalDataMountPathForCompose(ctx) {
|
|
196
197
|
if (ctx.devMountPath && typeof ctx.devMountPath === 'string') {
|
|
197
|
-
return path.join(ctx.devMountPath, 'mount').replace(/\\/g, '/');
|
|
198
|
+
return path.join(ctx.devMountPath, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
|
|
198
199
|
}
|
|
199
200
|
const requiresStorage =
|
|
200
201
|
ctx.serviceConfig?.requiresStorage ||
|
|
@@ -212,7 +213,7 @@ function resolveLocalDataMountPathForCompose(ctx) {
|
|
|
212
213
|
if (!sourceRoot) {
|
|
213
214
|
return null;
|
|
214
215
|
}
|
|
215
|
-
return path.join(sourceRoot, 'mount').replace(/\\/g, '/');
|
|
216
|
+
return path.join(sourceRoot, 'mount', 'data', ctx.appName).replace(/\\/g, '/');
|
|
216
217
|
} catch {
|
|
217
218
|
return null;
|
|
218
219
|
}
|
|
@@ -229,12 +230,8 @@ function buildComposeTemplateContext(ctx) {
|
|
|
229
230
|
ctx.appConfig?.requires?.storage ||
|
|
230
231
|
ctx.appConfig?.services?.storage ||
|
|
231
232
|
false;
|
|
232
|
-
if (localDataMountPath && requiresStorage
|
|
233
|
-
|
|
234
|
-
fsSync.mkdirSync(localDataMountPath, { recursive: true });
|
|
235
|
-
} catch {
|
|
236
|
-
// Non-fatal: tests may use non-writable devMountPath; host must create mount/ before run.
|
|
237
|
-
}
|
|
233
|
+
if (localDataMountPath && requiresStorage) {
|
|
234
|
+
prepareLocalDataMount(localDataMountPath, ctx.envFileAbsolutePath);
|
|
238
235
|
}
|
|
239
236
|
return {
|
|
240
237
|
...ctx.serviceConfig,
|
|
@@ -253,9 +250,7 @@ function buildComposeTemplateContext(ctx) {
|
|
|
253
250
|
// An app that administers its own operating system — creating accounts, managing
|
|
254
251
|
// sshd — cannot work that way and declares `runAsRoot: true` to opt out.
|
|
255
252
|
runAsRoot: ctx.appConfig?.runAsRoot === true,
|
|
256
|
-
// Extra ports
|
|
257
|
-
// such as SSH beside HTTP. Each host port uses the main port's formula so developers
|
|
258
|
-
// on one host do not collide.
|
|
253
|
+
// Extra protocol ports use the main port offset formula to avoid developer collisions.
|
|
259
254
|
additionalPorts: (ctx.appConfig?.additionalPorts ?? []).map((entry) => ({
|
|
260
255
|
name: entry.name,
|
|
261
256
|
containerPort: entry.containerPort,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** @fileoverview Safe binary metadata and keyed document persistence evidence. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { keyedGetEvidence, hasExecutedKeyedGet } = require('./datasource-test-run-capability-scope');
|
|
5
|
+
const BINARY_FIELDS = new Set(['contentType', 'size', 'filename', 'sha256']);
|
|
6
|
+
|
|
7
|
+
/** @param {*} value @returns {Object} */
|
|
8
|
+
function binarySummary(value) {
|
|
9
|
+
if (!value || typeof value !== 'object') return {};
|
|
10
|
+
return Object.fromEntries(Object.entries(value).filter(([key, item]) =>
|
|
11
|
+
BINARY_FIELDS.has(key) && ['string', 'number'].includes(typeof item)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Limit binary containers to metadata before printing or saving a report.
|
|
16
|
+
* Ordinary JSON record fields remain intact.
|
|
17
|
+
* @param {*} value
|
|
18
|
+
* @param {WeakSet} seen
|
|
19
|
+
* @returns {*}
|
|
20
|
+
*/
|
|
21
|
+
function sanitizeBinaryEvidence(value, seen = new WeakSet()) {
|
|
22
|
+
if (!value || typeof value !== 'object') return value;
|
|
23
|
+
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) return '[binary omitted]';
|
|
24
|
+
if (seen.has(value)) return '[Circular]';
|
|
25
|
+
seen.add(value);
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const items = value.map(item => sanitizeBinaryEvidence(item, seen));
|
|
28
|
+
seen.delete(value);
|
|
29
|
+
return items;
|
|
30
|
+
}
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, item] of Object.entries(value)) {
|
|
33
|
+
if (['getBinary', 'listBinary', '_binary'].includes(key)) out[key] = binarySummary(item);
|
|
34
|
+
else if (value.keyedGet === true && key === 'document') {
|
|
35
|
+
out[key] = { ...binarySummary(item) };
|
|
36
|
+
if (typeof item?.key === 'string') out[key].key = item.key;
|
|
37
|
+
if (typeof item?.persisted === 'boolean') out[key].persisted = item.persisted;
|
|
38
|
+
} else if (value.keyedGet === true && ['data', 'bytes', 'body', 'content', 'base64'].includes(key)) continue;
|
|
39
|
+
else out[key] = sanitizeBinaryEvidence(item, seen);
|
|
40
|
+
}
|
|
41
|
+
seen.delete(value);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {Object} value @param {string[]} lines */
|
|
46
|
+
function appendBinaryAndPersistence(value, lines) {
|
|
47
|
+
if (!value || typeof value !== 'object') return;
|
|
48
|
+
if (value.getBinary) lines.push(` getBinary: ${JSON.stringify(binarySummary(value.getBinary))}`);
|
|
49
|
+
if (value.scope?.expectedSource === 'get') {
|
|
50
|
+
const observed = value.observed || {};
|
|
51
|
+
lines.push(` Document persistence (expectedSource: get): documents=${Number(observed.documents) || 0}, chunks=${Number(observed.chunks) || 0}`);
|
|
52
|
+
}
|
|
53
|
+
for (const [key, child] of Object.entries(value)) {
|
|
54
|
+
if (key !== 'getBinary') appendBinaryAndPersistence(child, lines);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {Object} envelope @returns {string[]} */
|
|
59
|
+
function binaryEvidenceLines(envelope) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (const job of keyedGetEvidence(envelope)) {
|
|
62
|
+
const status = job.skipped === true ? 'skipped' : job.ok === true ? 'executed' : 'failed';
|
|
63
|
+
lines.push(` Keyed get: ${status} (${job.datasourceKey || envelope.datasourceKey || 'unknown'})`);
|
|
64
|
+
if (job.document) lines.push(` Document: ${JSON.stringify(binarySummary(job.document))}`);
|
|
65
|
+
}
|
|
66
|
+
appendBinaryAndPersistence(envelope?.integration?.stepResults, lines);
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {Object} envelope @param {string} key @returns {boolean} */
|
|
71
|
+
function hasKeyedDocumentPersistence(envelope, key) {
|
|
72
|
+
if (!hasExecutedKeyedGet(envelope, key)) return false;
|
|
73
|
+
const steps = envelope?.integration?.stepResults || [];
|
|
74
|
+
return steps.some(step => {
|
|
75
|
+
if (step.name !== 'document_persistence' || step.success === false || step.status === 'failed') return false;
|
|
76
|
+
const row = step.evidence?.datasources?.[key];
|
|
77
|
+
const model = row?.model;
|
|
78
|
+
return model?.scope?.expectedSource === 'get' && model.observed?.documents > 0 &&
|
|
79
|
+
model.observed?.chunks > 0 && (!Array.isArray(row.failures) || row.failures.length === 0);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {Object} result @param {string[]} keys @returns {boolean} */
|
|
84
|
+
function hasAllKeyedDocumentPersistence(result, keys) {
|
|
85
|
+
return keys.length > 0 && keys.every(key => {
|
|
86
|
+
const row = result?.results?.find(item => item.key === key);
|
|
87
|
+
return row?.success === true && hasKeyedDocumentPersistence(row.datasourceTestRun, key);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { binarySummary, sanitizeBinaryEvidence, binaryEvidenceLines, hasKeyedDocumentPersistence,
|
|
92
|
+
hasAllKeyedDocumentPersistence };
|
|
@@ -4,6 +4,43 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Keyed document retrieval has no list operation, so the `get` runs once against a business
|
|
9
|
+
* key instead of over a sync page. The dataplane records that run as step evidence
|
|
10
|
+
* (`sync_step_keyed_get.py`), which is why execution is read from evidence rather than from
|
|
11
|
+
* a row count. `IntegrationStepResult.evidence` is an open object by contract.
|
|
12
|
+
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
13
|
+
* @returns {Array<Object>} Keyed-get evidence entries, newest first as reported
|
|
14
|
+
*/
|
|
15
|
+
function keyedGetEvidence(envelope) {
|
|
16
|
+
const integration = envelope && typeof envelope === 'object' ? envelope.integration : null;
|
|
17
|
+
const steps = integration && Array.isArray(integration.stepResults) ? integration.stepResults : [];
|
|
18
|
+
return steps
|
|
19
|
+
.map(step => (step && typeof step.evidence === 'object' ? step.evidence : null))
|
|
20
|
+
.flatMap(evidence => evidence && Array.isArray(evidence.jobs) ? evidence.jobs : [evidence])
|
|
21
|
+
.filter(evidence => evidence && evidence.keyedGet === true);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A keyed `get` counts as executed only when it actually returned a document. A skipped run
|
|
26
|
+
* (no primary key value) or a failed one is not execution, and reporting it as such would
|
|
27
|
+
* turn `--strict-capability-scope` into a rubber stamp.
|
|
28
|
+
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
29
|
+
* @param {string|undefined|null} datasourceKey - Restrict to one datasource when given
|
|
30
|
+
* @returns {boolean} True when a keyed get ran successfully
|
|
31
|
+
*/
|
|
32
|
+
function hasExecutedKeyedGet(envelope, datasourceKey) {
|
|
33
|
+
const wanted = datasourceKey === undefined || datasourceKey === null || String(datasourceKey).trim() === ''
|
|
34
|
+
? ''
|
|
35
|
+
: String(datasourceKey).trim();
|
|
36
|
+
return keyedGetEvidence(envelope).some(evidence => {
|
|
37
|
+
if (evidence.ok !== true || evidence.skipped === true) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return wanted === '' || String(evidence.datasourceKey || '') === wanted;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
7
44
|
/**
|
|
8
45
|
* @param {Object|null|undefined} envelope - DatasourceTestRun-like
|
|
9
46
|
* @param {string|undefined|null} requestedCapabilityKey - From positional [capabilityKey]
|
|
@@ -23,6 +60,10 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
|
|
|
23
60
|
envelope && typeof envelope === 'object' && Array.isArray(envelope.capabilities)
|
|
24
61
|
? envelope.capabilities
|
|
25
62
|
: [];
|
|
63
|
+
if (key === 'get' && keyedGetEvidence(envelope).length > 0 &&
|
|
64
|
+
!hasExecutedKeyedGet(envelope, envelope.datasourceKey)) {
|
|
65
|
+
return { violated: true, message: 'Capabilities scope: keyed get was not executed successfully.' };
|
|
66
|
+
}
|
|
26
67
|
if (caps.length <= 1) {
|
|
27
68
|
return { violated: false };
|
|
28
69
|
}
|
|
@@ -39,5 +80,7 @@ function analyzeCapabilityScope(envelope, requestedCapabilityKey) {
|
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
module.exports = {
|
|
42
|
-
analyzeCapabilityScope
|
|
83
|
+
analyzeCapabilityScope,
|
|
84
|
+
keyedGetEvidence,
|
|
85
|
+
hasExecutedKeyedGet
|
|
43
86
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
|
|
7
8
|
const { SEP, appendReferenceLayoutLines } = require('./datasource-test-run-display');
|
|
8
9
|
const { buildDebugEnvelopeSlice } = require('./datasource-test-run-debug-slice');
|
|
9
10
|
const {
|
|
@@ -250,6 +251,7 @@ function applyRawLineCap(text) {
|
|
|
250
251
|
* @returns {string}
|
|
251
252
|
*/
|
|
252
253
|
function formatDatasourceTestRunDebugBlock(envelope, mode, isTTY) {
|
|
254
|
+
envelope = sanitizeBinaryEvidence(envelope);
|
|
253
255
|
if (!envelope || typeof envelope !== 'object') return '';
|
|
254
256
|
|
|
255
257
|
const lines = ['', SEP, `Debug (${mode})`];
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const chalk = require('chalk');
|
|
8
|
+
const { binaryEvidenceLines } = require('./datasource-binary-evidence');
|
|
8
9
|
const { sectionTitle, headerKeyValue, colorAggregateGlyph, successGlyph, failureGlyph } = require('./cli-test-layout-chalk');
|
|
9
10
|
const { appendCertificateTTY, appendCertificateIssuanceTTY } = require('./datasource-test-run-certificate-tty');
|
|
10
11
|
const { buildTtyMetaLines: buildTtyMetaLinesCore } = require('./datasource-test-run-tty-meta-lines');
|
|
12
|
+
const { appendStructuredIssueGuidance } = require('./datasource-test-run-issue-guidance');
|
|
11
13
|
|
|
12
14
|
const SEP = '────────────────────────────────';
|
|
13
15
|
|
|
@@ -297,9 +299,12 @@ function appendValidationIssueLines(lines, envelope, maxIssues = 5) {
|
|
|
297
299
|
const cap = Math.min(maxIssues, issues.length);
|
|
298
300
|
for (let i = 0; i < cap; i += 1) {
|
|
299
301
|
const iss = issues[i];
|
|
300
|
-
const
|
|
302
|
+
const issueCode = iss && (iss.stableCode || iss.code);
|
|
303
|
+
const code = issueCode ? chalk.red(`[${issueCode}] `) : '';
|
|
304
|
+
const severity = iss?.severity ? chalk.gray(`[${String(iss.severity)}] `) : '';
|
|
301
305
|
const msg = iss && iss.message ? String(iss.message) : JSON.stringify(iss);
|
|
302
|
-
lines.push(` ${code}${chalk.yellow(msg)}`);
|
|
306
|
+
lines.push(` ${code}${severity}${chalk.yellow(msg)}`);
|
|
307
|
+
appendStructuredIssueGuidance(lines, iss);
|
|
303
308
|
appendDpSec013Details(lines, iss);
|
|
304
309
|
}
|
|
305
310
|
if (issues.length > cap) {
|
|
@@ -448,6 +453,7 @@ function formatDatasourceTestRunTTY(envelope, options = {}) {
|
|
|
448
453
|
appendRefsSectionIfEnabled(lines, envelope, options);
|
|
449
454
|
appendValidationIssueLines(lines, envelope);
|
|
450
455
|
appendIntegrationStepLines(lines, envelope);
|
|
456
|
+
lines.push(...binaryEvidenceLines(envelope));
|
|
451
457
|
const focus = normalizedFocusCapabilityKey(options.focusCapabilityKey);
|
|
452
458
|
if (focus) {
|
|
453
459
|
lines.push(formatCapabilityFocusSection(envelope, focus));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured human guidance for DatasourceTestRun validation issues.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Preserve actionable issue fields in TTY output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
const STEP_VERBS = {
|
|
12
|
+
locate_file: 'open',
|
|
13
|
+
update_path: 'update',
|
|
14
|
+
add_mapping: 'add mapping',
|
|
15
|
+
set_value: 'set',
|
|
16
|
+
remove_value: 'remove',
|
|
17
|
+
add_item: 'add to',
|
|
18
|
+
remove_item: 'remove from'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const UPSERT_GUIDANCE = {
|
|
22
|
+
'DP-CIP-030': {
|
|
23
|
+
observed: 'The declared upsert does not contain the required create, update, and records-output composition.',
|
|
24
|
+
consequence: 'The runtime cannot execute one deterministic create-or-update operation.',
|
|
25
|
+
aiFixable: true,
|
|
26
|
+
remediation: [
|
|
27
|
+
'Run aifabrix datasource capability upsert <file-or-key> --overwrite --dry-run, review the patch, then apply and validate it.'
|
|
28
|
+
],
|
|
29
|
+
prevention: 'Author upsert through the capability command and keep both source halves valid.',
|
|
30
|
+
residualRisk: 'Publication remains blocked while the composition is malformed.',
|
|
31
|
+
rerun: 'Rerun the same focused datasource validation or E2E case.'
|
|
32
|
+
},
|
|
33
|
+
'DP-CIP-031': {
|
|
34
|
+
observed: 'The upsert declares no usable business identity.',
|
|
35
|
+
consequence: 'Existence cannot be resolved, so a retry may create a duplicate or update the wrong record.',
|
|
36
|
+
aiFixable: true,
|
|
37
|
+
remediation: [
|
|
38
|
+
'Run aifabrix datasource capability upsert <file-or-key> --identity <fields> --dry-run with author-approved mapped non-primary fields.'
|
|
39
|
+
],
|
|
40
|
+
prevention: 'Model business identity before exposing a composed write.',
|
|
41
|
+
residualRisk: 'Upsert must remain unavailable until identity is explicit and validated.',
|
|
42
|
+
rerun: 'Rerun the same focused datasource validation or E2E case.'
|
|
43
|
+
},
|
|
44
|
+
'DP-CIP-040': {
|
|
45
|
+
observed: 'No declared read operation can constrain the complete business identity.',
|
|
46
|
+
consequence: 'The source record may be missed, creating a duplicate or selecting the wrong update target.',
|
|
47
|
+
aiFixable: false,
|
|
48
|
+
remediation: [
|
|
49
|
+
'Declare a real exact lookup only if the source API supports it.',
|
|
50
|
+
'Otherwise keep separate create and update capabilities and withhold upsert readiness.'
|
|
51
|
+
],
|
|
52
|
+
prevention: 'Require complete equality lookup coverage before authoring upsert.',
|
|
53
|
+
residualRisk: 'AI cannot repair a source API that lacks exact lookup or uniqueness.',
|
|
54
|
+
rerun: 'After a supported lookup is declared, rerun the same focused validation and E2E proof.'
|
|
55
|
+
},
|
|
56
|
+
'DP-CIP-041': {
|
|
57
|
+
observed: 'A covering identity read is declared, but live filtering has not been proven.',
|
|
58
|
+
consequence: 'Static coverage alone cannot show that the source applies the constraint or exposes writes in time.',
|
|
59
|
+
aiFixable: false,
|
|
60
|
+
remediation: ['Run the focused live validation and two-write no-duplicate E2E proof; do not add a duplicate read declaration.'],
|
|
61
|
+
prevention: 'Keep exact lookup and no-duplicate proof scenarios with every upsert datasource.',
|
|
62
|
+
residualRisk: 'Read-after-write delay can leave a bounded duplicate window until runtime protection is proven.',
|
|
63
|
+
rerun: 'Rerun the same focused datasource validation and upsert E2E case.'
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function normalizeOptionalString(value) {
|
|
68
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasGuidanceValue(value) {
|
|
72
|
+
if (Array.isArray(value)) return value.some(hasGuidanceValue);
|
|
73
|
+
if (typeof value === 'boolean') return true;
|
|
74
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
75
|
+
return normalizeOptionalString(value) !== '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function firstGuidanceValue(...values) {
|
|
79
|
+
return values.find(hasGuidanceValue);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function guidancePayload(item) {
|
|
83
|
+
return normalizeOptionalString(item.value) || normalizeOptionalString(item.template);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function guidanceVerb(step) {
|
|
87
|
+
return STEP_VERBS[step] || step.replace(/_/g, ' ') || 'update';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render one guidance entry, which may be a string or a structured fix instruction.
|
|
92
|
+
*
|
|
93
|
+
* `fix.instructions[]` entries are objects ({step, path, value, template, note}),
|
|
94
|
+
* so stringifying them printed `[object Object]` and the remediation — the part a
|
|
95
|
+
* developer acts on — was lost.
|
|
96
|
+
*
|
|
97
|
+
* @param {unknown} item guidance entry
|
|
98
|
+
* @returns {string|null} display text, or null when there is nothing to show
|
|
99
|
+
*/
|
|
100
|
+
function formatGuidanceItem(item) {
|
|
101
|
+
if (typeof item === 'string') return item.trim() || null;
|
|
102
|
+
if (!item || typeof item !== 'object') return null;
|
|
103
|
+
|
|
104
|
+
const step = normalizeOptionalString(item.step);
|
|
105
|
+
const note = normalizeOptionalString(item.note);
|
|
106
|
+
const target = normalizeOptionalString(item.path);
|
|
107
|
+
const payload = guidancePayload(item);
|
|
108
|
+
|
|
109
|
+
// `explain` carries no path or value; its note *is* the instruction.
|
|
110
|
+
if (step === 'explain' || (!target && !payload)) {
|
|
111
|
+
return note || (step ? step.replace(/_/g, ' ') : null);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const verb = guidanceVerb(step);
|
|
115
|
+
// With no path the payload *is* the target (e.g. locate_file carries a path in
|
|
116
|
+
// `value`), so it reads as "open <file>" rather than "open: <file>".
|
|
117
|
+
let text = target ? `${verb} ${target}` : `${verb} ${payload}`;
|
|
118
|
+
if (target && payload) text += step === 'set_value' ? ` = ${payload}` : `: ${payload}`;
|
|
119
|
+
return note ? `${text} — ${note}` : text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appendGuidanceValue(lines, label, value) {
|
|
123
|
+
const values = Array.isArray(value) ? value : [value];
|
|
124
|
+
values
|
|
125
|
+
.map(formatGuidanceItem)
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.forEach((text) => {
|
|
128
|
+
lines.push(` ${chalk.gray(`${label}:`)} ${chalk.white(text)}`);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function enrichUpsertIssueGuidance(issue) {
|
|
133
|
+
if (!issue || typeof issue !== 'object') return issue;
|
|
134
|
+
const code = issue.stableCode || issue.code;
|
|
135
|
+
const fallback = UPSERT_GUIDANCE[code];
|
|
136
|
+
if (!fallback) return issue;
|
|
137
|
+
const enriched = { ...issue };
|
|
138
|
+
Object.entries(fallback).forEach(([key, value]) => {
|
|
139
|
+
if (!hasGuidanceValue(enriched[key])) enriched[key] = value;
|
|
140
|
+
});
|
|
141
|
+
return enriched;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function appendStructuredIssueGuidance(lines, issue) {
|
|
145
|
+
if (!issue || typeof issue !== 'object') return;
|
|
146
|
+
const display = enrichUpsertIssueGuidance(issue);
|
|
147
|
+
appendGuidanceValue(lines, 'Observed', firstGuidanceValue(
|
|
148
|
+
issue.observed, issue.details?.diagnosis, display.observed
|
|
149
|
+
));
|
|
150
|
+
appendGuidanceValue(lines, 'Consequence', firstGuidanceValue(
|
|
151
|
+
issue.impact, issue.consequence, issue.reason, display.consequence
|
|
152
|
+
));
|
|
153
|
+
if (typeof display.aiFixable === 'boolean') {
|
|
154
|
+
appendGuidanceValue(lines, 'Can AI fix?', display.aiFixable
|
|
155
|
+
? 'Yes — only as an authorized deterministic local manifest change.'
|
|
156
|
+
: 'No automatic fix is safe for this source or live-proof limitation.');
|
|
157
|
+
}
|
|
158
|
+
appendGuidanceValue(lines, 'Safe action', firstGuidanceValue(
|
|
159
|
+
issue.fix?.instructions, issue.remediation, issue.hint, display.remediation
|
|
160
|
+
));
|
|
161
|
+
appendGuidanceValue(lines, 'Prevention', firstGuidanceValue(
|
|
162
|
+
issue.prevention, issue.fix?.prevention, display.prevention
|
|
163
|
+
));
|
|
164
|
+
appendGuidanceValue(lines, 'Residual risk', firstGuidanceValue(
|
|
165
|
+
issue.residualRisk, issue.risk, display.residualRisk
|
|
166
|
+
));
|
|
167
|
+
appendGuidanceValue(lines, 'Rerun', firstGuidanceValue(
|
|
168
|
+
issue.rerun, issue.fix?.rerun, display.rerun
|
|
169
|
+
));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
appendStructuredIssueGuidance,
|
|
174
|
+
enrichUpsertIssueGuidance,
|
|
175
|
+
formatGuidanceItem
|
|
176
|
+
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
7
|
const chalk = require('chalk');
|
|
8
|
+
const { sanitizeBinaryEvidence } = require('./datasource-binary-evidence');
|
|
8
9
|
const logger = require('./logger');
|
|
9
10
|
const { getReportVersionStderrMessage } = require('./datasource-test-run-report-version');
|
|
10
11
|
const {
|
|
@@ -42,6 +43,7 @@ function emitCapabilityScopeDiagnostics(envelope, opts = {}) {
|
|
|
42
43
|
* @param {string} [options.requestedCapabilityKey]
|
|
43
44
|
*/
|
|
44
45
|
function printDatasourceTestRunForTTY(envelope, options = {}) {
|
|
46
|
+
envelope = sanitizeBinaryEvidence(envelope);
|
|
45
47
|
const mode = resolveDebugDisplayMode(options.debug);
|
|
46
48
|
const displayOpts = {
|
|
47
49
|
focusCapabilityKey: options.requestedCapabilityKey,
|
package/lib/utils/env-copy.js
CHANGED
|
@@ -56,16 +56,17 @@ function readDeveloperIdFromConfig(config) {
|
|
|
56
56
|
* Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
|
|
57
57
|
* @param {string} content - Env file content
|
|
58
58
|
* @param {string} outputPath - Resolved path of the .env file being written
|
|
59
|
+
* @param {string} [appName] - System key (defaults to output directory name)
|
|
59
60
|
* @returns {string} Content with /mnt/data replaced by path to mount directory
|
|
60
61
|
*/
|
|
61
|
-
function substituteMntDataForLocal(content, outputPath) {
|
|
62
|
+
function substituteMntDataForLocal(content, outputPath, appName) {
|
|
62
63
|
const outputDir = path.dirname(outputPath);
|
|
63
64
|
// Avoid introducing a drive letter for root-relative paths (e.g. "\tmp\out"),
|
|
64
65
|
// which are common in unit tests and some Windows path constructions.
|
|
65
66
|
const isWinRootRelative = process.platform === 'win32' && /^\\(?!\\)/.test(outputDir);
|
|
66
67
|
const localMountPath = isWinRootRelative
|
|
67
|
-
? path.win32.normalize(path.win32.join(outputDir, 'mount'))
|
|
68
|
-
: path.resolve(outputDir, 'mount');
|
|
68
|
+
? path.win32.normalize(path.win32.join(outputDir, 'mount', 'data', appName || path.basename(outputDir)))
|
|
69
|
+
: path.resolve(outputDir, 'mount', 'data', appName || path.basename(outputDir));
|
|
69
70
|
if (!fs.existsSync(localMountPath)) {
|
|
70
71
|
fs.mkdirSync(localMountPath, { recursive: true });
|
|
71
72
|
}
|
|
@@ -140,12 +141,12 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
|
|
|
140
141
|
baseContent = await fsp.readFile(outputPath, 'utf8');
|
|
141
142
|
} else if (appName) {
|
|
142
143
|
baseContent = await generateEnvContent(appName, null, 'local', false);
|
|
143
|
-
baseContent = substituteMntDataForLocal(baseContent, outputPath);
|
|
144
|
+
baseContent = substituteMntDataForLocal(baseContent, outputPath, appName);
|
|
144
145
|
} else {
|
|
145
146
|
baseContent = runContent;
|
|
146
147
|
}
|
|
147
148
|
let toWrite = mergeEnvMapIntoContent(baseContent, runMap, { appendMissingFromNewMap: false });
|
|
148
|
-
toWrite = substituteMntDataForLocal(toWrite, outputPath);
|
|
149
|
+
toWrite = substituteMntDataForLocal(toWrite, outputPath, appName);
|
|
149
150
|
await fsp.writeFile(outputPath, toWrite, { mode: 0o600 });
|
|
150
151
|
logger.log(
|
|
151
152
|
formatSuccessLine(
|
|
@@ -163,7 +164,7 @@ async function writeEnvOutputForReload(outputPath, runEnvPath, appName) {
|
|
|
163
164
|
async function writeEnvOutputForLocal(appName, outputPath) {
|
|
164
165
|
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
165
166
|
let localContent = await generateEnvContent(appName, null, 'local', false);
|
|
166
|
-
localContent = substituteMntDataForLocal(localContent, outputPath);
|
|
167
|
+
localContent = substituteMntDataForLocal(localContent, outputPath, appName);
|
|
167
168
|
let toWrite = localContent;
|
|
168
169
|
if (fs.existsSync(outputPath)) {
|
|
169
170
|
const existingContent = await fsp.readFile(outputPath, 'utf8');
|
|
@@ -319,7 +320,7 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
319
320
|
async function syncWrittenBuilderEnvToOutputPath(envPath, outputPath, envOutputPathLabel, copyOptions = {}) {
|
|
320
321
|
const { parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
321
322
|
const raw = fs.readFileSync(envPath, 'utf8');
|
|
322
|
-
const synced = substituteMntDataForLocal(raw, outputPath);
|
|
323
|
+
const synced = substituteMntDataForLocal(raw, outputPath, copyOptions.appName || path.basename(path.dirname(envPath)));
|
|
323
324
|
let toWrite = synced;
|
|
324
325
|
if (!copyOptions.freshEnv && fs.existsSync(outputPath)) {
|
|
325
326
|
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
@@ -344,7 +345,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
|
|
|
344
345
|
const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
|
|
345
346
|
const genOpts = extraOpts.appPath ? { appPath: extraOpts.appPath } : {};
|
|
346
347
|
let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false, genOpts);
|
|
347
|
-
localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
|
|
348
|
+
localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath, appName);
|
|
348
349
|
let toWrite = localEnvContent;
|
|
349
350
|
if (!extraOpts.freshEnv && fs.existsSync(outputPath)) {
|
|
350
351
|
const existingContent = fs.readFileSync(outputPath, 'utf8');
|
|
@@ -366,7 +367,7 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
|
|
|
366
367
|
async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
|
|
367
368
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
368
369
|
let patchedContent = await patchEnvContentForLocal(envContent, variables);
|
|
369
|
-
patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
|
|
370
|
+
patchedContent = substituteMntDataForLocal(patchedContent, outputPath, variables.app?.key || path.basename(path.dirname(envPath)));
|
|
370
371
|
fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
|
|
371
372
|
logger.log(formatSuccessLine(`Copied .env to: ${envOutputPathLabel}`));
|
|
372
373
|
}
|
|
@@ -405,7 +406,7 @@ async function copyOrRegenerateEnvForNamedApp(opts) {
|
|
|
405
406
|
return;
|
|
406
407
|
}
|
|
407
408
|
if (fs.existsSync(envPath)) {
|
|
408
|
-
await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true });
|
|
409
|
+
await syncWrittenBuilderEnvToOutputPath(envPath, outputPath, label, { freshEnv: freshEnv === true, appName });
|
|
409
410
|
return;
|
|
410
411
|
}
|
|
411
412
|
await writeLocalEnvToOutputPath(outputPath, appName, secretsPath, label, localOpts);
|
|
@@ -182,7 +182,9 @@ function pickBlockingDatasourceKey(rows) {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
function issueKey(issue) {
|
|
185
|
-
const code = issue && issue.
|
|
185
|
+
const code = issue && (issue.stableCode || issue.code)
|
|
186
|
+
? String(issue.stableCode || issue.code)
|
|
187
|
+
: '';
|
|
186
188
|
const msg = issue && issue.message ? String(issue.message) : '';
|
|
187
189
|
return `${code}::${msg}`.toLowerCase();
|
|
188
190
|
}
|
|
@@ -402,4 +404,3 @@ module.exports = {
|
|
|
402
404
|
systemCertStatus,
|
|
403
405
|
drillDownCommand
|
|
404
406
|
};
|
|
405
|
-
|
package/lib/utils/image-tags.js
CHANGED
|
@@ -48,7 +48,7 @@ function expandTag(entry, version) {
|
|
|
48
48
|
* @throws {Error} On blank entries, unsupported placeholders, missing version or invalid tags
|
|
49
49
|
*/
|
|
50
50
|
function resolveImageTags(tagInput, appVersion) {
|
|
51
|
-
if (tagInput === undefined || tagInput === null
|
|
51
|
+
if (tagInput === undefined || tagInput === null) {
|
|
52
52
|
return [DEFAULT_TAG];
|
|
53
53
|
}
|
|
54
54
|
const entries = String(tagInput).split(',').map((entry) => entry.trim());
|
|
@@ -72,7 +72,7 @@ function resolveImageTags(tagInput, appVersion) {
|
|
|
72
72
|
*/
|
|
73
73
|
function resolveBuildTagPlan(tagInput, appVersion) {
|
|
74
74
|
const tags = resolveImageTags(tagInput, appVersion);
|
|
75
|
-
const explicitSingle =
|
|
75
|
+
const explicitSingle = tagInput !== undefined && tagInput !== null && !String(tagInput).includes(',');
|
|
76
76
|
if (explicitSingle && !FLOATING_TAGS.includes(tags[0])) {
|
|
77
77
|
tags.push(DEFAULT_TAG);
|
|
78
78
|
}
|
|
@@ -42,7 +42,7 @@ function isPlatformCatalogKvRef(val) {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* True when repair should normalize a kv value to path-style kv://systemKey/variable.
|
|
45
|
-
* Platform catalog refs (e.g. kv://
|
|
45
|
+
* Platform catalog refs (e.g. kv://redis-passwordKeyVault) are never legacy.
|
|
46
46
|
*
|
|
47
47
|
* @param {string} val - Value from authentication.security or configuration
|
|
48
48
|
* @returns {boolean}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepare persistent storage before starting a local application container.
|
|
3
|
+
* @fileoverview Local storage mount and configured DATA_DIR initialization
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const dotenv = require('dotenv');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read only a DATA_DIR beneath the container's storage mount.
|
|
13
|
+
* @param {string|undefined} envFile - Resolved application env file
|
|
14
|
+
* @returns {string[]} Relative directory components
|
|
15
|
+
*/
|
|
16
|
+
function dataDirectoryParts(envFile) {
|
|
17
|
+
if (!envFile || !fs.existsSync(envFile)) return [];
|
|
18
|
+
const { DATA_DIR: dataDir } = dotenv.parse(fs.readFileSync(envFile, 'utf8'));
|
|
19
|
+
if (!dataDir || !dataDir.startsWith('/mnt/data/')) return [];
|
|
20
|
+
const parts = dataDir.slice('/mnt/data/'.length).split('/').filter(part => part && part !== '.');
|
|
21
|
+
if (parts.some(part => part === '..' || part.includes('\\') || part.includes('\0'))) {
|
|
22
|
+
throw new Error('DATA_DIR must remain inside the /mnt/data storage mount');
|
|
23
|
+
}
|
|
24
|
+
return parts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create the mount root and any configured DATA_DIR inside it, preserving state.
|
|
29
|
+
* Refuse child symlinks so initialization cannot write outside the storage root.
|
|
30
|
+
* @param {string} localPath - Host directory mounted at /mnt/data
|
|
31
|
+
* @param {string|undefined} envFile - Resolved application env file
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function prepareLocalDataMount(localPath, envFile) {
|
|
35
|
+
try {
|
|
36
|
+
const parts = dataDirectoryParts(envFile);
|
|
37
|
+
fs.mkdirSync(localPath, { recursive: true });
|
|
38
|
+
let directory = localPath;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
directory = path.join(directory, part);
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.lstatSync(directory);
|
|
43
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
44
|
+
throw new Error('DATA_DIR components must be directories, not symlinks or files');
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code !== 'ENOENT') throw error;
|
|
48
|
+
fs.mkdirSync(directory);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Filesystem errors can include env content or paths; report only their code.
|
|
53
|
+
const reason = error.code || error.message;
|
|
54
|
+
throw new Error(`Unable to prepare local storage before container startup: ${reason}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { prepareLocalDataMount };
|
|
@@ -55,7 +55,6 @@ const { resolveBashKvFromProcessEnv } = require('./secrets-bash-kv');
|
|
|
55
55
|
const EMPTY_ALLOWED_KV_FALLBACK = new Set([
|
|
56
56
|
'redis-passwordKeyVault',
|
|
57
57
|
'azure-openaiapi-urlKeyVault',
|
|
58
|
-
'secrets-azureOpenaiApiKeyVault',
|
|
59
58
|
'miso-controller-secrets-apiKeyVault'
|
|
60
59
|
]);
|
|
61
60
|
|