@aifabrix/builder 2.58.0 → 2.60.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/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +87 -0
- package/lib/agent-kit/start.js +48 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -103
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/standard-docker-build.js +4 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +14 -1
- package/lib/cli/setup-dev-path-commands.js +4 -0
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +13 -1
- package/lib/deployment/installation/azure-preflight.js +13 -2
- package/lib/deployment/installation/azure-readiness.js +24 -2
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +10 -7
- package/lib/deployment/installation/infra-manifest.js +5 -0
- package/lib/deployment/installation/registry-auth-mode.js +33 -10
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +33 -18
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/schema/application-schema.json +193 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +140 -34
- package/lib/schema/infra.parameter.yaml +30 -29
- package/lib/schema/infrastructure-schema.json +133 -14
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +13 -6
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/token-manager.js +29 -36
- package/package.json +4 -3
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
- package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +35 -7
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +37 -4
- package/templates/applications/miso-controller/application.yaml +24 -1
- package/templates/applications/miso-controller/env.template +22 -35
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +281 -75
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Resumable Dataplane qualification upload: accept, --wait/--no-wait, poll, resume.
|
|
3
|
-
* Mirrors dev-host-ops.js's job id + wait/no-wait contract
|
|
3
|
+
* Mirrors dev-host-ops.js's job id + wait/no-wait contract. Never invents
|
|
4
4
|
* qualification truth — accept/poll surface exactly what Dataplane returns.
|
|
5
5
|
* @author AI Fabrix Team
|
|
6
6
|
* @version 1.0.0
|
|
@@ -12,13 +12,38 @@ const logger = require('../utils/logger');
|
|
|
12
12
|
const { formatSuccessLine, formatWarningLine } = require('../utils/cli-test-layout-chalk');
|
|
13
13
|
const { formatApiError } = require('../utils/api-error-handler');
|
|
14
14
|
const { uploadApplicationViaPipelineWithAuthRetry } = require('../utils/pipeline-upload-auth-retry');
|
|
15
|
-
const { pollQualificationJobUntilTerminal
|
|
15
|
+
const { pollQualificationJobUntilTerminal, requireQualificationSuccess,
|
|
16
|
+
isTransientQualificationPollError } = require('./upload-qualification-poll');
|
|
16
17
|
const { createQualificationPollHandlers } = require('../utils/qualification-poll-ui');
|
|
17
18
|
|
|
19
|
+
function isAmbiguousAcceptance(error) {
|
|
20
|
+
const status = error?.response?.status || error?.statusCode || error?.status;
|
|
21
|
+
const code = error?.cause?.code || error?.code;
|
|
22
|
+
return isTransientQualificationPollError(status) || (!status && (
|
|
23
|
+
error instanceof TypeError || ['AbortError', 'TimeoutError'].includes(error?.name) ||
|
|
24
|
+
['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND',
|
|
25
|
+
'ECONNABORTED', 'UND_ERR_CONNECT_TIMEOUT'].includes(code)
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function submitQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options) {
|
|
30
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
31
|
+
try {
|
|
32
|
+
const res = await uploadApplicationViaPipelineWithAuthRetry(
|
|
33
|
+
dataplaneUrl, authConfig, payload, systemKey, options
|
|
34
|
+
);
|
|
35
|
+
if (res?.success !== false || !isTransientQualificationPollError(res.status) || attempt === 1) {
|
|
36
|
+
return res;
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (!isAmbiguousAcceptance(error) || attempt === 1) throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
18
44
|
/**
|
|
19
|
-
* Ctrl-C
|
|
20
|
-
* the
|
|
21
|
-
* This only makes that safe fact visible, printing the resume command before exiting 130.
|
|
45
|
+
* Ctrl-C detaches from status following without asserting an unobserved job outcome.
|
|
46
|
+
* Prints the resume command before exiting 130.
|
|
22
47
|
* @param {string} jobId
|
|
23
48
|
* @param {string} systemKey
|
|
24
49
|
* @returns {() => void} Call to remove the listeners once polling finishes normally.
|
|
@@ -27,7 +52,7 @@ function registerUploadInterruptHint(jobId, systemKey) {
|
|
|
27
52
|
const onSignal = () => {
|
|
28
53
|
logger.log(
|
|
29
54
|
formatWarningLine(
|
|
30
|
-
`
|
|
55
|
+
`Stopped following upload job ${jobId}; its current outcome is unknown. Resume with: aifabrix upload ${systemKey} --resume ${jobId}`
|
|
31
56
|
)
|
|
32
57
|
);
|
|
33
58
|
process.exit(130);
|
|
@@ -52,12 +77,24 @@ function registerUploadInterruptHint(jobId, systemKey) {
|
|
|
52
77
|
* @returns {Promise<{jobId: string, state: string, resumed: boolean, candidateFingerprint: string}>}
|
|
53
78
|
*/
|
|
54
79
|
async function acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await submitQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, {
|
|
83
|
+
...options,
|
|
84
|
+
async: true
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!isAmbiguousAcceptance(error)) throw error;
|
|
88
|
+
const unknown = new Error('Upload acceptance outcome is unknown. Rerun the same upload command to recover the job.');
|
|
89
|
+
unknown.cause = error;
|
|
90
|
+
unknown.statusCode = error?.response?.status || error?.statusCode || error?.status;
|
|
91
|
+
throw unknown;
|
|
92
|
+
}
|
|
59
93
|
if (res?.success === false) {
|
|
60
|
-
const
|
|
94
|
+
const ambiguous = isTransientQualificationPollError(res.status);
|
|
95
|
+
const err = new Error(ambiguous
|
|
96
|
+
? 'Upload acceptance outcome is unknown. Rerun the same upload command to recover the job.'
|
|
97
|
+
: formatApiError(res, dataplaneUrl));
|
|
61
98
|
const status = Number(res.status || res.statusCode);
|
|
62
99
|
err.statusCode = status >= 400 && status < 600 ? status : 400;
|
|
63
100
|
throw err;
|
|
@@ -107,6 +144,14 @@ async function resumeQualificationUpload(dataplaneUrl, authConfig, jobId, system
|
|
|
107
144
|
*/
|
|
108
145
|
async function runQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options = {}) {
|
|
109
146
|
const accepted = await acceptQualificationUpload(dataplaneUrl, authConfig, payload, systemKey, options);
|
|
147
|
+
if (accepted.state === 'completed' || accepted.state === 'failed') {
|
|
148
|
+
requireQualificationSuccess(accepted);
|
|
149
|
+
return {
|
|
150
|
+
...accepted,
|
|
151
|
+
candidateFingerprint: accepted.result?.candidateFingerprint,
|
|
152
|
+
terminal: accepted
|
|
153
|
+
};
|
|
154
|
+
}
|
|
110
155
|
const wait = options.wait !== false;
|
|
111
156
|
if (!wait) {
|
|
112
157
|
if (options.json) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Poll a Dataplane qualification
|
|
2
|
+
* @fileoverview Poll a Dataplane qualification job while retaining resumable identity.
|
|
3
3
|
* Never invents success and never marks the candidate failed just because the CLI stopped polling —
|
|
4
4
|
* timing out returns a resumable jobId instead.
|
|
5
5
|
* @author AI Fabrix Team
|
|
@@ -13,7 +13,7 @@ const { formatApiError } = require('../utils/api-error-handler');
|
|
|
13
13
|
|
|
14
14
|
const POLL_INTERVAL_MS = 3000;
|
|
15
15
|
const POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
|
-
const TRANSIENT_HTTP_STATUSES = new Set([502, 503, 504]);
|
|
16
|
+
const TRANSIENT_HTTP_STATUSES = new Set([429, 502, 503, 504]);
|
|
17
17
|
const MAX_TRANSIENT_POLL_ERRORS = 20;
|
|
18
18
|
|
|
19
19
|
const GOVERNANCE_SCENARIOS_MISSING = 'GOVERNANCE_SCENARIOS_MISSING';
|
|
@@ -46,6 +46,17 @@ function formatQualificationJobFailure(job) {
|
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Throw only for an authoritative stored failure, retaining the original job. */
|
|
50
|
+
function requireQualificationSuccess(job) {
|
|
51
|
+
if (job.state !== 'failed') return;
|
|
52
|
+
const failure = formatQualificationJobFailure(job);
|
|
53
|
+
const err = new Error(failure.message);
|
|
54
|
+
err.jobId = job.jobId;
|
|
55
|
+
err.job = job;
|
|
56
|
+
err.skipDoctor = failure.userFacing;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* @param {number|string} status
|
|
51
62
|
* @returns {boolean}
|
|
@@ -54,6 +65,35 @@ function isTransientQualificationPollError(status) {
|
|
|
54
65
|
return TRANSIENT_HTTP_STATUSES.has(Number(status));
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
/** Preserve the accepted identity without asserting a business outcome. */
|
|
69
|
+
function unavailableJob(jobId, message) {
|
|
70
|
+
const error = new Error(
|
|
71
|
+
`${message} Job ${jobId}: status unavailable. Resume with: aifabrix upload <systemKey> --resume ${jobId}`
|
|
72
|
+
);
|
|
73
|
+
error.jobId = jobId;
|
|
74
|
+
error.resumable = true;
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Normalize transport exceptions so returned HTTP errors and throws share retries. */
|
|
79
|
+
async function readQualificationJob(dataplaneUrl, authConfig, jobId) {
|
|
80
|
+
try {
|
|
81
|
+
return await getPublishJobStatus(dataplaneUrl, authConfig, jobId);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const status = error?.response?.status || error?.statusCode || error?.status;
|
|
84
|
+
const code = error?.cause?.code || error?.code;
|
|
85
|
+
const network = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EAI_AGAIN',
|
|
86
|
+
'ENOTFOUND', 'ECONNABORTED', 'UND_ERR_CONNECT_TIMEOUT'].includes(code) ||
|
|
87
|
+
error instanceof TypeError || ['AbortError', 'TimeoutError'].includes(error?.name);
|
|
88
|
+
if (isTransientQualificationPollError(status) || (!status && network)) {
|
|
89
|
+
return { success: false, status: status || 503 };
|
|
90
|
+
}
|
|
91
|
+
const failure = unavailableJob(jobId, 'Unable to check upload progress.');
|
|
92
|
+
failure.statusCode = status;
|
|
93
|
+
throw failure;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
/**
|
|
58
98
|
* @param {string} dataplaneUrl
|
|
59
99
|
* @param {Object} authConfig
|
|
@@ -64,19 +104,19 @@ function isTransientQualificationPollError(status) {
|
|
|
64
104
|
async function fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, pollUi = {}) {
|
|
65
105
|
const started = Date.now();
|
|
66
106
|
for (let attempt = 0; attempt <= MAX_TRANSIENT_POLL_ERRORS; attempt += 1) {
|
|
67
|
-
const res = await
|
|
107
|
+
const res = await readQualificationJob(dataplaneUrl, authConfig, jobId);
|
|
68
108
|
if (res?.success !== false) {
|
|
69
109
|
return res.data;
|
|
70
110
|
}
|
|
71
111
|
if (!isTransientQualificationPollError(res.status)) {
|
|
72
|
-
const err =
|
|
112
|
+
const err = unavailableJob(jobId, formatApiError(res, dataplaneUrl));
|
|
73
113
|
const status = Number(res.status);
|
|
74
114
|
err.statusCode = status >= 400 && status < 600 ? status : 400;
|
|
75
115
|
throw err;
|
|
76
116
|
}
|
|
77
117
|
if (attempt >= MAX_TRANSIENT_POLL_ERRORS) {
|
|
78
|
-
throw
|
|
79
|
-
`The upload service returned HTTP ${res.status} repeatedly
|
|
118
|
+
throw unavailableJob(
|
|
119
|
+
jobId, `The upload service returned HTTP ${res.status} repeatedly.`
|
|
80
120
|
);
|
|
81
121
|
}
|
|
82
122
|
if (typeof pollUi.onTransientError === 'function') {
|
|
@@ -84,13 +124,13 @@ async function fetchQualificationJobWithRetry(dataplaneUrl, authConfig, jobId, p
|
|
|
84
124
|
}
|
|
85
125
|
await sleep(POLL_INTERVAL_MS);
|
|
86
126
|
}
|
|
87
|
-
throw
|
|
127
|
+
throw unavailableJob(jobId, 'Status retries exhausted.');
|
|
88
128
|
}
|
|
89
129
|
|
|
90
130
|
/**
|
|
91
131
|
* Poll a Dataplane qualification job to terminal state (completed|failed). The timeout only
|
|
92
132
|
* bounds how long *this CLI process* waits — Dataplane keeps running the job regardless; timing
|
|
93
|
-
* out returns a resumable jobId, never a fabricated failure
|
|
133
|
+
* out returns a resumable jobId, never a fabricated failure.
|
|
94
134
|
* @param {string} dataplaneUrl
|
|
95
135
|
* @param {Object} authConfig
|
|
96
136
|
* @param {string} jobId
|
|
@@ -112,25 +152,13 @@ async function pollQualificationJobUntilTerminal(
|
|
|
112
152
|
if (typeof pollUi.onPollProgress === 'function') {
|
|
113
153
|
pollUi.onPollProgress(job);
|
|
114
154
|
}
|
|
115
|
-
|
|
116
|
-
const failure = formatQualificationJobFailure(job);
|
|
117
|
-
const err = new Error(failure.message);
|
|
118
|
-
err.jobId = jobId;
|
|
119
|
-
err.job = job;
|
|
120
|
-
err.skipDoctor = failure.userFacing;
|
|
121
|
-
throw err;
|
|
122
|
-
}
|
|
155
|
+
requireQualificationSuccess(job);
|
|
123
156
|
if (job.state === 'completed') {
|
|
124
157
|
return job;
|
|
125
158
|
}
|
|
126
159
|
await sleep(POLL_INTERVAL_MS);
|
|
127
160
|
}
|
|
128
|
-
|
|
129
|
-
`Timed out waiting for upload job ${jobId}. It is still running — resume with: aifabrix upload <systemKey> --resume ${jobId}`
|
|
130
|
-
);
|
|
131
|
-
timeoutErr.jobId = jobId;
|
|
132
|
-
timeoutErr.resumable = true;
|
|
133
|
-
throw timeoutErr;
|
|
161
|
+
throw unavailableJob(jobId, 'Timed out waiting for upload progress.');
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
module.exports = {
|
|
@@ -139,5 +167,6 @@ module.exports = {
|
|
|
139
167
|
isTransientQualificationPollError,
|
|
140
168
|
fetchQualificationJobWithRetry,
|
|
141
169
|
formatQualificationJobFailure,
|
|
170
|
+
requireQualificationSuccess,
|
|
142
171
|
pollQualificationJobUntilTerminal
|
|
143
172
|
};
|
|
@@ -4,43 +4,11 @@
|
|
|
4
4
|
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const chalk = require('chalk');
|
|
8
7
|
const logger = require('../utils/logger');
|
|
9
8
|
const { formatProgress, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
10
9
|
const { getDataplaneUrl } = require('../datasource/deploy');
|
|
11
|
-
const { listEnvironmentApplications } = require('../api/environments.api');
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
* Find dataplane service application key from environment applications list
|
|
15
|
-
* @async
|
|
16
|
-
* @function findDataplaneServiceAppKey
|
|
17
|
-
* @param {string} controllerUrl - Controller URL
|
|
18
|
-
* @param {string} environment - Environment key
|
|
19
|
-
* @param {Object} authConfig - Authentication configuration
|
|
20
|
-
* @returns {Promise<string|null>} Dataplane service appKey or null if not found
|
|
21
|
-
*/
|
|
22
|
-
// eslint-disable-next-line complexity
|
|
23
|
-
async function findDataplaneServiceAppKey(controllerUrl, environment, authConfig) {
|
|
24
|
-
try {
|
|
25
|
-
const response = await listEnvironmentApplications(controllerUrl, environment, authConfig, { pageSize: 100 });
|
|
26
|
-
if (!response.success || !response.data) return null;
|
|
27
|
-
const applications = response.data.data || response.data || [];
|
|
28
|
-
for (const app of applications) {
|
|
29
|
-
const appKey = app.key || app.id;
|
|
30
|
-
if (!appKey) continue;
|
|
31
|
-
const keyLower = appKey.toLowerCase();
|
|
32
|
-
const appType = app.configuration?.type || app.type;
|
|
33
|
-
const nameLower = (app.displayName || app.name || '').toLowerCase();
|
|
34
|
-
if (keyLower === 'dataplane' || keyLower.includes('dataplane') || (appType === 'service' && nameLower.includes('dataplane'))) {
|
|
35
|
-
return appKey;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
logger.log(chalk.gray(` Could not list applications: ${error.message}`));
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
11
|
+
const DATAPLANE_APPLICATION_KEY = 'dataplane';
|
|
44
12
|
|
|
45
13
|
/**
|
|
46
14
|
* Check if error is a "Not Found" error
|
|
@@ -65,7 +33,7 @@ function createDataplaneNotFoundError() {
|
|
|
65
33
|
}
|
|
66
34
|
|
|
67
35
|
/**
|
|
68
|
-
*
|
|
36
|
+
* Get dataplane URL from the canonical controller application key.
|
|
69
37
|
* @async
|
|
70
38
|
* @param {string} controllerUrl - Controller URL
|
|
71
39
|
* @param {string} environment - Environment key
|
|
@@ -73,18 +41,23 @@ function createDataplaneNotFoundError() {
|
|
|
73
41
|
* @returns {Promise<string>} Dataplane URL
|
|
74
42
|
* @throws {Error} If dataplane URL cannot be retrieved
|
|
75
43
|
*/
|
|
76
|
-
async function
|
|
44
|
+
async function getExactDataplaneUrl(controllerUrl, environment, authConfig, silent) {
|
|
77
45
|
try {
|
|
78
|
-
const
|
|
46
|
+
const dataplaneUrl = await getDataplaneUrl(
|
|
47
|
+
controllerUrl,
|
|
48
|
+
DATAPLANE_APPLICATION_KEY,
|
|
49
|
+
environment,
|
|
50
|
+
authConfig
|
|
51
|
+
);
|
|
79
52
|
if (!silent) {
|
|
80
|
-
logger.log(formatSuccessLine(`Dataplane URL: ${
|
|
53
|
+
logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
|
|
81
54
|
}
|
|
82
|
-
return
|
|
83
|
-
} catch (
|
|
84
|
-
if (isNotFoundError(
|
|
55
|
+
return dataplaneUrl;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isNotFoundError(error)) {
|
|
85
58
|
throw createDataplaneNotFoundError();
|
|
86
59
|
}
|
|
87
|
-
throw
|
|
60
|
+
throw error;
|
|
88
61
|
}
|
|
89
62
|
}
|
|
90
63
|
|
|
@@ -105,15 +78,7 @@ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts
|
|
|
105
78
|
logger.log(formatProgress('Getting dataplane URL from controller...'));
|
|
106
79
|
}
|
|
107
80
|
try {
|
|
108
|
-
|
|
109
|
-
if (dataplaneAppKey) {
|
|
110
|
-
const dataplaneUrl = await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
|
|
111
|
-
if (!silent) {
|
|
112
|
-
logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
|
|
113
|
-
}
|
|
114
|
-
return dataplaneUrl;
|
|
115
|
-
}
|
|
116
|
-
return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent);
|
|
81
|
+
return await getExactDataplaneUrl(controllerUrl, environment, authConfig, silent);
|
|
117
82
|
} catch (error) {
|
|
118
83
|
if (error.message.includes('Could not discover dataplane URL')) {
|
|
119
84
|
throw error;
|
|
@@ -126,6 +91,5 @@ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts
|
|
|
126
91
|
}
|
|
127
92
|
|
|
128
93
|
module.exports = {
|
|
129
|
-
discoverDataplaneUrl
|
|
130
|
-
findDataplaneServiceAppKey
|
|
94
|
+
discoverDataplaneUrl
|
|
131
95
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `{NAME}` expansion in env content (plan 200.0).
|
|
3
|
+
*
|
|
4
|
+
* Values resolved from the manifest `platforms` / `environments` sections are substituted into
|
|
5
|
+
* `env.template` values. Runs after `kv://` and before `url://`, so a resolved value may itself
|
|
6
|
+
* carry a declarative URL.
|
|
7
|
+
*
|
|
8
|
+
* Only the value side of `KEY=value` is touched, and only `{UPPER_SNAKE}` tokens that are not
|
|
9
|
+
* preceded by `$`. That leaves Builder's existing `${VAR}` expansion and `{lowercase}` text in
|
|
10
|
+
* comments alone — both appear in shipped templates.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview Platform/environment name expansion for env.template
|
|
13
|
+
* @author AI Fabrix Team
|
|
14
|
+
* @version 1.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { resolveConfiguration } = require('../utils/platform-resolution');
|
|
20
|
+
|
|
21
|
+
/** `{NAME}` not preceded by `$`; uppercase only, matching the configuration name pattern. */
|
|
22
|
+
const NAME_TOKEN = /(?<!\$)\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} [manifest] - Application manifest
|
|
26
|
+
* @returns {boolean} True when the manifest declares either section
|
|
27
|
+
*/
|
|
28
|
+
function declaresPlatformSections(manifest) {
|
|
29
|
+
if (!manifest || typeof manifest !== 'object') return false;
|
|
30
|
+
return Array.isArray(manifest.platforms) || Array.isArray(manifest.environments);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expandValue(value, values, key, strict, context = {}) {
|
|
34
|
+
return value.replace(NAME_TOKEN, (match, name) => {
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(values, name)) return values[name];
|
|
36
|
+
if (!strict) return match;
|
|
37
|
+
// Without a resource group no platform matched, so only environments and built-ins resolved.
|
|
38
|
+
// Saying so turns "why is this missing" into "declare it here".
|
|
39
|
+
const where = context.resourceGroup
|
|
40
|
+
? `resource group "${context.resourceGroup}" and environment "${context.environment || ''}"`
|
|
41
|
+
: `environment "${context.environment || ''}" (no resource group, so platform values are unavailable)`;
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${key} references {${name}}, which no platform or environment configuration resolves for ` +
|
|
44
|
+
`${where}. Declare it under environments, or run against a resource group that supplies it.`
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Expands `{NAME}` on the value side of every assignment line.
|
|
51
|
+
* @param {string} content - Env content
|
|
52
|
+
* @param {Object} values - Resolved names
|
|
53
|
+
* @param {Object} [opts] - `strict` (default true) errors on an unresolved name; false leaves the
|
|
54
|
+
* token in place for a later resolver (the deploy manifest, resolved by Miso per installation)
|
|
55
|
+
* @returns {string}
|
|
56
|
+
* @throws {Error} When strict and a referenced name is not resolved
|
|
57
|
+
*/
|
|
58
|
+
function expandNamesInEnvContent(content, values, opts = {}) {
|
|
59
|
+
const strict = opts.strict !== false;
|
|
60
|
+
const context = { environment: opts.environment, resourceGroup: opts.resourceGroup };
|
|
61
|
+
const lines = String(content || '').split('\n');
|
|
62
|
+
const out = lines.map(line => {
|
|
63
|
+
const trimmed = line.trimStart();
|
|
64
|
+
if (!trimmed || trimmed.startsWith('#')) return line;
|
|
65
|
+
const separator = line.indexOf('=');
|
|
66
|
+
if (separator < 0) return line;
|
|
67
|
+
const key = line.slice(0, separator);
|
|
68
|
+
const value = line.slice(separator + 1);
|
|
69
|
+
return `${key}=${expandValue(value, values, key.trim(), strict, context)}`;
|
|
70
|
+
});
|
|
71
|
+
return out.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolves the manifest sections and expands `{NAME}` when either section is declared.
|
|
76
|
+
* A manifest without them is returned untouched, so existing applications are unaffected.
|
|
77
|
+
* @param {string} content - Env content after kv:// resolution
|
|
78
|
+
* @param {Object} params - `manifest`, `environment` (deployment env key), `resourceGroup`, `strict`
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
function expandPlatformNamesIfPresent(content, params = {}) {
|
|
82
|
+
const { manifest, environment, resourceGroup, strict } = params;
|
|
83
|
+
if (!declaresPlatformSections(manifest)) return content;
|
|
84
|
+
const { values } = resolveConfiguration(manifest, { environment, resourceGroup });
|
|
85
|
+
return expandNamesInEnvContent(content, values, { strict, environment, resourceGroup });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
NAME_TOKEN,
|
|
90
|
+
declaresPlatformSections,
|
|
91
|
+
expandNamesInEnvContent,
|
|
92
|
+
expandPlatformNamesIfPresent
|
|
93
|
+
};
|
|
@@ -44,6 +44,7 @@ const { computeEffectiveEnvironmentScopedResources, redisDbIndexForScopedRunEnv
|
|
|
44
44
|
const { applyRedisDbIndexToEnvContent } = require('../utils/redis-env-scope');
|
|
45
45
|
const { expandDeclarativeUrlsIfPresent } = require('./secrets-env-declarative-expand');
|
|
46
46
|
const {
|
|
47
|
+
mergeAdditionalPublishedPorts,
|
|
47
48
|
mergeDockerManifestPublishedPort,
|
|
48
49
|
rewriteDockerManifestPublicPortEnvLine
|
|
49
50
|
} = require('../utils/docker-manifest-public-port');
|
|
@@ -154,6 +155,12 @@ async function getDockerRedisDbEndpoints() {
|
|
|
154
155
|
return { redisHost, redisPort, dbHost, dbPort };
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
/** Host ports clients connect to: the manifest's own, plus any additionalPorts. */
|
|
159
|
+
async function mergePublishedPorts(envVars, appDoc) {
|
|
160
|
+
await mergeDockerManifestPublishedPort(envVars, appDoc);
|
|
161
|
+
await mergeAdditionalPublishedPorts(envVars, appDoc);
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
/** Docker env transformations: ports, infra endpoints, PORT. */
|
|
158
165
|
async function applyDockerTransformations(resolved, variablesPath) {
|
|
159
166
|
resolved = await resolveServicePortsInEnvContent(resolved, 'docker');
|
|
@@ -164,7 +171,7 @@ async function applyDockerTransformations(resolved, variablesPath) {
|
|
|
164
171
|
const containerPort = getContainerPortFromPath(variablesPath) ?? getContainerPortFromDockerEnv(dockerEnv) ?? 3000;
|
|
165
172
|
const envVars = await buildEnvVarMap('docker', null, null, { appPort: containerPort });
|
|
166
173
|
const appDoc = loadVariablesFromPath(variablesPath);
|
|
167
|
-
await
|
|
174
|
+
await mergePublishedPorts(envVars, appDoc);
|
|
168
175
|
envVars.REDIS_HOST = redisHost;
|
|
169
176
|
envVars.REDIS_PORT = String(redisPort);
|
|
170
177
|
envVars.DB_HOST = dbHost;
|
|
@@ -187,6 +194,32 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
187
194
|
return resolved;
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Expands manifest `{NAME}` values when the application declares platforms/environments.
|
|
199
|
+
* Local Builder runs pass no resource group, so they resolve as the local platform.
|
|
200
|
+
* @param {string} content - Env content after kv:// resolution
|
|
201
|
+
* @param {string|null} variablesPath - Application manifest path (null in env-only mode)
|
|
202
|
+
* @param {string} runEnvKey - Deployment environment key
|
|
203
|
+
* @param {Object} [options] - `resourceGroup` when a deployment target is known
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function expandPlatformNamesForApp(content, variablesPath, runEnvKey, options = {}) {
|
|
207
|
+
if (!variablesPath) return content;
|
|
208
|
+
const { expandPlatformNamesIfPresent } = require('./env-platform-expand');
|
|
209
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
210
|
+
let manifest = null;
|
|
211
|
+
try {
|
|
212
|
+
manifest = loadConfigFile(variablesPath);
|
|
213
|
+
} catch {
|
|
214
|
+
return content;
|
|
215
|
+
}
|
|
216
|
+
return expandPlatformNamesIfPresent(content, {
|
|
217
|
+
manifest,
|
|
218
|
+
environment: runEnvKey,
|
|
219
|
+
resourceGroup: options.resourceGroup
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
/**
|
|
191
224
|
* Generate .env content from template and secrets (no disk write).
|
|
192
225
|
* When options.envOnly is true, variablesPath is null (no application config).
|
|
@@ -210,15 +243,16 @@ async function generateEnvContent(appName, secretsPath, environment = 'local', f
|
|
|
210
243
|
}
|
|
211
244
|
const secrets = await loadSecrets(secretsPath, appName);
|
|
212
245
|
const { runEnvKey, effective } = await buildScopedKvContext(appPath, options);
|
|
213
|
-
const scopedKv = { envKey: runEnvKey, effective };
|
|
214
246
|
let resolved = await resolveKvReferences(template, secrets, environment, secretsPaths, appName, {
|
|
215
247
|
envKey: runEnvKey,
|
|
216
248
|
effective,
|
|
217
249
|
envTemplatePath: templatePath
|
|
218
250
|
});
|
|
219
251
|
if (!secretsPath) {
|
|
220
|
-
await materializeResolvedKvSecretsToUserLocal(template, secrets,
|
|
252
|
+
await materializeResolvedKvSecretsToUserLocal(template, secrets, { envKey: runEnvKey, effective }, options);
|
|
221
253
|
}
|
|
254
|
+
// Platform/environment {NAME} before url://, so a resolved value may carry a declarative URL.
|
|
255
|
+
resolved = expandPlatformNamesForApp(resolved, variablesPath, runEnvKey, options);
|
|
222
256
|
resolved = await expandDeclarativeUrlsIfPresent(
|
|
223
257
|
resolved,
|
|
224
258
|
appName,
|
|
@@ -74,17 +74,20 @@ function summarizeSyncLine(sync) {
|
|
|
74
74
|
if (!sync || typeof sync !== 'object') {
|
|
75
75
|
return 'Not configured';
|
|
76
76
|
}
|
|
77
|
-
const
|
|
77
|
+
const disabled = sync.enabled === false;
|
|
78
|
+
const parts = [disabled ? 'disabled' : sync.enabled === true ? 'enabled' : 'enabled (legacy default)'];
|
|
79
|
+
if (sync.schedule) {
|
|
80
|
+
parts.push(`${disabled ? 'retained schedule' : 'scheduled'} ${JSON.stringify(sync.schedule)}`);
|
|
81
|
+
} else {
|
|
82
|
+
parts.push('manual-only');
|
|
83
|
+
}
|
|
78
84
|
if (sync.mode) {
|
|
79
85
|
parts.push(String(sync.mode));
|
|
80
86
|
}
|
|
81
87
|
if (sync.batchSize !== undefined && sync.batchSize !== null) {
|
|
82
88
|
parts.push(`batch size ${sync.batchSize}`);
|
|
83
89
|
}
|
|
84
|
-
|
|
85
|
-
parts.push(`schedule ${JSON.stringify(sync.schedule)}`);
|
|
86
|
-
}
|
|
87
|
-
return parts.length ? parts.join(', ') : 'Present';
|
|
90
|
+
return parts.join(', ');
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource synchronization defaults for newly authored sync blocks.
|
|
3
|
+
* @fileoverview Keeps default creation separate from preservation decisions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DATASOURCE_SYNC = Object.freeze({
|
|
9
|
+
enabled: true,
|
|
10
|
+
mode: 'pull',
|
|
11
|
+
schedule: '@monthly',
|
|
12
|
+
batchSize: 500
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const DATASOURCE_SYNC_MODES = new Set(['pull', 'push', 'bidirectional']);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Record<string, unknown>} [overrides]
|
|
19
|
+
* @returns {Record<string, unknown>}
|
|
20
|
+
*/
|
|
21
|
+
function createDefaultDatasourceSync(overrides = {}) {
|
|
22
|
+
return { ...DEFAULT_DATASOURCE_SYNC, ...overrides };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve sync for a newly materialized BID datasource.
|
|
27
|
+
* An explicit datasource block is preserved exactly, including no schedule.
|
|
28
|
+
* @param {Record<string, unknown>} datasource
|
|
29
|
+
* @param {Record<string, unknown>} bidSync
|
|
30
|
+
* @returns {unknown}
|
|
31
|
+
*/
|
|
32
|
+
function resolveAuthoredDatasourceSync(datasource, bidSync) {
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(datasource, 'sync')) {
|
|
34
|
+
const explicit = datasource.sync;
|
|
35
|
+
return explicit && typeof explicit === 'object' ? { ...explicit } : explicit;
|
|
36
|
+
}
|
|
37
|
+
const overrides = {};
|
|
38
|
+
for (const key of ['enabled', 'schedule', 'batchSize']) {
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(bidSync, key)) {
|
|
40
|
+
overrides[key] = bidSync[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (DATASOURCE_SYNC_MODES.has(String(bidSync.mode))) {
|
|
44
|
+
overrides.mode = bidSync.mode;
|
|
45
|
+
}
|
|
46
|
+
return createDefaultDatasourceSync(overrides);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
DEFAULT_DATASOURCE_SYNC,
|
|
51
|
+
createDefaultDatasourceSync,
|
|
52
|
+
resolveAuthoredDatasourceSync
|
|
53
|
+
};
|
|
@@ -24,7 +24,9 @@ const preflight = require('./azure-preflight');
|
|
|
24
24
|
const readiness = require('./azure-readiness');
|
|
25
25
|
const registryAuth = require('./registry-auth-mode');
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// The Marketplace template stopped accepting moriCallbackApiKey: the installation receives its
|
|
28
|
+
// Mori key at licence registration instead, so infra no longer provisions or projects one.
|
|
29
|
+
const INFRA_SECRETS = Object.freeze(['ADMIN_PASSWORD', 'ENCRYPTION_KEY']);
|
|
28
30
|
|
|
29
31
|
function missingInputError(file, fields) {
|
|
30
32
|
const error = new Error(
|
|
@@ -50,6 +52,16 @@ async function applyAccessOptions(doc, input, deps) {
|
|
|
50
52
|
if (input.preset !== undefined) {
|
|
51
53
|
doc.preset = { ...doc.preset, value: normalizePreset(input.preset), source: 'cli' };
|
|
52
54
|
}
|
|
55
|
+
// The template sizes compute and PostgreSQL from capacityProfile, and the workspace already
|
|
56
|
+
// records a preset with the same values. Deriving one from the other keeps a single source:
|
|
57
|
+
// an installation cannot end up recording preset 'm' while deploying 's'.
|
|
58
|
+
if (doc.parameters.capacityProfile) {
|
|
59
|
+
doc.parameters.capacityProfile = {
|
|
60
|
+
...doc.parameters.capacityProfile,
|
|
61
|
+
value: normalizePreset(doc.preset.value),
|
|
62
|
+
source: 'workspacePreset'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
53
65
|
applyLicenseOptions(doc, input);
|
|
54
66
|
const enabled = input.allowMyIp || doc.allowMyIp.value === true;
|
|
55
67
|
if (!enabled) {
|
|
@@ -127,10 +127,21 @@ function assertDeploymentAuthority(doc, acr, az) {
|
|
|
127
127
|
if (!hasPermission(az, subscriptionScope, DEPLOYMENT_WRITE)) {
|
|
128
128
|
throw new Error(`The signed-in Azure account cannot create deployments in subscription ${doc.subscriptionId.value}`);
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
// Checked separately, because they are different permissions on different resources.
|
|
131
|
+
//
|
|
132
|
+
// Credential mode used to return here and skip both. That was wrong for the first one: the
|
|
133
|
+
// installation still needs Contributor granted in the resource group it is creating, and
|
|
134
|
+
// skipping the check hid the fact that the grant was being skipped too. The controller then
|
|
135
|
+
// could not create anything, and the failure surfaced three layers away as "Environment 'dev'
|
|
136
|
+
// has no recorded placement".
|
|
131
137
|
if (doc.parameters.deployRoleAssignments.value !== false && !hasPermission(az, subscriptionScope, ROLE_ASSIGNMENT_WRITE)) {
|
|
132
|
-
throw new Error(`The signed-in Azure account cannot create role assignments in subscription ${doc.subscriptionId.value}; ask for Owner or User Access Administrator, or set parameters.deployRoleAssignments.value to false when roles already exist
|
|
138
|
+
throw new Error(`The signed-in Azure account cannot create role assignments in subscription ${doc.subscriptionId.value}; ask for Owner or User Access Administrator, or set parameters.deployRoleAssignments.value to false when roles already exist`);
|
|
133
139
|
}
|
|
140
|
+
// The AcrPull grant reaches into the registry's own resource group, which a customer
|
|
141
|
+
// installation cannot touch at all. Credential mode does not request it, so do not require
|
|
142
|
+
// permission for it.
|
|
143
|
+
if (isAdminAcrAuth(doc)) return;
|
|
144
|
+
if (doc.parameters.deployRegistryRoleAssignment?.value === false) return;
|
|
134
145
|
if (!hasPermission(az, acr.id, ROLE_ASSIGNMENT_WRITE)) {
|
|
135
146
|
throw new Error(`The signed-in Azure account cannot assign AcrPull on registry ${doc.acrName.value}; ask for Owner or User Access Administrator on that registry, or use --acr-auth credentials with a pair saved by "aifabrix dev set-registry"`);
|
|
136
147
|
}
|