@aifabrix/builder 2.59.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/git-identity.js +180 -0
- package/lib/agent-kit/setup.js +3 -0
- package/lib/agent-kit/start.js +45 -1
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -1
- 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/setup-app.js +14 -1
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/role-assistant.js +7 -0
- package/lib/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- package/lib/deployment/installation/azure-infra-stage.js +3 -1
- package/lib/deployment/installation/infra-catalog.js +2 -5
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +10 -6
- 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/infra-parameter.schema.json +139 -33
- package/lib/schema/infra.parameter.yaml +28 -28
- package/lib/schema/infrastructure-schema.json +10 -35
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/package.json +2 -2
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
- package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
- 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 +13 -3
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- 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 +21 -34
- package/templates/marketplace/main.json +70 -222
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git commit identity for onboarding and Agent Kit GitHub work.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. Existing `git config` — never rewritten, because it is the user's own setting.
|
|
6
|
+
* 2. The AI Fabrix login token's claims, offered as editable defaults.
|
|
7
|
+
* 3. The prompt answer, written to git config.
|
|
8
|
+
*
|
|
9
|
+
* The GitHub token is deliberately not used for the address: `gh api user` returns null whenever
|
|
10
|
+
* the account keeps its email private, and reading `user/emails` would demand the broader `user`
|
|
11
|
+
* scope for a value the login token already holds.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Resolve and persist git user.name / user.email
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
const logger = require('../utils/logger');
|
|
22
|
+
|
|
23
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Function} exec - execFileSync
|
|
27
|
+
* @param {string} key - Config key
|
|
28
|
+
* @param {boolean} global - Read the global scope only
|
|
29
|
+
* @returns {string} Trimmed value or ''
|
|
30
|
+
*/
|
|
31
|
+
function readConfig(exec, key, global) {
|
|
32
|
+
try {
|
|
33
|
+
const args = global ? ['config', '--global', '--get', key] : ['config', '--get', key];
|
|
34
|
+
return String(exec('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Current identity, preferring any effective value (local or global) over none.
|
|
42
|
+
* @param {Object} [options] - `execFileSync`
|
|
43
|
+
* @returns {{ name: string, email: string, complete: boolean }}
|
|
44
|
+
*/
|
|
45
|
+
function readGitIdentity(options = {}) {
|
|
46
|
+
const exec = options.execFileSync || execFileSync;
|
|
47
|
+
const name = readConfig(exec, 'user.name', false);
|
|
48
|
+
const email = readConfig(exec, 'user.email', false);
|
|
49
|
+
return { name, email, complete: Boolean(name && email) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodeJwtClaims(token) {
|
|
53
|
+
const raw = String(token || '');
|
|
54
|
+
const parts = raw.split('.');
|
|
55
|
+
if (parts.length < 2) return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Name and email from the AI Fabrix login token, used as prompt defaults.
|
|
65
|
+
* @param {Object} [options] - `config` and `controller` overrides for tests
|
|
66
|
+
* @returns {Promise<{ name: string, email: string }>} Empty strings when unavailable
|
|
67
|
+
*/
|
|
68
|
+
async function identityFromLoginToken(options = {}) {
|
|
69
|
+
const config = options.config || require('../core/config');
|
|
70
|
+
try {
|
|
71
|
+
const controller = options.controller || (await config.getControllerUrl());
|
|
72
|
+
if (!controller) return { name: '', email: '' };
|
|
73
|
+
const entry = await config.getDeviceToken(controller);
|
|
74
|
+
const claims = decodeJwtClaims(entry && (entry.token || entry.accessToken));
|
|
75
|
+
if (!claims) return { name: '', email: '' };
|
|
76
|
+
return {
|
|
77
|
+
name: String(claims.name || '').trim(),
|
|
78
|
+
email: String(claims.email || claims.preferred_username || '').trim()
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return { name: '', email: '' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {Object} [options] - `config`
|
|
87
|
+
* @returns {Promise<string>} adminEmail from config.yaml, or ''
|
|
88
|
+
*/
|
|
89
|
+
async function emailFromConfig(options = {}) {
|
|
90
|
+
const config = options.config || require('../core/config');
|
|
91
|
+
try {
|
|
92
|
+
const cfg = await config.getConfig();
|
|
93
|
+
return String((cfg && cfg.adminEmail) || '').trim();
|
|
94
|
+
} catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} value - Candidate address
|
|
101
|
+
* @returns {boolean|string} true, or the reason it is rejected
|
|
102
|
+
*/
|
|
103
|
+
function validateEmail(value) {
|
|
104
|
+
return EMAIL_RE.test(String(value || '').trim()) || 'Enter an email address, for example name@example.com';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateName(value) {
|
|
108
|
+
return String(value || '').trim().length > 0 || 'Enter the name to record on commits';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function promptIdentity(defaults, deps) {
|
|
112
|
+
const inquirer = deps.inquirer || require('inquirer');
|
|
113
|
+
return inquirer.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'name',
|
|
117
|
+
message: 'Name for git commits',
|
|
118
|
+
default: defaults.name || undefined,
|
|
119
|
+
validate: validateName
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'email',
|
|
124
|
+
message: 'Email for git commits',
|
|
125
|
+
default: defaults.email || undefined,
|
|
126
|
+
validate: validateEmail
|
|
127
|
+
}
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeGitIdentity(identity, options) {
|
|
132
|
+
const exec = options.execFileSync || execFileSync;
|
|
133
|
+
exec('git', ['config', '--global', 'user.name', identity.name], { stdio: 'ignore' });
|
|
134
|
+
exec('git', ['config', '--global', 'user.email', identity.email], { stdio: 'ignore' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function missingIdentityError(defaults) {
|
|
138
|
+
const name = defaults.name || 'Your Name';
|
|
139
|
+
const email = defaults.email || 'you@example.com';
|
|
140
|
+
return new Error(
|
|
141
|
+
'Git commit identity is not configured. Run:\n' +
|
|
142
|
+
` git config --global user.name ${JSON.stringify(name)}\n` +
|
|
143
|
+
` git config --global user.email ${email}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Ensures git has a commit identity, asking only when one is missing.
|
|
149
|
+
* @param {Object} [options] - `interactive`, `execFileSync`, `inquirer`, `config`
|
|
150
|
+
* @returns {Promise<{ name: string, email: string, source: 'git'|'prompt' }>}
|
|
151
|
+
*/
|
|
152
|
+
async function ensureGitIdentity(options = {}) {
|
|
153
|
+
const current = readGitIdentity(options);
|
|
154
|
+
if (current.complete) {
|
|
155
|
+
return { name: current.name, email: current.email, source: 'git' };
|
|
156
|
+
}
|
|
157
|
+
const token = await identityFromLoginToken(options);
|
|
158
|
+
const defaults = {
|
|
159
|
+
name: current.name || token.name,
|
|
160
|
+
email: current.email || token.email || (await emailFromConfig(options))
|
|
161
|
+
};
|
|
162
|
+
if (!options.interactive) {
|
|
163
|
+
throw missingIdentityError(defaults);
|
|
164
|
+
}
|
|
165
|
+
logger.log('Git commit identity is not set. Defaults come from your AI Fabrix login; edit if needed.');
|
|
166
|
+
const answers = await promptIdentity(defaults, options);
|
|
167
|
+
const identity = { name: String(answers.name).trim(), email: String(answers.email).trim() };
|
|
168
|
+
writeGitIdentity(identity, options);
|
|
169
|
+
logger.log(`Git identity set to ${identity.name} <${identity.email}>`);
|
|
170
|
+
return { ...identity, source: 'prompt' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
readGitIdentity,
|
|
175
|
+
identityFromLoginToken,
|
|
176
|
+
emailFromConfig,
|
|
177
|
+
ensureGitIdentity,
|
|
178
|
+
validateEmail,
|
|
179
|
+
validateName
|
|
180
|
+
};
|
package/lib/agent-kit/setup.js
CHANGED
|
@@ -8,6 +8,7 @@ const { auditLog } = require('../core/audit-logger');
|
|
|
8
8
|
const { normalizeSetup, preflight, resolveWork } = require('./setup-context');
|
|
9
9
|
const { acquireWorkspace } = require('./setup-state');
|
|
10
10
|
const { ensureController, ensureGithubLogin, ensureProvider } = require('./setup-auth');
|
|
11
|
+
const { ensureGitIdentity } = require('./git-identity');
|
|
11
12
|
const { prepareWorkspace, validateExisting } = require('./setup-workspace');
|
|
12
13
|
const { prepareSession, attachSession } = require('./setup-session');
|
|
13
14
|
const { managedSetupOptions } = require('./setup-managed');
|
|
@@ -19,6 +20,8 @@ async function authenticate(context, options, setPhase) {
|
|
|
19
20
|
if (context.fullName) {
|
|
20
21
|
setPhase('github-login');
|
|
21
22
|
ensureGithubLogin(options);
|
|
23
|
+
// Commits need an author. The login token supplies the default; the user can change it.
|
|
24
|
+
await ensureGitIdentity({ ...options, interactive: options.interactive !== false });
|
|
22
25
|
}
|
|
23
26
|
if (context.code === 'cursor') return null;
|
|
24
27
|
setPhase('agent-login');
|
package/lib/agent-kit/start.js
CHANGED
|
@@ -116,6 +116,27 @@ function claudeInnerArgs(p) {
|
|
|
116
116
|
]);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* POSIX single-quote escaping for a value sent to a shell via tmux send-keys.
|
|
121
|
+
* @param {string} value - Raw value
|
|
122
|
+
* @returns {string} Quoted value
|
|
123
|
+
*/
|
|
124
|
+
function shellQuote(value) {
|
|
125
|
+
const escaped = String(value).split('\'').join('\'\\\'\'');
|
|
126
|
+
return `'${escaped}'`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Codex authenticates inside the session, not in the window that launched it. Running it as the
|
|
131
|
+
* tmux session command makes the session end with the sign-in process, so the shell owns the
|
|
132
|
+
* window and codex is started inside it with send-keys.
|
|
133
|
+
* @param {object} p - Start parameters
|
|
134
|
+
* @returns {string} Shell command to run inside the session
|
|
135
|
+
*/
|
|
136
|
+
function codexStartCommand(p) {
|
|
137
|
+
return p.binary ? `exec ${shellQuote(p.binary)}` : 'exec codex';
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
/**
|
|
120
141
|
* @param {object} p
|
|
121
142
|
* @returns {object}
|
|
@@ -127,12 +148,17 @@ function buildTmuxSpec(p) {
|
|
|
127
148
|
? ['new-session', '-d', '-s', p.session, '-c', p.cwd]
|
|
128
149
|
: ['new-session', '-s', p.session, '-c', p.cwd];
|
|
129
150
|
const remote = p.app === 'claude' && p.remoteControl;
|
|
151
|
+
// codex: shell owns the window, codex starts inside it (see codexStartCommand).
|
|
152
|
+
const launchArgs = p.app === 'codex'
|
|
153
|
+
? [...mode]
|
|
154
|
+
: [...mode, ...(p.binary ? ['env', p.binary] : [spec.bin]), ...extra];
|
|
130
155
|
return {
|
|
131
156
|
bin: 'tmux',
|
|
132
|
-
args:
|
|
157
|
+
args: launchArgs,
|
|
133
158
|
session: p.session,
|
|
134
159
|
appBin: spec.bin,
|
|
135
160
|
needTmux: true,
|
|
161
|
+
innerCommand: p.app === 'codex' ? codexStartCommand(p) : undefined,
|
|
136
162
|
remoteControlName: remote ? p.remoteControlName : undefined,
|
|
137
163
|
remoteControlPrefix: remote ? p.remoteControlPrefix : undefined
|
|
138
164
|
};
|
|
@@ -333,6 +359,22 @@ function executeStart(options, context, execute) {
|
|
|
333
359
|
startCodexRemote(options, context.remoteSpec);
|
|
334
360
|
}
|
|
335
361
|
spawnLaunch(options, context.spec);
|
|
362
|
+
sendInnerCommand(options, context.spec);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Starts the agent inside an existing session window. Only codex uses this; the session must exist
|
|
367
|
+
* first, which is why it runs after the launch.
|
|
368
|
+
* @param {object} options - Start options
|
|
369
|
+
* @param {object} spec - Launch spec
|
|
370
|
+
* @returns {void}
|
|
371
|
+
*/
|
|
372
|
+
function sendInnerCommand(options, spec) {
|
|
373
|
+
if (!spec.innerCommand) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const exec = options.execFileSync || execFileSync;
|
|
377
|
+
exec('tmux', ['send-keys', '-t', `=${spec.session}`, spec.innerCommand, 'Enter'], { stdio: 'inherit' });
|
|
336
378
|
}
|
|
337
379
|
|
|
338
380
|
/**
|
|
@@ -363,6 +405,8 @@ function runStart(options) {
|
|
|
363
405
|
|
|
364
406
|
module.exports = {
|
|
365
407
|
START_APPS,
|
|
408
|
+
codexStartCommand,
|
|
409
|
+
sendInnerCommand,
|
|
366
410
|
normalizeStartApp,
|
|
367
411
|
sanitizeSessionName,
|
|
368
412
|
resolveSessionName,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataplane asynchronous Role Assistant test job API.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Enqueue and poll published Role Assistant cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { createDataplaneApiClient } = require('./index');
|
|
10
|
+
const { throwIfApiFailed } = require('./api-response-errors');
|
|
11
|
+
const { unwrapApiData } = require('../utils/external-system-readiness-core');
|
|
12
|
+
|
|
13
|
+
function jobPath(systemIdOrKey, suffix = '') {
|
|
14
|
+
const key = encodeURIComponent(String(systemIdOrKey || '').trim());
|
|
15
|
+
return `/api/v1/external/systems/${key}/role-assistant-tests/jobs${suffix}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unwrapJob(res) {
|
|
19
|
+
const value = unwrapApiData(res);
|
|
20
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && value.data) {
|
|
21
|
+
return value.data;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function enqueueRoleAssistantTestJob(
|
|
27
|
+
dataplaneUrl,
|
|
28
|
+
authConfig,
|
|
29
|
+
systemIdOrKey,
|
|
30
|
+
body
|
|
31
|
+
) {
|
|
32
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
33
|
+
const path = jobPath(systemIdOrKey);
|
|
34
|
+
const res = await client.post(path, { body, timeoutMs: 60000 });
|
|
35
|
+
throwIfApiFailed(res, {
|
|
36
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
37
|
+
});
|
|
38
|
+
return unwrapJob(res);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getRoleAssistantTestJob(
|
|
42
|
+
dataplaneUrl,
|
|
43
|
+
authConfig,
|
|
44
|
+
systemIdOrKey,
|
|
45
|
+
jobId
|
|
46
|
+
) {
|
|
47
|
+
const suffix = `/${encodeURIComponent(String(jobId || '').trim())}`;
|
|
48
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
49
|
+
const path = jobPath(systemIdOrKey, suffix);
|
|
50
|
+
const res = await client.get(path, { timeoutMs: 60000 });
|
|
51
|
+
throwIfApiFailed(res, {
|
|
52
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
53
|
+
});
|
|
54
|
+
return unwrapJob(res);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
enqueueRoleAssistantTestJob,
|
|
59
|
+
getRoleAssistantTestJob
|
|
60
|
+
};
|
|
@@ -121,16 +121,23 @@ async function getWorkRoles(dataplaneUrl, authConfig) {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* GET-equivalent work.skills for one roleRef.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* Defaults to detail=full so search.resourceTypes (executable grammar) is present.
|
|
125
|
+
* A caller may request progressive resource/operation detail for skills-only checks.
|
|
126
126
|
* @requiresPermission {Dataplane} Authenticated (oauth2: [])
|
|
127
127
|
* @async
|
|
128
128
|
*/
|
|
129
|
-
async function getWorkSkills(dataplaneUrl, authConfig, roleRef) {
|
|
130
|
-
|
|
129
|
+
async function getWorkSkills(dataplaneUrl, authConfig, roleRef, options = {}) {
|
|
130
|
+
const args = {
|
|
131
131
|
roleRef: String(roleRef || '').trim(),
|
|
132
|
-
detail: 'full'
|
|
133
|
-
}
|
|
132
|
+
detail: String(options.detail || 'full').trim()
|
|
133
|
+
};
|
|
134
|
+
if (typeof options.resourceType === 'string' && options.resourceType.trim()) {
|
|
135
|
+
args.resourceType = options.resourceType.trim();
|
|
136
|
+
}
|
|
137
|
+
if (typeof options.operation === 'string' && options.operation.trim()) {
|
|
138
|
+
args.operation = options.operation.trim();
|
|
139
|
+
}
|
|
140
|
+
return executeLayerBTool(dataplaneUrl, authConfig, LAYER_B_TOOLS.skills, args);
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
/**
|
package/lib/app/deploy.js
CHANGED
|
@@ -268,7 +268,14 @@ async function executeStandardDeployment(appName, options) {
|
|
|
268
268
|
const controllerUrl = config.controllerUrl || 'unknown';
|
|
269
269
|
const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
// A controller deployment ships tokens plus the platforms/environments sections, so the
|
|
272
|
+
// Controller resolves them per installation and can promote dev -> tst without regenerating.
|
|
273
|
+
// Local runs and an explicit --resource-group resolve here instead.
|
|
274
|
+
const resolveHere = Boolean(options.local || options.resourceGroup);
|
|
275
|
+
const { manifest, manifestPath } = await generateAndValidateManifest(appName, {
|
|
276
|
+
...options,
|
|
277
|
+
...(resolveHere ? { target: { environment: config.envKey, resourceGroup: options.resourceGroup } } : {})
|
|
278
|
+
});
|
|
272
279
|
applyManifestOverrides(manifest, options);
|
|
273
280
|
validateImageIsPullable(manifest.image, appName);
|
|
274
281
|
displayDeploymentInfo(manifest, manifestPath);
|
package/lib/app/show-display.js
CHANGED
|
@@ -234,6 +234,7 @@ function logExternalSystemMain(ext) {
|
|
|
234
234
|
logger.log(` Version: ${ext.version ?? '—'}`);
|
|
235
235
|
logger.log(` Credential: ${formatCredentialShowLine(ext)}`);
|
|
236
236
|
logger.log(` Status: ${formatDataplaneStatusDisplay(ext.status)}`);
|
|
237
|
+
logger.log(` System link: ${ext.systemUrl ?? '—'}`);
|
|
237
238
|
logger.log(` API docs: ${ext.openApiDocsPageUrl ?? ext.apiDocumentUrl ?? '—'}`);
|
|
238
239
|
logger.log(` MCP server: ${ext.mcpServerUrl ?? '—'}`);
|
|
239
240
|
logger.log(` OpenAPI spec: ${ext.apiDocumentUrl ?? '—'}`);
|
package/lib/app/show-online.js
CHANGED
|
@@ -89,6 +89,7 @@ function buildOnlineSummaryFromDataplane(appKey, controllerUrl, dataplaneUrl, ex
|
|
|
89
89
|
|
|
90
90
|
async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCert, authBundle) {
|
|
91
91
|
await enrichExternalShowSummary(summary, appKey, verifyCert, authBundle);
|
|
92
|
+
attachSystemUrl(summary);
|
|
92
93
|
attachRunDefaultsFromUserConfig(summary, appKey, await getConfig());
|
|
93
94
|
await attachDeclarativeUrlsToShowApplication(summary, appKey);
|
|
94
95
|
if (json) {
|
|
@@ -100,6 +101,20 @@ async function finishOnlineShow(summary, appKey, json, permissionsOnly, verifyCe
|
|
|
100
101
|
displayShow(summary, { permissionsOnly: !!permissionsOnly });
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Expose the Dataplane system browser route for humans and agents.
|
|
106
|
+
* Preserve the deployment base path (for example /data).
|
|
107
|
+
* @param {Object} summary - Online application summary
|
|
108
|
+
*/
|
|
109
|
+
function attachSystemUrl(summary) {
|
|
110
|
+
const ext = summary.externalSystem;
|
|
111
|
+
if (!summary.isExternal || !ext || ext.error || !ext.dataplaneUrl) return;
|
|
112
|
+
const baseUrl = ext.dataplaneUrl.replace(/\/+$/, '');
|
|
113
|
+
const systemKey = ext.systemKey || summary.appKey;
|
|
114
|
+
const section = /^(ra-|ek-)/.test(systemKey) ? 'enterprise-knowledge' : 'connected-systems';
|
|
115
|
+
ext.systemUrl = `${baseUrl}/${section}/${encodeURIComponent(systemKey)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
async function runOnlineFromDataplane(appKey, json, permissionsOnly, verifyCert, authConfig, authResult) {
|
|
104
119
|
const environment = await resolveEnvironment();
|
|
105
120
|
const dataplaneUrl = await resolveDataplaneUrl(authResult.actualControllerUrl, environment, authConfig);
|
|
@@ -81,13 +81,16 @@ async function executeDockerImageBuild(appName, options, buildConfig) {
|
|
|
81
81
|
await ensureRunSecretsForApp(appName);
|
|
82
82
|
const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath);
|
|
83
83
|
const { primary, aliases, tags } = options.tagPlan;
|
|
84
|
+
// `build.target` selects a stage of a multi-stage Dockerfile, so one source tree can
|
|
85
|
+
// produce more than one image. Absent, Docker builds the last stage as before.
|
|
86
|
+
const target = buildConfig && buildConfig.target;
|
|
84
87
|
await dockerBuild.executeDockerBuildWithTag(
|
|
85
88
|
effectiveImageName,
|
|
86
89
|
imageName,
|
|
87
90
|
dockerfilePath,
|
|
88
91
|
contextPath,
|
|
89
92
|
primary,
|
|
90
|
-
{ ...options, buildArgs, tagAliases: aliases }
|
|
93
|
+
{ ...options, buildArgs, tagAliases: aliases, target }
|
|
91
94
|
);
|
|
92
95
|
await postBuildTasks(appName, buildConfig);
|
|
93
96
|
logger.log(formatSuccessParagraph('Build completed successfully!'));
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -362,7 +362,7 @@ function setupDockerfileGenerateCommand(program) {
|
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
function setupDeployCommand(program) {
|
|
365
|
-
program.command('deploy <appKey|systemKey>')
|
|
365
|
+
addDeployTargetOptions(program.command('deploy <appKey|systemKey>'))
|
|
366
366
|
.description('Deploy builder app or external system via Miso Controller (Azure or --local)')
|
|
367
367
|
.addHelpText('after', DEPLOY_HELP_AFTER)
|
|
368
368
|
.option('--local', 'Send manifest to controller then run app locally (app: same as aifabrix run <app>; external: restart dataplane)')
|
|
@@ -410,6 +410,19 @@ function setupDeployCommand(program) {
|
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Deployment-target options. A controller deployment normally ships {NAME} tokens plus the
|
|
415
|
+
* platforms/environments sections; these flags force local resolution instead.
|
|
416
|
+
* @param {Command} command - Commander command
|
|
417
|
+
* @returns {Command} The command
|
|
418
|
+
*/
|
|
419
|
+
function addDeployTargetOptions(command) {
|
|
420
|
+
return command.option(
|
|
421
|
+
'-g, --resource-group <name>',
|
|
422
|
+
'Target Azure resource group; resolves platforms values in the generated manifest instead of shipping tokens'
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
413
426
|
function setupPushDeployDockerfileCommands(program) {
|
|
414
427
|
program.command('push <appKey>')
|
|
415
428
|
.description('Push builder appKey image to Azure Container Registry')
|
package/lib/cli/setup-utility.js
CHANGED
|
@@ -65,6 +65,23 @@ async function handleSplitJsonCommand(appName, options) {
|
|
|
65
65
|
return generator.splitDeployJson(deployJsonPath, outputDir);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Deployment target for manifest generation. A resource group resolves platform values; the
|
|
70
|
+
* environment key resolves the environments layer. Without either, {NAME} tokens are preserved.
|
|
71
|
+
* @param {Object} options - Command options
|
|
72
|
+
* @returns {Promise<{ resourceGroup: string|undefined, environment: string }>}
|
|
73
|
+
*/
|
|
74
|
+
async function buildGenerationTarget(options = {}) {
|
|
75
|
+
// Only an explicit target resolves. The default artifact keeps {NAME} tokens and carries the
|
|
76
|
+
// platforms/environments sections, so the Controller resolves them per installation.
|
|
77
|
+
if (!options.resourceGroup && !options.env) return undefined;
|
|
78
|
+
const { resolveEnvironment } = require('../core/config');
|
|
79
|
+
const environment = options.env
|
|
80
|
+
? String(options.env).trim().toLowerCase()
|
|
81
|
+
: await resolveEnvironment();
|
|
82
|
+
return { resourceGroup: options.resourceGroup, environment };
|
|
83
|
+
}
|
|
84
|
+
|
|
68
85
|
/**
|
|
69
86
|
* Logs split-json results
|
|
70
87
|
* @param {Object} result - Generated file paths
|
|
@@ -93,6 +110,11 @@ function logSplitJsonResult(result) {
|
|
|
93
110
|
function setupJsonCommand(program) {
|
|
94
111
|
program.command('json <appKey|systemKey>')
|
|
95
112
|
.description('Write deployment JSON to disk for version control')
|
|
113
|
+
.option(
|
|
114
|
+
'-g, --resource-group <name>',
|
|
115
|
+
'Target Azure resource group; resolves platforms/environments values instead of leaving {NAME} tokens'
|
|
116
|
+
)
|
|
117
|
+
.option('-e, --env <env>', 'Deployment environment key for environments resolution (default: current environment)')
|
|
96
118
|
.addHelpText('after', JSON_HELP_AFTER)
|
|
97
119
|
.action(async(appName, options) => {
|
|
98
120
|
try {
|
|
@@ -105,7 +127,8 @@ function setupJsonCommand(program) {
|
|
|
105
127
|
json: false
|
|
106
128
|
});
|
|
107
129
|
const result = await generator.generateDeployJsonWithValidation(appName, {
|
|
108
|
-
...options
|
|
130
|
+
...options,
|
|
131
|
+
target: await buildGenerationTarget(options)
|
|
109
132
|
});
|
|
110
133
|
if (result.success) {
|
|
111
134
|
const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
|
|
@@ -41,6 +41,10 @@ const {
|
|
|
41
41
|
patchCaseStatusAfterRun
|
|
42
42
|
} = require('../role-assistant/test-pack-sync');
|
|
43
43
|
const { collectString } = require('./governance');
|
|
44
|
+
const {
|
|
45
|
+
hasRoleAssistantTestSubject,
|
|
46
|
+
runPublishedCaseAsSubject
|
|
47
|
+
} = require('../role-assistant/test-job-runner');
|
|
44
48
|
const {
|
|
45
49
|
sectionTitle,
|
|
46
50
|
headerKeyValue,
|
|
@@ -338,6 +342,9 @@ async function resolveExecutableCaseResult(roleKey, roleCase, opts, result, verd
|
|
|
338
342
|
run: false,
|
|
339
343
|
noPublish: true
|
|
340
344
|
});
|
|
345
|
+
if (hasRoleAssistantTestSubject(opts)) {
|
|
346
|
+
return runPublishedCaseAsSubject(roleKey, roleCase, opts, authContext);
|
|
347
|
+
}
|
|
341
348
|
const evidencePreflight = await assertRoleAssistantEvidencePreflight(roleCase, authContext);
|
|
342
349
|
const executionResult = await runRoleAssistantCaseLifecycle(roleKey, roleCase, authContext, opts);
|
|
343
350
|
const executionVerdict = lifecycleVerdictFromCaseExecution(executionResult, verdict);
|
|
@@ -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
|
+
};
|