@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
|
@@ -81,6 +81,41 @@ async function mergeDockerManifestPublishedPort(envVars, appDoc) {
|
|
|
81
81
|
envVars[publicPortKey] = String(devIdNum > 0 ? pubBase + devIdNum * 100 : pubBase);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Publish `<NAME>_PUBLIC_PORT` for every `additionalPorts` entry.
|
|
86
|
+
*
|
|
87
|
+
* An application serving a second protocol needs to tell clients which port to use, and
|
|
88
|
+
* that is the *published* host port, not the one it listens on inside the container. The
|
|
89
|
+
* two differ by the developer-id offset, so a template that types the number by hand
|
|
90
|
+
* silently disagrees with the manifest the moment either changes — and the application
|
|
91
|
+
* then advertises a port nothing is listening on.
|
|
92
|
+
*
|
|
93
|
+
* Named after the port, so `additionalPorts: [{ name: ssh, hostPort: 2200 }]` gives
|
|
94
|
+
* `SSH_PUBLIC_PORT`, which an env.template consumes as `${SSH_PUBLIC_PORT}`.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} envVars - Mutated map from buildEnvVarMap
|
|
97
|
+
* @param {Object|null|undefined} appDoc - application.yaml root
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
async function mergeAdditionalPublishedPorts(envVars, appDoc) {
|
|
101
|
+
const entries = appDoc && Array.isArray(appDoc.additionalPorts) ? appDoc.additionalPorts : [];
|
|
102
|
+
if (entries.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const devIdNum = await require('./env-map').getDeveloperIdNumber(null);
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (!entry || typeof entry.name !== 'string') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const base = Number(entry.hostPort);
|
|
111
|
+
if (!Number.isFinite(base)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = `${entry.name.trim().toUpperCase().replace(/-/g, '_')}_PUBLIC_PORT`;
|
|
115
|
+
envVars[key] = String(devIdNum > 0 ? base + devIdNum * 100 : base);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
/**
|
|
85
120
|
* Rewrite first line for the matched *_PUBLIC_PORT after interpolateEnvVars (early kv pass may have wrong value).
|
|
86
121
|
* @param {string} resolved
|
|
@@ -111,6 +146,7 @@ function rewriteDockerManifestPublicPortEnvLine(resolved, envVars, appDoc) {
|
|
|
111
146
|
module.exports = {
|
|
112
147
|
findDockerHostKeyForAppKey,
|
|
113
148
|
publicPortKeyForAppKey,
|
|
149
|
+
mergeAdditionalPublishedPorts,
|
|
114
150
|
mergeDockerManifestPublishedPort,
|
|
115
151
|
rewriteDockerManifestPublicPortEnvLine
|
|
116
152
|
};
|
|
@@ -11,6 +11,26 @@ const { formatBlockingError } = require('../cli-test-layout-chalk');
|
|
|
11
11
|
|
|
12
12
|
const chalk = require('chalk');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Resolves legacy application lookup codes without replacing API explanations.
|
|
16
|
+
* @param {Object|string} error - Validation error entry
|
|
17
|
+
* @returns {{message: string, applicationNotFound: boolean}} Display information
|
|
18
|
+
*/
|
|
19
|
+
function getValidationMessage(error) {
|
|
20
|
+
const message = typeof error === 'string' ? error : error.msg || error.message;
|
|
21
|
+
const code = typeof error === 'object' ? error.code : undefined;
|
|
22
|
+
const applicationNotFound = message === 'APPLICATION_NOT_FOUND' || code === 'APPLICATION_NOT_FOUND';
|
|
23
|
+
if (applicationNotFound && (!message || message === 'APPLICATION_NOT_FOUND')) {
|
|
24
|
+
return {
|
|
25
|
+
message: 'The controller could not find the application. ' +
|
|
26
|
+
'Check that it is registered on the selected controller and that its application key matches. ' +
|
|
27
|
+
'(Error code: APPLICATION_NOT_FOUND)',
|
|
28
|
+
applicationNotFound
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { message: message || 'Invalid value', applicationNotFound };
|
|
32
|
+
}
|
|
33
|
+
|
|
14
34
|
/**
|
|
15
35
|
* Adds main error message to lines array
|
|
16
36
|
* @param {Array<string>} lines - Lines array to append to
|
|
@@ -52,7 +72,7 @@ function addValidationErrorsList(lines, errors) {
|
|
|
52
72
|
lines.push(chalk.yellow('Validation errors:'));
|
|
53
73
|
errors.forEach(err => {
|
|
54
74
|
const field = err.field || err.path || err.loc?.join('.') || 'validation';
|
|
55
|
-
const message = err
|
|
75
|
+
const { message } = getValidationMessage(err);
|
|
56
76
|
const value = err.value !== undefined ? ` (value: ${JSON.stringify(err.value)})` : '';
|
|
57
77
|
if (field === 'validation' || field === 'unknown') {
|
|
58
78
|
lines.push(chalk.gray(` • ${message}${value}`));
|
|
@@ -91,10 +111,10 @@ function addConfigurationErrors(lines, configuration) {
|
|
|
91
111
|
/**
|
|
92
112
|
* Adds actionable guidance tips to lines
|
|
93
113
|
* @param {Array<string>} lines - Lines array to append to
|
|
94
|
-
* @param {
|
|
114
|
+
* @param {Array<Object|string>} errors - Validation errors
|
|
95
115
|
*/
|
|
96
|
-
function addValidationGuidance(lines,
|
|
97
|
-
if (!
|
|
116
|
+
function addValidationGuidance(lines, errors) {
|
|
117
|
+
if (!Array.isArray(errors) || !errors.some(err => !getValidationMessage(err).applicationNotFound)) {
|
|
98
118
|
return;
|
|
99
119
|
}
|
|
100
120
|
lines.push(chalk.gray('Tips:'));
|
|
@@ -117,7 +137,7 @@ function formatValidationError(errorData) {
|
|
|
117
137
|
addValidationErrorMessage(lines, errorData);
|
|
118
138
|
addValidationErrorsList(lines, errorData.errors);
|
|
119
139
|
addConfigurationErrors(lines, errorData.configuration);
|
|
120
|
-
addValidationGuidance(lines, errorData.errors
|
|
140
|
+
addValidationGuidance(lines, errorData.errors);
|
|
121
141
|
|
|
122
142
|
if (errorData.instance) {
|
|
123
143
|
lines.push(chalk.gray(`Endpoint: ${errorData.instance}`));
|
|
@@ -17,6 +17,7 @@ const { Help } = require('commander');
|
|
|
17
17
|
* @type {Array<{ name: string, commands: Array<{ name: string, term?: string }> }>}
|
|
18
18
|
*/
|
|
19
19
|
const CATEGORIES = [
|
|
20
|
+
{ name: 'Getting Started', commands: [{ name: 'help', term: 'help [command]' }, { name: 'onboarding' }] },
|
|
20
21
|
{
|
|
21
22
|
name: 'Infrastructure (Local Development)',
|
|
22
23
|
commands: [
|
|
@@ -209,9 +210,9 @@ function formatCommandCategories(helper, program) {
|
|
|
209
210
|
for (const { term, desc: d } of lines) out.push(` ${term.padEnd(pad)}${d}`);
|
|
210
211
|
out.push('');
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
if (!program.commands.some(command => command.name() === 'help')) {
|
|
214
|
+
out.push('Help:', ` ${'help [command]'.padEnd(pad)}display help for command`, '');
|
|
215
|
+
}
|
|
215
216
|
out.push(` ${''.padEnd(pad)}Tip: aifabrix <command> --help for flags and examples`);
|
|
216
217
|
out.push('');
|
|
217
218
|
return out;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform and environment configuration resolution (plan 200.0).
|
|
3
|
+
*
|
|
4
|
+
* The deployment's Azure resource group is matched against the manifest `platforms` list, yielding
|
|
5
|
+
* `{platform}` and `{platform.name}`. Platform values expand from those, then the matching
|
|
6
|
+
* `environments` entry overrides by name. The result is one flat set of names that `env.template`
|
|
7
|
+
* references as `{NAME}`.
|
|
8
|
+
*
|
|
9
|
+
* Pure by contract: manifest + resource group + environment key in, resolved set out. No
|
|
10
|
+
* filesystem, no Azure, no network — the same function must be implementable in Miso and produce
|
|
11
|
+
* identical output for the shared test vectors.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Resource-group driven configuration resolution
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
/** Local Docker has no resource group; every local run resolves as this platform. */
|
|
21
|
+
const LOCAL_PLATFORM = 'dev';
|
|
22
|
+
/** Capture for `*` in a resource pattern: one group-name segment. */
|
|
23
|
+
const NAME_CAPTURE = '[A-Za-z0-9][A-Za-z0-9-]*';
|
|
24
|
+
const NAME_WILDCARD = '*';
|
|
25
|
+
|
|
26
|
+
function escapeLiteral(text) {
|
|
27
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compiles a `resource` pattern into a matcher.
|
|
32
|
+
* `{platform}` matches one of `options`; `*` captures a name segment.
|
|
33
|
+
* @param {Object} entry - Platform entry
|
|
34
|
+
* @returns {{ regex: RegExp, order: string[] }} Matcher and the capture order
|
|
35
|
+
*/
|
|
36
|
+
function compileResourcePattern(entry) {
|
|
37
|
+
const order = [];
|
|
38
|
+
let source = '';
|
|
39
|
+
const pattern = String(entry.resource || '');
|
|
40
|
+
const tokens = pattern.split(/(\{platform\}|\*)/);
|
|
41
|
+
for (const token of tokens) {
|
|
42
|
+
if (token === '{platform}') {
|
|
43
|
+
const options = Array.isArray(entry.options) ? entry.options : [];
|
|
44
|
+
source += `(${options.map(escapeLiteral).join('|')})`;
|
|
45
|
+
order.push('platform');
|
|
46
|
+
} else if (token === NAME_WILDCARD) {
|
|
47
|
+
source += `(${NAME_CAPTURE})`;
|
|
48
|
+
order.push('name');
|
|
49
|
+
} else if (token) {
|
|
50
|
+
source += escapeLiteral(token);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { regex: new RegExp(`^${source}$`), order };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} entry - Platform entry
|
|
58
|
+
* @throws {Error} When the entry cannot yield a platform
|
|
59
|
+
*/
|
|
60
|
+
function assertPlatformDeclaration(entry) {
|
|
61
|
+
const hasToken = String(entry.resource || '').includes('{platform}');
|
|
62
|
+
const hasOptions = Array.isArray(entry.options) && entry.options.length > 0;
|
|
63
|
+
if (hasToken && !hasOptions) {
|
|
64
|
+
throw new Error(`Platform "${entry.resource}" uses {platform} but declares no options`);
|
|
65
|
+
}
|
|
66
|
+
if (!hasToken && !entry.platform) {
|
|
67
|
+
throw new Error(`Platform "${entry.resource}" has no {platform} token, so it must declare platform:`);
|
|
68
|
+
}
|
|
69
|
+
if (hasToken && entry.platform) {
|
|
70
|
+
throw new Error(`Platform "${entry.resource}" declares both {platform} and platform:; use one`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchEntry(entry, resourceGroup) {
|
|
75
|
+
assertPlatformDeclaration(entry);
|
|
76
|
+
const { regex, order } = compileResourcePattern(entry);
|
|
77
|
+
const found = regex.exec(resourceGroup);
|
|
78
|
+
if (!found) return null;
|
|
79
|
+
const captured = {};
|
|
80
|
+
order.forEach((key, index) => {
|
|
81
|
+
captured[key] = found[index + 1];
|
|
82
|
+
});
|
|
83
|
+
const platform = captured.platform || entry.platform;
|
|
84
|
+
const name = entry.name === NAME_WILDCARD ? captured.name : entry.name;
|
|
85
|
+
if (!name) {
|
|
86
|
+
throw new Error(`Platform "${entry.resource}" uses name: * but the pattern captures no name segment`);
|
|
87
|
+
}
|
|
88
|
+
return { entry, platform, name };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Matches a resource group against the manifest platforms.
|
|
93
|
+
* @param {Array} platforms - Manifest `platforms`
|
|
94
|
+
* @param {string} resourceGroup - Target Azure resource group
|
|
95
|
+
* @returns {{ entry: Object, platform: string, name: string }} The single match
|
|
96
|
+
* @throws {Error} On no match or more than one match
|
|
97
|
+
*/
|
|
98
|
+
function matchPlatform(platforms, resourceGroup) {
|
|
99
|
+
const entries = Array.isArray(platforms) ? platforms : [];
|
|
100
|
+
const group = String(resourceGroup || '').trim();
|
|
101
|
+
if (!group) {
|
|
102
|
+
throw new Error('A resource group is required to resolve a platform');
|
|
103
|
+
}
|
|
104
|
+
const matches = entries.map(entry => matchEntry(entry, group)).filter(Boolean);
|
|
105
|
+
if (matches.length === 0) {
|
|
106
|
+
const tried = entries.map(entry => `"${entry.resource}"`).join(', ') || '(none declared)';
|
|
107
|
+
throw new Error(`Resource group "${group}" matches no platform. Patterns tried: ${tried}`);
|
|
108
|
+
}
|
|
109
|
+
if (matches.length > 1) {
|
|
110
|
+
const ambiguous = matches.map(match => `"${match.entry.resource}"`).join(', ');
|
|
111
|
+
throw new Error(`Resource group "${group}" matches more than one platform: ${ambiguous}. Make the patterns exclusive.`);
|
|
112
|
+
}
|
|
113
|
+
return matches[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Expands `{platform}`, `{platform.name}` and `{resourceGroup}` in a value.
|
|
118
|
+
* @param {string} value - Raw value
|
|
119
|
+
* @param {Object} context - `platform`, `name`, `resourceGroup`
|
|
120
|
+
* @returns {string}
|
|
121
|
+
* @throws {Error} On an unknown token
|
|
122
|
+
*/
|
|
123
|
+
function expandPlatformTokens(value, context) {
|
|
124
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
125
|
+
return text.replace(/\{([a-zA-Z][a-zA-Z.]*)\}/g, (match, token) => {
|
|
126
|
+
if (token === 'platform') return context.platform;
|
|
127
|
+
if (token === 'platform.name') return context.name;
|
|
128
|
+
if (token === 'resourceGroup') return context.resourceGroup;
|
|
129
|
+
throw new Error(`Unsupported token ${match}; use {platform}, {platform.name} or {resourceGroup}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyItems(target, items, context) {
|
|
134
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
135
|
+
target.set(item.name, expandPlatformTokens(item.value, context));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Local runs have no resource group, but still need platform values. The first entry that can
|
|
141
|
+
* produce the local platform supplies them; an `environments` entry still overrides by name.
|
|
142
|
+
* A `*` name cannot be captured without a group, so it resolves to ''.
|
|
143
|
+
* @param {Array} platforms - Manifest platforms
|
|
144
|
+
* @returns {{ entry: Object, platform: string, name: string }|null}
|
|
145
|
+
*/
|
|
146
|
+
function matchLocalPlatform(platforms) {
|
|
147
|
+
const entry = platforms.find(item =>
|
|
148
|
+
item.platform === LOCAL_PLATFORM ||
|
|
149
|
+
(Array.isArray(item.options) && item.options.includes(LOCAL_PLATFORM)));
|
|
150
|
+
if (!entry) return null;
|
|
151
|
+
return { entry, platform: LOCAL_PLATFORM, name: entry.name === NAME_WILDCARD ? '' : entry.name };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {Object} manifest - Application manifest
|
|
156
|
+
* @param {string} resourceGroup - Target resource group ('' for local Docker)
|
|
157
|
+
* @returns {{ entry: Object, platform: string, name: string }|null}
|
|
158
|
+
*/
|
|
159
|
+
function matchIfDeclared(manifest, resourceGroup, allowLocalPlatform) {
|
|
160
|
+
const platforms = manifest && Array.isArray(manifest.platforms) ? manifest.platforms : [];
|
|
161
|
+
if (platforms.length === 0) return null;
|
|
162
|
+
if (!resourceGroup) return allowLocalPlatform ? matchLocalPlatform(platforms) : null;
|
|
163
|
+
return matchPlatform(platforms, resourceGroup);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function environmentEntry(manifest, environment) {
|
|
167
|
+
const entries = manifest && Array.isArray(manifest.environments) ? manifest.environments : [];
|
|
168
|
+
return entries.find(item => item.environment === environment) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyBuiltIns(values, context, environment) {
|
|
172
|
+
const builtIns = [
|
|
173
|
+
// PLATFORM is omitted when nothing determined it, so a consumer fails closed on {PLATFORM}
|
|
174
|
+
// rather than receiving a guess.
|
|
175
|
+
...(context.platform ? [['PLATFORM', context.platform]] : []),
|
|
176
|
+
['PLATFORM_NAME', context.name],
|
|
177
|
+
['RESOURCE_GROUP', context.resourceGroup],
|
|
178
|
+
['ENVIRONMENT', environment]
|
|
179
|
+
];
|
|
180
|
+
// Built-ins never overwrite an authored name of the same spelling.
|
|
181
|
+
for (const [key, value] of builtIns) {
|
|
182
|
+
if (!values.has(key)) values.set(key, value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolves the configuration set for one deployment.
|
|
188
|
+
* @param {Object} manifest - Application manifest with optional `platforms` / `environments`
|
|
189
|
+
* @param {Object} target - `resourceGroup` (omitted for local Docker), `environment`, and
|
|
190
|
+
* `allowLocalPlatform` (default true). A caller that is not a local run — the Controller, which
|
|
191
|
+
* knows the environment but may not know the group — passes false so a missing group resolves no
|
|
192
|
+
* platform at all instead of silently claiming `dev`.
|
|
193
|
+
* @returns {{ platform: string|null, name: string|null, resourceGroup: string|null, values: Object }}
|
|
194
|
+
*/
|
|
195
|
+
function resolveConfiguration(manifest, target = {}) {
|
|
196
|
+
const resourceGroup = target.resourceGroup ? String(target.resourceGroup).trim() : '';
|
|
197
|
+
const environment = target.environment ? String(target.environment).trim() : '';
|
|
198
|
+
const allowLocalPlatform = target.allowLocalPlatform !== false;
|
|
199
|
+
const matched = matchIfDeclared(manifest, resourceGroup, allowLocalPlatform);
|
|
200
|
+
const localPlatform = allowLocalPlatform ? LOCAL_PLATFORM : '';
|
|
201
|
+
const context = {
|
|
202
|
+
platform: matched ? matched.platform : localPlatform,
|
|
203
|
+
name: matched ? matched.name : '',
|
|
204
|
+
resourceGroup
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const values = new Map();
|
|
208
|
+
if (matched) applyItems(values, matched.entry.configuration, context);
|
|
209
|
+
const envEntry = environmentEntry(manifest, environment);
|
|
210
|
+
if (envEntry) applyItems(values, envEntry.configuration, context);
|
|
211
|
+
applyBuiltIns(values, context, environment);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
platform: context.platform || null,
|
|
215
|
+
name: matched && matched.name ? matched.name : null,
|
|
216
|
+
resourceGroup: resourceGroup || null,
|
|
217
|
+
values: Object.fromEntries(values)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
LOCAL_PLATFORM,
|
|
223
|
+
matchPlatform,
|
|
224
|
+
expandPlatformTokens,
|
|
225
|
+
resolveConfiguration
|
|
226
|
+
};
|
|
@@ -148,16 +148,21 @@ function qualifyRepository(registry, repository) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* Deployment/registration image reference (`registry/name:tag`)
|
|
151
|
+
* Deployment/registration image reference (`registry/name:tag`).
|
|
152
|
+
*
|
|
153
|
+
* Deliberately ignores the installation registry (`dev set-registry`): a generated manifest is a
|
|
154
|
+
* committed artifact shared by the team, so it must carry the application's own `image.registry`,
|
|
155
|
+
* not one developer's local default. `push` applies the installation registry when it publishes,
|
|
156
|
+
* and an explicit `options.registry` still wins here for CI that targets another registry.
|
|
152
157
|
* @param {Object} variables - Application manifest
|
|
153
158
|
* @param {string} fallbackName - Name when the manifest has no image name or app key
|
|
154
|
-
* @param {Object} [options] -
|
|
159
|
+
* @param {Object} [options] - `registry` (explicit override only)
|
|
155
160
|
* @returns {string}
|
|
156
161
|
*/
|
|
157
162
|
function buildManifestImageReference(variables, fallbackName, options = {}) {
|
|
158
163
|
const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
|
|
159
164
|
const tag = variables?.image?.tag || 'latest';
|
|
160
|
-
const { registry } = resolveEffectiveRegistry(variables, options);
|
|
165
|
+
const { registry } = resolveEffectiveRegistry(variables, { ...options, installationRegistry: '' });
|
|
161
166
|
return `${qualifyRepository(registry, imageName)}:${tag}`;
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const config = require('../core/config');
|
|
13
13
|
const logger = require('./logger');
|
|
14
|
+
const { maskSensitiveData } = require('./log-redaction');
|
|
14
15
|
const {
|
|
15
16
|
refreshClientToken,
|
|
16
17
|
refreshDeviceToken
|
|
@@ -99,55 +100,47 @@ async function getOrRefreshClientToken(environment, appName, controllerUrl) {
|
|
|
99
100
|
* @returns {Promise<{token: string, controller: string}|null>} Token and controller URL, or null if not available
|
|
100
101
|
*/
|
|
101
102
|
async function getOrRefreshDeviceToken(controllerUrl) {
|
|
102
|
-
// Try to get existing token
|
|
103
103
|
const tokenInfo = await getDeviceToken(controllerUrl);
|
|
104
|
-
|
|
105
|
-
if (!tokenInfo) {
|
|
106
|
-
return
|
|
104
|
+
if (!tokenInfo) return null;
|
|
105
|
+
if (reusableDeviceToken(tokenInfo) && !shouldRefreshToken(tokenInfo.expiresAt)) {
|
|
106
|
+
return reusableDeviceToken(tokenInfo);
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
// Check if token should be refreshed proactively (within 15 minutes of expiry)
|
|
110
|
-
// This ensures we refresh before Keycloak's SSO Session Idle timeout (30 minutes)
|
|
111
|
-
const needsRefresh = shouldRefreshToken(tokenInfo.expiresAt);
|
|
112
|
-
|
|
113
|
-
if (!needsRefresh) {
|
|
114
|
-
// Token is valid and doesn't need refresh yet
|
|
115
|
-
return {
|
|
116
|
-
token: tokenInfo.token,
|
|
117
|
-
controller: tokenInfo.controller
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Token needs refresh (expired or within 15 minutes of expiry)
|
|
122
|
-
// Try to refresh if refresh token exists
|
|
123
108
|
if (!tokenInfo.refreshToken) {
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
'Access token expired and no refresh token available. Please login again using: aifabrix login'
|
|
128
|
-
);
|
|
109
|
+
const usable = reusableDeviceToken(tokenInfo);
|
|
110
|
+
if (!usable) {
|
|
111
|
+
warnRefreshFailureOnce(controllerUrl, 'No usable access token and no refresh token available. Please login again using: aifabrix login');
|
|
129
112
|
}
|
|
130
|
-
return
|
|
113
|
+
return usable;
|
|
131
114
|
}
|
|
132
|
-
|
|
133
115
|
try {
|
|
134
116
|
const refreshed = await refreshDeviceToken(controllerUrl, tokenInfo.refreshToken);
|
|
135
|
-
return {
|
|
136
|
-
token: refreshed.token,
|
|
137
|
-
controller: controllerUrl
|
|
138
|
-
};
|
|
117
|
+
return { token: refreshed.token, controller: controllerUrl };
|
|
139
118
|
} catch (error) {
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
119
|
+
let message = error.message || String(error);
|
|
120
|
+
for (const secret of [tokenInfo.token, tokenInfo.refreshToken]) {
|
|
121
|
+
if (secret) message = message.split(secret).join('***');
|
|
122
|
+
}
|
|
123
|
+
message = maskSensitiveData(message);
|
|
124
|
+
if (message.includes('Refresh token has expired')) {
|
|
125
|
+
warnRefreshTokenExpiredOnce(controllerUrl, message);
|
|
144
126
|
} else {
|
|
145
|
-
warnRefreshFailureOnce(controllerUrl,
|
|
127
|
+
warnRefreshFailureOnce(controllerUrl, message);
|
|
146
128
|
}
|
|
147
|
-
return
|
|
129
|
+
return reusableDeviceToken(tokenInfo);
|
|
148
130
|
}
|
|
149
131
|
}
|
|
150
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Reuse only a token whose actual expiry is known and still in the future.
|
|
135
|
+
* @param {Object} tokenInfo - Stored device token
|
|
136
|
+
* @returns {Object|null} Usable token, without refresh credentials
|
|
137
|
+
*/
|
|
138
|
+
function reusableDeviceToken(tokenInfo) {
|
|
139
|
+
const expiry = typeof tokenInfo.expiresAt === 'string' ? Date.parse(tokenInfo.expiresAt) : NaN;
|
|
140
|
+
return tokenInfo.token && Number.isFinite(expiry) && expiry > Date.now()
|
|
141
|
+
? { token: tokenInfo.token, controller: tokenInfo.controller } : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
151
144
|
/**
|
|
152
145
|
* Validates deployment auth parameters
|
|
153
146
|
* @function validateDeploymentAuthParams
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aifabrix/builder",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.60.0",
|
|
4
4
|
"description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -131,8 +131,9 @@
|
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"dependencies": {
|
|
134
|
-
"@aifabrix/miso-client": "4.
|
|
135
|
-
"
|
|
134
|
+
"@aifabrix/miso-client": "4.23.1",
|
|
135
|
+
"@azure/identity": "4.11.1",
|
|
136
|
+
"adm-zip": "^0.6.1",
|
|
136
137
|
"ajv": "^8.20.0",
|
|
137
138
|
"ajv-formats": "^3.0.1",
|
|
138
139
|
"axios": "^1.18.1",
|
package/templates/README.md
CHANGED
|
@@ -23,9 +23,10 @@ Application templates are folder-based and located under `templates/applications
|
|
|
23
23
|
| `aifabrix-miso/builder/miso-controller/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/miso-controller/` |
|
|
24
24
|
| `aifabrix-miso/builder/keycloak/` (`env.template`, `application.yaml`, `Dockerfile`, `cimd-lenient-parse/`) | `templates/applications/keycloak/` |
|
|
25
25
|
| `aifabrix-miso/infrastructure/marketplace/` (`main.json`, `createUiDefinition.json`) | `templates/marketplace/` |
|
|
26
|
+
| `aifabrix-miso/packages/miso-controller/configs/infra.parameter.yaml` | `lib/schema/infra.parameter.yaml` |
|
|
26
27
|
| `aifabrix-dataplane/builder/dataplane/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/dataplane/` |
|
|
27
28
|
|
|
28
|
-
**Schema master (dataplane):** Also copies dataplane `app/schemas/json/` core integration schemas + `type/*` and `configs/deployment-rules.yaml` into `lib/schema/` (same contract as dataplane `make sync-json`). Do not hand-edit those mirrors — edit dataplane SSOT, then sync. Builder-owned schemas in `lib/schema/` (wizard
|
|
29
|
+
**Schema master (dataplane):** Also copies dataplane `app/schemas/json/` core integration schemas + `type/*` and `configs/deployment-rules.yaml` into `lib/schema/` (same contract as dataplane `make sync-json`). Do not hand-edit those mirrors — edit dataplane SSOT, then sync. `lib/schema/infra.parameter.yaml` is the Miso catalog mirror above; other Builder-owned schemas in `lib/schema/` (wizard and the rest) are left alone.
|
|
29
30
|
|
|
30
31
|
Overrides: `AIFABRIX_MISO_SRC`, `AIFABRIX_DATAPLANE_SRC` / `AIFABRIX_DATAPLANE_ROOT` (repo roots). Install with `aifabrix up-platform` / `up-builder-api` — do not hand-edit the template copies as primary.
|
|
31
32
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
schemaVersion: "1"
|
|
2
|
-
kitVersion: "1.
|
|
2
|
+
kitVersion: "1.2.0"
|
|
3
3
|
minimumBuilderVersion: "2.54.2"
|
|
4
4
|
skills:
|
|
5
5
|
- aifabrix-plan
|
|
@@ -26,6 +26,9 @@ managed:
|
|
|
26
26
|
- source: workspace/README.md
|
|
27
27
|
target: README.md
|
|
28
28
|
mode: if-missing
|
|
29
|
+
- source: workspace/BUILDER_IMPROVEMENT_FINDINGS.md
|
|
30
|
+
target: BUILDER_IMPROVEMENT_FINDINGS.md
|
|
31
|
+
mode: if-missing
|
|
29
32
|
optional:
|
|
30
33
|
- source: adapters/cursor/commands
|
|
31
34
|
target: .cursor/commands
|
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
Builder-owned. `aifabrix agent-kit update` refreshes this file. If workspace `README.md` is missing, install/update creates it. An existing README is never overwritten.
|
|
4
4
|
|
|
5
|
+
## Onboard
|
|
6
|
+
|
|
7
|
+
On an organisation-managed SSH workspace with a provisioned `config.yaml`:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
af onboarding
|
|
11
|
+
af onboarding --coding claude # or codex or cursor for this run
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For Claude, onboarding starts or reuses a detached tmux session and prints
|
|
15
|
+
its attach command. For Codex or Cursor, open the printed workspace in the
|
|
16
|
+
selected coding client over SSH, then say “Hi”. `--coding` selects a platform
|
|
17
|
+
for one run without changing the provisioned default.
|
|
18
|
+
The agent reads this workspace's `AGENTS.md` and `README.md`, checks which
|
|
19
|
+
packages already exist, and guides you to a first delivery task.
|
|
20
|
+
|
|
21
|
+
On a Linux or macOS terminal with Builder, Git, and tmux:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
aifabrix agent-kit setup https://controller.example.com
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This prepares a local workspace under the configured AI Fabrix work directory and starts
|
|
28
|
+
Codex. Select Claude with `--code claude`. GitHub is optional: add `--repo owner/name`
|
|
29
|
+
to reuse a matching local checkout, clone a remote, or create a private repository.
|
|
30
|
+
The repository name becomes the folder name; unresolved access stops safely.
|
|
31
|
+
Setup preserves existing agent permissions. Change environment afterward with
|
|
32
|
+
`aifabrix auth --set-environment <environment>`.
|
|
33
|
+
|
|
5
34
|
## Install
|
|
6
35
|
|
|
7
36
|
From this git repository (Builder CLI on PATH):
|
|
@@ -57,13 +86,18 @@ curl -fsSL https://claude.ai/install.sh | bash
|
|
|
57
86
|
claude auth login
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
**
|
|
89
|
+
**Optional terminal session in tmux:** run from this workspace so closing the
|
|
90
|
+
terminal does not kill Claude. The command enables Remote Control and this
|
|
91
|
+
workspace's Claude auto-approval by default. Remote Control is the **same**
|
|
92
|
+
session on this PC, [claude.ai/code](https://claude.ai/code), and the Claude
|
|
93
|
+
mobile app (Code). That is not `claude --cloud` (a separate cloud VM). Claude
|
|
94
|
+
`--tmux` is for worktrees only.
|
|
61
95
|
|
|
62
96
|
```bash
|
|
63
97
|
aifabrix agent-kit start claude
|
|
64
98
|
```
|
|
65
99
|
|
|
66
|
-
tmux session name defaults to this folder. Remote Control name is `hostname-user-folder` (for example `
|
|
100
|
+
tmux session name defaults to this folder. Remote Control name is `hostname-user-folder` (for example `devhost-alice-acme-workspace`). Override with `--name`; opt out with `--no-remote-control` and/or `--skip-approve-all`. Detach: `Ctrl-b` then `d`. Reattach: `tmux attach -t <folder>`. Use `aifabrix agent-kit list` and `agent-kit kill <folder>` to manage sessions. Same account on phone and PC (`claude auth login`, Pro/Max/Team — not an API key). On the phone: Claude app → **Code**. Skills: `/aifabrix-plan` (and the other `/aifabrix-*` names). First `claude` must run inside this workspace, not your home directory. Full notes: `.agents/skills/shared/hosts.md`.
|
|
67
101
|
{{/if}}
|
|
68
102
|
{{#if codex}}
|
|
69
103
|
|
|
@@ -76,7 +110,10 @@ npm install -g @openai/codex
|
|
|
76
110
|
codex login
|
|
77
111
|
```
|
|
78
112
|
|
|
79
|
-
**
|
|
113
|
+
**Optional terminal session in tmux:** run from this workspace so a terminal
|
|
114
|
+
Codex session stays after SSH disconnects. This session is separate from a
|
|
115
|
+
Codex app chat opened over SSH. The command requests native Codex Remote
|
|
116
|
+
Control and writes workspace auto-approval by default:
|
|
80
117
|
|
|
81
118
|
```bash
|
|
82
119
|
aifabrix agent-kit start codex
|
|
@@ -14,6 +14,46 @@ Do not edit platform product source (`aifabrix-dataplane`, `aifabrix-miso`, `aif
|
|
|
14
14
|
|
|
15
15
|
Vendor limits, tenant ids, credentials, PII stance, partitions, and the designated mutation subject live only in `integration/<systemKey>/README.md`.
|
|
16
16
|
|
|
17
|
+
`agent-kit update` rewrites `.agents/skills/**`, `AGENTKIT.md`, and managed sections; `--force` discards local edits. Do not edit them: file gaps in `BUILDER_IMPROVEMENT_FINDINGS.md`, sanitized. Never weaken a gate to clear one. Routing: `.agents/skills/shared/feedback.md`.
|
|
18
|
+
|
|
19
|
+
## First greeting
|
|
20
|
+
|
|
21
|
+
On a first “Hi”, start with the business problem in plain language. Explain
|
|
22
|
+
that their process, people, outcome, systems, and a real example become a plan,
|
|
23
|
+
Connected Systems, a Role Assistant, and proof. Show these skills in a table:
|
|
24
|
+
|
|
25
|
+
| Skill | How it helps |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| aifabrix-plan | Turn the business context or spec into a plan; ask about gaps |
|
|
28
|
+
| aifabrix-connected-system | Validate, build, test, and publish needed systems |
|
|
29
|
+
| aifabrix-role-assistant | Create or update a catalog role using published systems |
|
|
30
|
+
| aifabrix-prove | Test a real business example and record the result |
|
|
31
|
+
|
|
32
|
+
Render skill names with `$` in Codex and `/` in Claude Code or Cursor. Do not
|
|
33
|
+
run CLI commands or lead with inventory, sign-in, or gates on this first reply.
|
|
34
|
+
|
|
35
|
+
Ask one opening question: what business process or role should we help, and
|
|
36
|
+
what should improve? Invite a spec, API document, sample payload, or example
|
|
37
|
+
workflow if they have one. Say a plain-language description is enough to start;
|
|
38
|
+
missing details will become explicit questions. Keep the greeting brief.
|
|
39
|
+
|
|
40
|
+
## After the business context
|
|
41
|
+
|
|
42
|
+
Before planning or using online Builder commands, check the workspace and
|
|
43
|
+
sign-in. Run `aifabrix auth status`; if signed out, follow
|
|
44
|
+
`.agents/skills/shared/login.md` and ask for the Controller URL only if needed.
|
|
45
|
+
Read `README.md` and list package folder names under `integration/` and
|
|
46
|
+
`role-assistant/`; do not open package data or secrets for discovery. If signed
|
|
47
|
+
in, run `aifabrix list` to find remote systems. If listing fails, report the
|
|
48
|
+
failure; do not infer that there are no systems. Distinguish local packages
|
|
49
|
+
from remote entries, and published Connected Systems from drafts, Role
|
|
50
|
+
Assistants, and Enterprise Knowledge. Never print tokens.
|
|
51
|
+
|
|
52
|
+
Then suggest the relevant skill from the first table and explain its gate.
|
|
53
|
+
Summarize sign-in and discovery only as needed for the chosen work; give at
|
|
54
|
+
most two relevant remote examples unless asked for the
|
|
55
|
+
full list. Do not print email, IDs, URLs, or versions in a routine status.
|
|
56
|
+
|
|
17
57
|
## Secrets
|
|
18
58
|
|
|
19
59
|
Use `kv://` and `aifabrix secret`. Never store, print, ask for, or commit secret values.
|
|
@@ -27,7 +67,8 @@ Do not collapse these into a generic success boolean. Do not start the next life
|
|
|
27
67
|
|
|
28
68
|
## Coding hosts
|
|
29
69
|
|
|
30
|
-
Cursor, Claude Code, and Codex — local
|
|
70
|
+
Cursor, Claude Code, and Codex — local or remote SSH projects, with optional
|
|
71
|
+
tmux terminal sessions: `.agents/skills/shared/hosts.md`.
|
|
31
72
|
Install and sign-in: `AGENTKIT.md`. Builder CLI login from chat: `.agents/skills/shared/login.md`.
|
|
32
73
|
|
|
33
74
|
## Docs
|