@aifabrix/builder 2.56.0 → 2.58.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.
Files changed (74) hide show
  1. package/lib/agent-kit/session-report.js +76 -0
  2. package/lib/agent-kit/start.js +124 -57
  3. package/lib/agent-kit/tmux-sessions.js +163 -0
  4. package/lib/api/environments.api.js +38 -0
  5. package/lib/api/marketplace-bootstrap.api.js +31 -0
  6. package/lib/app/push.js +98 -41
  7. package/lib/build/standard-docker-build.js +13 -11
  8. package/lib/cli/setup-app.js +16 -5
  9. package/lib/cli/setup-dev-path-commands.js +49 -2
  10. package/lib/cli/setup-environment.js +89 -19
  11. package/lib/commands/agent-kit-help.js +32 -15
  12. package/lib/commands/agent-kit.js +90 -8
  13. package/lib/commands/dev-registry.js +155 -0
  14. package/lib/commands/dev-show-display.js +10 -2
  15. package/lib/commands/setup-image-refresh.js +26 -0
  16. package/lib/core/config-attach-extensions.js +4 -1
  17. package/lib/deployment/installation/azure-infra-stage.js +191 -0
  18. package/lib/deployment/installation/azure-preflight.js +147 -0
  19. package/lib/deployment/installation/azure-readiness.js +101 -0
  20. package/lib/deployment/installation/docker-register.js +112 -0
  21. package/lib/deployment/installation/environment-auth.js +76 -0
  22. package/lib/deployment/installation/environment-manifest.js +165 -0
  23. package/lib/deployment/installation/environment-stage.js +191 -0
  24. package/lib/deployment/installation/index.js +39 -0
  25. package/lib/deployment/installation/infra-catalog.js +211 -0
  26. package/lib/deployment/installation/infra-manifest.js +176 -0
  27. package/lib/deployment/installation/infra-projection.js +97 -0
  28. package/lib/deployment/installation/public-ip.js +65 -0
  29. package/lib/deployment/installation/registry-auth-mode.js +98 -0
  30. package/lib/deployment/installation/stage-input.js +184 -0
  31. package/lib/deployment/installation/stage-output.js +53 -0
  32. package/lib/deployment/installation/workspace-env.js +76 -0
  33. package/lib/deployment/installation/workspace-files.js +178 -0
  34. package/lib/deployment/installation/workspace-schema.js +40 -0
  35. package/lib/deployment/installation/workspace-secrets.js +66 -0
  36. package/lib/deployment/installation/workspace.js +128 -0
  37. package/lib/deployment/marketplace-environment.js +305 -0
  38. package/lib/generator/builders.js +12 -9
  39. package/lib/generator/helpers.js +55 -56
  40. package/lib/generator/split-variables.js +11 -5
  41. package/lib/internal/fs-real-sync.js +29 -0
  42. package/lib/role-assistant/package-validate.js +4 -0
  43. package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
  44. package/lib/role-assistant/test-runner.js +14 -17
  45. package/lib/schema/application-schema.json +20 -2
  46. package/lib/schema/infra-parameter.schema.json +7 -0
  47. package/lib/schema/infra.parameter.yaml +26 -36
  48. package/lib/schema/infrastructure-schema.json +1531 -397
  49. package/lib/schema/installation-environment.schema.json +601 -0
  50. package/lib/utils/app-register-config.js +2 -4
  51. package/lib/utils/build-resolve-image.js +2 -21
  52. package/lib/utils/cli-utils.js +8 -1
  53. package/lib/utils/config-registry-preference.js +106 -0
  54. package/lib/utils/docker-build.js +53 -19
  55. package/lib/utils/env-map.js +23 -6
  56. package/lib/utils/image-tags.js +88 -0
  57. package/lib/utils/promote-dev-docker-image.js +6 -0
  58. package/lib/utils/registry-credentials.js +236 -0
  59. package/lib/utils/resolve-docker-image-ref.js +78 -9
  60. package/lib/utils/secrets-helpers.js +9 -1
  61. package/lib/utils/variable-transformer.js +5 -4
  62. package/lib/validation/required-variables.js +41 -0
  63. package/lib/validation/validator.js +8 -5
  64. package/package.json +6 -1
  65. package/templates/README.md +4 -0
  66. package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
  67. package/templates/agent-kit/skills/shared/hosts.md +10 -10
  68. package/templates/applications/dataplane/application.yaml +1 -1
  69. package/templates/applications/dataplane/env.template +1 -1
  70. package/templates/applications/miso-controller/application.yaml +3 -2
  71. package/templates/applications/miso-controller/env.template +15 -6
  72. package/templates/marketplace/createUiDefinition.json +77 -0
  73. package/templates/marketplace/install.parameters.example.json +9 -0
  74. package/templates/marketplace/main.json +4087 -0
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Installation workspace filesystem helpers: `infrastructure/<resource-group>/`
5
+ * resolution, deterministic atomic JSON writes, hash-guarded generated files and ignore checks.
6
+ */
7
+
8
+ // Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
9
+ const fs = require('../../internal/fs-real-sync');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+ const { spawnSync } = require('child_process');
13
+
14
+ const WORKSPACE_PARENT = 'infrastructure';
15
+ const FILES = Object.freeze({
16
+ template: 'azure-main.json',
17
+ infra: 'environment-infra.json',
18
+ envTemplate: 'env.template',
19
+ env: '.env'
20
+ });
21
+
22
+ /**
23
+ * @param {string} envKey - dev, tst or pro
24
+ * @returns {string} Environment file name
25
+ */
26
+ function environmentFileName(envKey) {
27
+ return `environment-${envKey}.json`;
28
+ }
29
+
30
+ function sha256(content) {
31
+ return crypto.createHash('sha256').update(content).digest('hex');
32
+ }
33
+
34
+ function runGit(args, cwd) {
35
+ return spawnSync('git', args, { cwd, encoding: 'utf8' });
36
+ }
37
+
38
+ /**
39
+ * Builder workspace root: the Git top-level of the current directory, else the current directory.
40
+ * @param {string} [cwd] - Starting directory
41
+ * @returns {string} Absolute root path
42
+ */
43
+ function resolveWorkspaceRoot(cwd = process.cwd()) {
44
+ const result = runGit(['rev-parse', '--show-toplevel'], cwd);
45
+ if (!result.error && result.status === 0 && result.stdout.trim()) {
46
+ return path.resolve(result.stdout.trim());
47
+ }
48
+ return path.resolve(cwd);
49
+ }
50
+
51
+ /**
52
+ * Resolves `infrastructure/<resource-group>/` and proves it stays inside the workspace root.
53
+ * @param {string} root - Workspace root
54
+ * @param {string} resourceGroup - Validated resource-group key
55
+ * @returns {string} Absolute workspace directory
56
+ */
57
+ function resolveWorkspaceDir(root, resourceGroup) {
58
+ const parent = path.join(root, WORKSPACE_PARENT);
59
+ const dir = path.join(parent, resourceGroup);
60
+ const relative = path.relative(parent, dir);
61
+ if (relative !== resourceGroup || relative.startsWith('..') || path.isAbsolute(relative)) {
62
+ throw new Error('Resolved installation workspace escapes the infrastructure directory');
63
+ }
64
+ for (const candidate of [parent, dir]) {
65
+ if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
66
+ throw new Error(`Installation workspace path must not be a symbolic link: ${candidate}`);
67
+ }
68
+ }
69
+ return dir;
70
+ }
71
+
72
+ /**
73
+ * Deterministic JSON text (two-space indentation, trailing newline).
74
+ * @param {Object} document - JSON document
75
+ * @returns {string}
76
+ */
77
+ function toJsonText(document) {
78
+ return `${JSON.stringify(document, null, 2)}\n`;
79
+ }
80
+
81
+ /**
82
+ * @param {string} file - JSON file path
83
+ * @returns {Object|null} Parsed document or null when absent
84
+ */
85
+ function readJsonIfExists(file) {
86
+ if (!fs.existsSync(file)) return null;
87
+ try {
88
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
89
+ } catch (_) {
90
+ throw new Error(`Workspace file is not valid JSON: ${file}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Writes content atomically through an owner-only temporary file in the same directory.
96
+ * The temporary file is removed on every failure path.
97
+ * @param {string} file - Target path
98
+ * @param {string} content - Content
99
+ * @param {number} mode - Final file mode
100
+ */
101
+ function writeFileAtomic(file, content, mode) {
102
+ const dir = path.dirname(file);
103
+ fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
104
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
105
+ try {
106
+ fs.writeFileSync(tmp, content, { mode: 0o600, flag: 'wx' });
107
+ fs.chmodSync(tmp, mode);
108
+ fs.renameSync(tmp, file);
109
+ } finally {
110
+ if (fs.existsSync(tmp)) fs.rmSync(tmp, { force: true });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} file - Target path
116
+ * @param {Object} document - JSON document
117
+ */
118
+ function writeJsonAtomic(file, document) {
119
+ writeFileAtomic(file, toJsonText(document), 0o644);
120
+ }
121
+
122
+ /**
123
+ * Writes a committed generated file only when it is absent, unchanged since generation, or
124
+ * already identical. Returns the new SHA-256 hash.
125
+ * @param {string} file - Target path
126
+ * @param {string} content - Generated content
127
+ * @param {string|undefined} recordedHash - Hash recorded at the previous generation
128
+ * @returns {string} Hash of the written content
129
+ * @throws {Error} When a user-modified generated file would be replaced
130
+ */
131
+ function writeGeneratedFile(file, content, recordedHash) {
132
+ const nextHash = sha256(content);
133
+ if (fs.existsSync(file)) {
134
+ const currentHash = sha256(fs.readFileSync(file));
135
+ if (currentHash === nextHash) return nextHash;
136
+ if (currentHash !== recordedHash) {
137
+ throw new Error(
138
+ `Generated workspace file ${path.basename(file)} was modified after generation. ` +
139
+ `Review it with "git diff -- ${file}", restore it (git checkout -- <file>) or delete it, then rerun.`
140
+ );
141
+ }
142
+ }
143
+ writeFileAtomic(file, content, 0o644);
144
+ return nextHash;
145
+ }
146
+
147
+ /**
148
+ * Refuses to write a secret file that Git could track.
149
+ * @param {string} file - Secret file path
150
+ * @param {string} root - Workspace root
151
+ * @throws {Error} When the file is inside a Git work tree and not ignored, or already tracked
152
+ */
153
+ function assertIgnoredSecretFile(file, root) {
154
+ const inside = runGit(['rev-parse', '--is-inside-work-tree'], root);
155
+ if (inside.error || inside.status !== 0) return;
156
+ const relative = path.relative(root, file);
157
+ if (runGit(['ls-files', '--error-unmatch', '--', relative], root).status === 0) {
158
+ throw new Error(`${relative} is tracked by Git; remove it from the index (git rm --cached) before continuing`);
159
+ }
160
+ if (runGit(['check-ignore', '-q', '--', relative], root).status !== 0) {
161
+ throw new Error(`${relative} is not ignored by Git; add ".env" to .gitignore before Builder writes secrets there`);
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ FILES,
167
+ WORKSPACE_PARENT,
168
+ environmentFileName,
169
+ sha256,
170
+ resolveWorkspaceRoot,
171
+ resolveWorkspaceDir,
172
+ toJsonText,
173
+ readJsonIfExists,
174
+ writeFileAtomic,
175
+ writeJsonAtomic,
176
+ writeGeneratedFile,
177
+ assertIgnoredSecretFile
178
+ };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Validates installation workspace manifests against their Builder-owned schemas
5
+ * before they are written, so unknown or secret-bearing shapes never reach version control.
6
+ */
7
+
8
+ const Ajv = require('ajv');
9
+ const addFormats = require('ajv-formats');
10
+ const { formatValidationErrors } = require('../../utils/error-formatter');
11
+
12
+ let validators = null;
13
+
14
+ function getValidators() {
15
+ if (!validators) {
16
+ const ajv = new Ajv({ allErrors: true, strict: false });
17
+ addFormats(ajv);
18
+ validators = {
19
+ infra: ajv.compile(require('../../schema/infrastructure-schema.json')),
20
+ environment: ajv.compile(require('../../schema/installation-environment.schema.json'))
21
+ };
22
+ }
23
+ return validators;
24
+ }
25
+
26
+ /**
27
+ * @param {'infra'|'environment'} kind - Manifest kind
28
+ * @param {Object} document - Manifest document
29
+ * @param {string} fileName - File name for messages
30
+ * @throws {Error} When the document does not match its schema
31
+ */
32
+ function assertWorkspaceDocument(kind, document, fileName) {
33
+ const validate = getValidators()[kind];
34
+ if (!validate(document)) {
35
+ const messages = formatValidationErrors(validate.errors);
36
+ throw new Error(`${fileName} does not match its schema:\n - ${messages.join('\n - ')}`);
37
+ }
38
+ }
39
+
40
+ module.exports = { assertWorkspaceDocument };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Resolves the workspace `env.template` contract through the configured Builder
5
+ * secret system and materializes values into the owner-only workspace `.env`.
6
+ * Automated writes target only the primary user secrets file, never the shared store.
7
+ */
8
+
9
+ const { SECRET_BINDINGS, ENV_TEMPLATE_KEYS } = require('./infra-catalog');
10
+ const { mergeWorkspaceEnv } = require('./workspace-env');
11
+
12
+ function secretSetHint(binding) {
13
+ return `aifabrix secret set ${binding.kv} <value>`;
14
+ }
15
+
16
+ function defaultSecretsApi() {
17
+ const { loadSecrets } = require('../../core/secrets');
18
+ const { ensureSecretsForKeys } = require('../../core/secrets-ensure');
19
+ return { loadSecrets, ensureSecretsForKeys };
20
+ }
21
+
22
+ function asValue(raw) {
23
+ if (raw === undefined || raw === null) return '';
24
+ return String(raw);
25
+ }
26
+
27
+ /**
28
+ * Resolves and materializes the vendor's secret contract into `.env`.
29
+ * @param {Object} params - Inputs
30
+ * @param {string} params.vendor - AZURE or DOCKER
31
+ * @param {string} params.envPath - Workspace .env path
32
+ * @param {string} params.root - Workspace root
33
+ * @param {string[]} [params.names] - Subset of contract names to resolve (defaults to vendor contract)
34
+ * @param {Object} [params.secretsApi] - Injected secret-system functions (tests)
35
+ * @returns {Promise<Object.<string,string>>} Resolved values (never logged)
36
+ * @throws {Error} When a required secret is missing; message names only variable and kv key
37
+ */
38
+ async function materializeWorkspaceSecrets({ vendor, envPath, root, names, secretsApi }) {
39
+ const api = secretsApi || defaultSecretsApi();
40
+ const selected = names || ENV_TEMPLATE_KEYS[vendor];
41
+ const generated = selected.filter(name => SECRET_BINDINGS[name].generate).map(name => SECRET_BINDINGS[name].kv);
42
+ if (generated.length > 0) {
43
+ await api.ensureSecretsForKeys(generated);
44
+ }
45
+ const secrets = (await api.loadSecrets()) || {};
46
+ const resolved = {};
47
+ const missing = [];
48
+ for (const name of selected) {
49
+ const binding = SECRET_BINDINGS[name];
50
+ const value = asValue(secrets[binding.kv]);
51
+ if (value.startsWith('kv://')) {
52
+ missing.push(`${name} (kv://${binding.kv}): unresolved reference; run ${secretSetHint(binding)}`);
53
+ } else if (value) {
54
+ resolved[name] = value;
55
+ } else if (binding.required) {
56
+ missing.push(`${name} (kv://${binding.kv}): run ${secretSetHint(binding)}`);
57
+ }
58
+ }
59
+ if (missing.length > 0) {
60
+ throw new Error(`Required installation secrets are not set:\n - ${missing.join('\n - ')}`);
61
+ }
62
+ mergeWorkspaceEnv(envPath, resolved, { root });
63
+ return resolved;
64
+ }
65
+
66
+ module.exports = { materializeWorkspaceSecrets };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Loads and generates the `infrastructure/<resource-group>/` installation workspace:
5
+ * vendor template snapshot, `environment-infra.json`, per-environment files and `env.template`,
6
+ * with recorded provenance hashes.
7
+ */
8
+
9
+ // Real-sync adapter: installation workspace state must not disappear under Jest fs mocks.
10
+ const fs = require('../../internal/fs-real-sync');
11
+ const path = require('path');
12
+ const files = require('./workspace-files');
13
+ const { buildEnvTemplate, assertTemplateCoverage } = require('./infra-catalog');
14
+ const { buildInfraDocument } = require('./infra-manifest');
15
+ const { buildEnvironmentDocument } = require('./environment-manifest');
16
+ const { ENVIRONMENT_STAGES } = require('./stage-input');
17
+ const { assertWorkspaceDocument } = require('./workspace-schema');
18
+
19
+ const SHIPPED_TEMPLATE = path.resolve(__dirname, '../../../templates/marketplace/main.json');
20
+
21
+ function builderVersion() {
22
+ return require('../../../package.json').version;
23
+ }
24
+
25
+ /**
26
+ * Resolves workspace paths and reads the persisted infra manifest (if any).
27
+ * @param {string} resourceGroup - Validated resource-group key
28
+ * @param {Object} [context] - Optional overrides
29
+ * @param {string} [context.root] - Workspace root
30
+ * @returns {{ root: string, dir: string, paths: Object, infra: Object|null }}
31
+ */
32
+ function openWorkspace(resourceGroup, context = {}) {
33
+ const root = context.root || files.resolveWorkspaceRoot();
34
+ const dir = files.resolveWorkspaceDir(root, resourceGroup);
35
+ const paths = {
36
+ template: path.join(dir, files.FILES.template),
37
+ infra: path.join(dir, files.FILES.infra),
38
+ envTemplate: path.join(dir, files.FILES.envTemplate),
39
+ env: path.join(dir, files.FILES.env),
40
+ environment: envKey => path.join(dir, files.environmentFileName(envKey))
41
+ };
42
+ const infra = files.readJsonIfExists(paths.infra);
43
+ if (infra && infra.resourceGroup?.value !== resourceGroup) {
44
+ throw new Error(`Workspace identity mismatch: ${paths.infra} records resource group "${infra.resourceGroup?.value}"`);
45
+ }
46
+ return { root, dir, paths, infra };
47
+ }
48
+
49
+ function loadShippedTemplate(templatePath = SHIPPED_TEMPLATE) {
50
+ const text = fs.readFileSync(templatePath, 'utf8');
51
+ const template = JSON.parse(text);
52
+ assertTemplateCoverage(template);
53
+ return { text, template, hash: files.sha256(text) };
54
+ }
55
+
56
+ function writeGenerated(workspace, doc, name, content) {
57
+ const recorded = doc.generated.files?.[name];
58
+ const hash = files.writeGeneratedFile(path.join(workspace.dir, name), content, recorded);
59
+ if (hash !== recorded) {
60
+ doc.generated.files = { ...(doc.generated.files || {}), [name]: hash };
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function ensureEnvironmentFiles(workspace) {
67
+ for (const envKey of ENVIRONMENT_STAGES) {
68
+ const file = workspace.paths.environment(envKey);
69
+ if (!fs.existsSync(file)) saveEnvironment(workspace, envKey, buildEnvironmentDocument({ envKey, existing: null }));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Creates or refreshes the workspace for a vendor and persists `environment-infra.json`.
75
+ * @param {Object} workspace - Result of openWorkspace
76
+ * @param {{ vendor: string, resourceGroup: string }} input - Validated stage input
77
+ * @param {Object} [context] - Optional overrides
78
+ * @param {string} [context.templatePath] - Vendor template source (tests)
79
+ * @param {Function} [context.now] - Clock
80
+ * @returns {{ doc: Object, template: Object, templateHash: string }}
81
+ */
82
+ function materializeWorkspace(workspace, input, context = {}) {
83
+ const shipped = loadShippedTemplate(context.templatePath);
84
+ const doc = buildInfraDocument({
85
+ vendor: input.vendor, resourceGroup: input.resourceGroup, template: shipped.template, existing: workspace.infra
86
+ });
87
+ let changed = !workspace.infra;
88
+ if (input.vendor === 'AZURE') {
89
+ changed = writeGenerated(workspace, doc, files.FILES.template, shipped.text) || changed;
90
+ ensureEnvironmentFiles(workspace);
91
+ }
92
+ changed = writeGenerated(workspace, doc, files.FILES.envTemplate, buildEnvTemplate(input.vendor)) || changed;
93
+ if (changed) {
94
+ doc.generated = {
95
+ ...doc.generated,
96
+ builderVersion: builderVersion(),
97
+ templateContentVersion: shipped.template.contentVersion || null,
98
+ templateSha256: shipped.hash,
99
+ generatedAt: (context.now ? context.now() : new Date()).toISOString()
100
+ };
101
+ }
102
+ saveInfra(workspace, doc);
103
+ return { doc, template: shipped.template, templateHash: shipped.hash };
104
+ }
105
+
106
+ /**
107
+ * Persists the infra manifest.
108
+ * @param {Object} workspace - Workspace
109
+ * @param {Object} doc - Manifest
110
+ */
111
+ function saveInfra(workspace, doc) {
112
+ assertWorkspaceDocument('infra', doc, files.FILES.infra);
113
+ files.writeJsonAtomic(workspace.paths.infra, doc);
114
+ workspace.infra = doc;
115
+ }
116
+
117
+ /**
118
+ * Persists an environment file after schema validation.
119
+ * @param {Object} workspace - Workspace
120
+ * @param {string} envKey - dev, tst or pro
121
+ * @param {Object} doc - Environment document
122
+ */
123
+ function saveEnvironment(workspace, envKey, doc) {
124
+ assertWorkspaceDocument('environment', doc, files.environmentFileName(envKey));
125
+ files.writeJsonAtomic(workspace.paths.environment(envKey), doc);
126
+ }
127
+
128
+ module.exports = { SHIPPED_TEMPLATE, openWorkspace, materializeWorkspace, saveInfra, saveEnvironment, loadShippedTemplate };