@aifabrix/builder 2.55.2 → 2.57.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 (88) 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/api/pipeline.api.js +32 -3
  7. package/lib/app/deploy.js +6 -5
  8. package/lib/cli/setup-app.js +7 -6
  9. package/lib/cli/setup-environment.js +76 -19
  10. package/lib/cli/setup-external-system-help.js +15 -3
  11. package/lib/cli/setup-external-system.js +21 -3
  12. package/lib/commands/agent-kit-help.js +32 -15
  13. package/lib/commands/agent-kit.js +90 -8
  14. package/lib/commands/repair-datasource-auto-rbac.js +4 -9
  15. package/lib/commands/upload-openapi-sync-followup.js +59 -0
  16. package/lib/commands/upload-qualification-client.js +131 -0
  17. package/lib/commands/upload-qualification-followups.js +104 -0
  18. package/lib/commands/upload-qualification-poll.js +143 -0
  19. package/lib/commands/upload.js +37 -156
  20. package/lib/deployment/deploy-poll-ui.js +14 -10
  21. package/lib/deployment/deployer-status.js +4 -4
  22. package/lib/deployment/installation/azure-infra-stage.js +160 -0
  23. package/lib/deployment/installation/azure-preflight.js +102 -0
  24. package/lib/deployment/installation/azure-readiness.js +71 -0
  25. package/lib/deployment/installation/docker-register.js +112 -0
  26. package/lib/deployment/installation/environment-auth.js +76 -0
  27. package/lib/deployment/installation/environment-manifest.js +165 -0
  28. package/lib/deployment/installation/environment-stage.js +191 -0
  29. package/lib/deployment/installation/index.js +39 -0
  30. package/lib/deployment/installation/infra-catalog.js +165 -0
  31. package/lib/deployment/installation/infra-manifest.js +161 -0
  32. package/lib/deployment/installation/infra-projection.js +86 -0
  33. package/lib/deployment/installation/public-ip.js +65 -0
  34. package/lib/deployment/installation/stage-input.js +169 -0
  35. package/lib/deployment/installation/stage-output.js +53 -0
  36. package/lib/deployment/installation/workspace-env.js +76 -0
  37. package/lib/deployment/installation/workspace-files.js +178 -0
  38. package/lib/deployment/installation/workspace-schema.js +40 -0
  39. package/lib/deployment/installation/workspace-secrets.js +66 -0
  40. package/lib/deployment/installation/workspace.js +128 -0
  41. package/lib/deployment/marketplace-environment.js +305 -0
  42. package/lib/external-system/deploy.js +46 -51
  43. package/lib/generator/builders.js +4 -0
  44. package/lib/generator/helpers.js +55 -99
  45. package/lib/generator/index.js +0 -4
  46. package/lib/generator/split-variables.js +11 -5
  47. package/lib/internal/fs-real-sync.js +29 -0
  48. package/lib/programmatic/builder-api-request.js +8 -4
  49. package/lib/programmatic/capability-registry.js +2 -0
  50. package/lib/programmatic/get-deploy-status.js +45 -0
  51. package/lib/programmatic/get-upload-status.js +45 -0
  52. package/lib/programmatic/http-accepted-response.js +31 -0
  53. package/lib/programmatic/openapi-descriptions.js +14 -4
  54. package/lib/programmatic/request-timeout.js +21 -15
  55. package/lib/programmatic/route-handler-map.js +6 -0
  56. package/lib/programmatic/run-deploy.js +7 -3
  57. package/lib/programmatic/run-upload.js +11 -7
  58. package/lib/role-assistant/package-validate.js +4 -0
  59. package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
  60. package/lib/role-assistant/test-runner.js +14 -17
  61. package/lib/schema/application-schema.json +20 -2
  62. package/lib/schema/infra-parameter.schema.json +7 -0
  63. package/lib/schema/infra.parameter.yaml +32 -43
  64. package/lib/schema/infrastructure-schema.json +1480 -399
  65. package/lib/schema/installation-environment.schema.json +601 -0
  66. package/lib/utils/cli-utils.js +3 -2
  67. package/lib/utils/qualification-poll-ui.js +136 -0
  68. package/lib/utils/variable-transformer.js +3 -0
  69. package/lib/validation/required-variables.js +41 -0
  70. package/lib/validation/validator.js +8 -5
  71. package/package.json +7 -2
  72. package/templates/README.md +4 -0
  73. package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
  74. package/templates/agent-kit/skills/shared/hosts.md +10 -10
  75. package/templates/applications/builder-api/env.template +4 -0
  76. package/templates/applications/dataplane/application.yaml +1 -1
  77. package/templates/applications/dataplane/env.template +1 -1
  78. package/templates/applications/keycloak/Dockerfile +12 -5
  79. package/templates/applications/keycloak/application.yaml +2 -2
  80. package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
  81. package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
  82. package/templates/applications/keycloak/env.template +13 -1
  83. package/templates/applications/miso-controller/application.yaml +3 -2
  84. package/templates/applications/miso-controller/env.template +28 -22
  85. package/templates/marketplace/createUiDefinition.json +77 -0
  86. package/templates/marketplace/install.parameters.example.json +9 -0
  87. package/templates/marketplace/main.json +4087 -0
  88. package/lib/generator/deploy-manifest-azure-kv.js +0 -81
@@ -11,7 +11,24 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { loadConfigFile } = require('../utils/config-format');
14
- const { urlTokenToKeyVaultSecretName } = require('./deploy-manifest-azure-kv');
14
+ const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
15
+
16
+ /**
17
+ * Baseline `required` for a keyvault-located key, from infra.parameter.yaml's
18
+ * `requiredOnSystemOnboarding` (matched by exact key or keyPattern). Returns false when the
19
+ * catalog can't be loaded or has no matching entry, so generation never hard-fails on this.
20
+ * @param {string} vaultKey - Key with any `kv://` prefix already stripped
21
+ * @returns {boolean}
22
+ */
23
+ function baselineRequiredForVaultKey(vaultKey) {
24
+ try {
25
+ const catalog = getInfraParameterCatalog();
26
+ const entry = catalog.findEntryForKey(vaultKey);
27
+ return entry?.requiredOnSystemOnboarding === true;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
15
32
 
16
33
  /**
17
34
  * Loads application config file (application.yaml, application.json, or legacy path) via converter.
@@ -149,34 +166,41 @@ function validatePortalInput(portalInput, variableName) {
149
166
  }
150
167
 
151
168
  /**
152
- * Parses environment variables from env.template and merges portalInput from application config
169
+ * Parses environment variables from env.template and merges explicit application config
153
170
  * @param {string} envTemplate - Content of env.template file
154
171
  * @param {Object|null} [variablesConfig=null] - Optional configuration from application.yaml
155
172
  * @returns {Array<Object>} Configuration array with merged portalInput
156
173
  * @throws {Error} If portalInput structure is invalid
157
174
  */
158
175
  /**
159
- * Creates a map of portal overlay metadata from variables config
160
- * @function createPortalInputMap
176
+ * Creates a map of configuration metadata from application.yaml.
177
+ * `required` is declarative: env names and kv:// values never imply it.
178
+ * @function createApplicationConfigMap
161
179
  * @param {Object|null} variablesConfig - Configuration from application.yaml
162
- * @returns {Map} Map of variable names to { portalInput, category? }
180
+ * @returns {Map} Map of variable names to { portalInput?, category?, required? }
163
181
  */
164
- function createPortalInputMap(variablesConfig) {
165
- const portalInputMap = new Map();
182
+ function createApplicationConfigMap(variablesConfig) {
183
+ const applicationConfigMap = new Map();
166
184
  if (variablesConfig && variablesConfig.configuration && Array.isArray(variablesConfig.configuration)) {
167
185
  for (const configItem of variablesConfig.configuration) {
168
- if (configItem.name && configItem.portalInput) {
169
- // Validate portalInput before adding to map
186
+ if (!configItem.name) continue;
187
+ const overlay = {};
188
+ if (configItem.portalInput) {
170
189
  validatePortalInput(configItem.portalInput, configItem.name);
171
- const overlay = { portalInput: configItem.portalInput };
172
- if (typeof configItem.category === 'string' && configItem.category.trim().length > 0) {
173
- overlay.category = configItem.category.trim();
174
- }
175
- portalInputMap.set(configItem.name, overlay);
190
+ overlay.portalInput = configItem.portalInput;
191
+ }
192
+ if (typeof configItem.category === 'string' && configItem.category.trim().length > 0) {
193
+ overlay.category = configItem.category.trim();
194
+ }
195
+ if (typeof configItem.required === 'boolean') {
196
+ overlay.required = configItem.required;
197
+ }
198
+ if (Object.keys(overlay).length > 0) {
199
+ applicationConfigMap.set(configItem.name, overlay);
176
200
  }
177
201
  }
178
202
  }
179
- return portalInputMap;
203
+ return applicationConfigMap;
180
204
  }
181
205
 
182
206
  /**
@@ -210,68 +234,13 @@ function parseEnvironmentVariableLine(line) {
210
234
  }
211
235
 
212
236
  /**
213
- * Determines location and required status for a variable
237
+ * Determines storage location for a variable.
214
238
  * @function determineVariableLocation
215
239
  * @param {string} value - Variable value
216
- * @param {string} key - Variable key
217
- * @returns {Object} Object with location and required properties
240
+ * @returns {string} Deployment configuration location
218
241
  */
219
- function determineVariableLocation(value, key) {
220
- let location = 'variable';
221
- let required = false;
222
-
223
- if (value.startsWith('kv://')) {
224
- location = 'keyvault';
225
- required = true;
226
- } else if (value.startsWith('url://')) {
227
- location = 'keyvault';
228
- required = true;
229
- }
230
-
231
- // Check if it's a sensitive variable
232
- const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth'];
233
- if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
234
- required = true;
235
- }
236
-
237
- if (location === 'keyvault' && value.startsWith('kv://')) {
238
- try {
239
- const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
240
- const entry = getInfraParameterCatalog().findEntryForKey(value.slice('kv://'.length));
241
- if (entry?.scope === 'infra' && entry.generator?.type === 'emptyAllowed') {
242
- required = false;
243
- }
244
- } catch {
245
- // Keep the conservative required=true default when the catalog is unavailable.
246
- }
247
- }
248
-
249
- return { location, required };
250
- }
251
-
252
- /**
253
- * Maps env.template url:// value(s) to Key Vault secret name(s) for deploy JSON.
254
- * Comma-separated lists are supported (e.g. CORS ALLOWED_ORIGINS): each segment
255
- * starting with url:// is mapped; other segments are left unchanged (e.g. http://localhost:*).
256
- *
257
- * @param {string} appKey - application.yaml app.key
258
- * @param {string} value - Full value after KEY= (must start with url:// for caller)
259
- * @returns {string} Comma-joined secret names / literals
260
- */
261
- function mapUrlTemplateValueToKeyVaultNames(appKey, value) {
262
- const segments = value
263
- .split(',')
264
- .map((s) => s.trim())
265
- .filter((s) => s.length > 0);
266
- return segments
267
- .map((seg) => {
268
- if (seg.startsWith('url://')) {
269
- const token = seg.slice('url://'.length).trim();
270
- return urlTokenToKeyVaultSecretName(appKey, token);
271
- }
272
- return seg;
273
- })
274
- .join(',');
242
+ function determineVariableLocation(value) {
243
+ return value.startsWith('kv://') ? 'keyvault' : 'variable';
275
244
  }
276
245
 
277
246
  /**
@@ -280,26 +249,22 @@ function mapUrlTemplateValueToKeyVaultNames(appKey, value) {
280
249
  * @param {string} key - Variable key
281
250
  * @param {string} value - Variable value
282
251
  * @param {string} location - Variable location
283
- * @param {boolean} required - Whether variable is required
284
- * @param {Map} portalInputMap - Map of portalInput configurations
285
- * @param {string|null} appKey - application.yaml app.key (required when value uses url://)
252
+ * @param {Map} applicationConfigMap - Explicit application.yaml configuration
286
253
  * @returns {Object} Configuration item
287
254
  */
288
- function createConfigItem(key, value, location, required, portalInputMap, appKey) {
255
+ function createConfigItem(key, value, location, applicationConfigMap) {
289
256
  let storedValue = value;
290
257
  if (location === 'keyvault') {
291
258
  if (value.startsWith('kv://')) {
292
259
  storedValue = value.replace('kv://', '');
293
- } else if (value.startsWith('url://')) {
294
- if (!appKey) {
295
- throw new Error(
296
- `Cannot resolve ${key}=${value}: application app.key is required to map url:// to a Key Vault secret name`
297
- );
298
- }
299
- storedValue = mapUrlTemplateValueToKeyVaultNames(appKey, value);
300
260
  }
301
261
  }
302
262
 
263
+ const overlay = applicationConfigMap.get(key);
264
+ const required =
265
+ typeof overlay?.required === 'boolean'
266
+ ? overlay.required
267
+ : location === 'keyvault' && baselineRequiredForVaultKey(storedValue);
303
268
  const configItem = {
304
269
  name: key,
305
270
  value: storedValue,
@@ -307,10 +272,8 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
307
272
  required
308
273
  };
309
274
 
310
- // Merge portalInput / category if present in application config
311
- if (portalInputMap.has(key)) {
312
- const overlay = portalInputMap.get(key);
313
- configItem.portalInput = overlay.portalInput;
275
+ if (overlay) {
276
+ if (overlay.portalInput) configItem.portalInput = overlay.portalInput;
314
277
  if (typeof overlay.category === 'string' && overlay.category.length > 0) {
315
278
  configItem.category = overlay.category;
316
279
  }
@@ -322,26 +285,19 @@ function createConfigItem(key, value, location, required, portalInputMap, appKey
322
285
  function parseEnvironmentVariables(envTemplate, variablesConfig = null) {
323
286
  const configuration = [];
324
287
  const lines = envTemplate.split('\n');
325
- const portalInputMap = createPortalInputMap(variablesConfig);
326
- const appKey =
327
- variablesConfig && variablesConfig.app && variablesConfig.app.key
328
- ? String(variablesConfig.app.key).trim()
329
- : null;
330
-
288
+ const applicationConfigMap = createApplicationConfigMap(variablesConfig);
331
289
  for (const line of lines) {
332
290
  const parsed = parseEnvironmentVariableLine(line);
333
291
  if (!parsed) {
334
292
  continue;
335
293
  }
336
294
 
337
- const { location, required } = determineVariableLocation(parsed.value, parsed.key);
295
+ const location = determineVariableLocation(parsed.value);
338
296
  const configItem = createConfigItem(
339
297
  parsed.key,
340
298
  parsed.value,
341
299
  location,
342
- required,
343
- portalInputMap,
344
- appKey
300
+ applicationConfigMap
345
301
  );
346
302
  configuration.push(configItem);
347
303
  }
@@ -22,7 +22,6 @@ const { generateControllerManifest } = require('./external-controller-manifest')
22
22
  const { resolveVersionForApp } = require('../utils/image-version');
23
23
  const { getContainerPort } = require('../utils/port-resolver');
24
24
  const { buildEnvVarMap } = require('../utils/env-map');
25
- const { rewriteFrontDoorHostForAzureDeploy } = require('./deploy-manifest-azure-kv');
26
25
 
27
26
  /**
28
27
  * Generates deployment JSON from application configuration files
@@ -192,8 +191,6 @@ function buildAndValidateDeployment(appName, variables, envTemplate, rbac, optio
192
191
  substituteEnvVarsInDeployment(deployment, envVarMap);
193
192
  }
194
193
 
195
- rewriteFrontDoorHostForAzureDeploy(deployment);
196
-
197
194
  // Ensure no other ${...} placeholders remain in manifest
198
195
  _validator.validateNoUnresolvedVariablesInDeployment(deployment);
199
196
 
@@ -239,7 +236,6 @@ async function buildDeploymentManifestInMemory(appName, options = {}) {
239
236
  }
240
237
  const envVarMap = await buildEnvVarMap('docker', null, null, { appPort: effectivePort });
241
238
  substituteEnvVarsInDeployment(deployment, envVarMap);
242
- rewriteFrontDoorHostForAzureDeploy(deployment);
243
239
  _validator.validateNoUnresolvedVariablesInDeployment(deployment);
244
240
  return { deployment, appPath };
245
241
  }
@@ -57,7 +57,7 @@ function extractOptionalSections(deployment) {
57
57
  const names = [
58
58
  'healthCheck', 'authentication', 'build', 'repository', 'deployment',
59
59
  'startupCommand', 'runtimeVersion', 'scaling', 'frontDoorRouting',
60
- 'environmentScopedResources'
60
+ 'environmentScopedResources', 'required'
61
61
  ];
62
62
  for (const sectionName of names) {
63
63
  extractOptionalSection(deployment, sectionName, optional);
@@ -66,19 +66,25 @@ function extractOptionalSections(deployment) {
66
66
  }
67
67
 
68
68
  /**
69
- * Portal-only configuration for application.yaml (name + optional category + portalInput).
69
+ * Explicit application configuration metadata for application.yaml.
70
+ * required=false remains the default and is omitted.
70
71
  * @param {Array} configuration - Configuration array from deployment JSON
71
- * @returns {Array<{ name: string, category?: string, portalInput: Object }>}
72
+ * @returns {Array<{ name: string, category?: string, required?: boolean, portalInput?: Object }>}
72
73
  */
73
74
  function extractPortalInputConfiguration(configuration) {
74
75
  if (!Array.isArray(configuration) || configuration.length === 0) return [];
75
76
  return configuration
76
- .filter(item => item && item.portalInput && typeof item.name === 'string')
77
+ .filter(item => item && typeof item.name === 'string' && (
78
+ item.portalInput || item.required === true ||
79
+ (typeof item.category === 'string' && item.category.trim().length > 0)
80
+ ))
77
81
  .map(item => {
78
- const entry = { name: item.name, portalInput: item.portalInput };
82
+ const entry = { name: item.name };
83
+ if (item.portalInput) entry.portalInput = item.portalInput;
79
84
  if (typeof item.category === 'string' && item.category.trim().length > 0) {
80
85
  entry.category = item.category.trim();
81
86
  }
87
+ if (item.required === true) entry.required = true;
82
88
  return entry;
83
89
  });
84
90
  }
@@ -137,8 +137,37 @@ function appendFileSync(p, data, opts) {
137
137
  return delegate('appendFileSync', opts !== undefined ? [p, data, opts] : [p, data]);
138
138
  }
139
139
 
140
+ /**
141
+ * @param {string} p
142
+ * @param {number|string} mode
143
+ * @returns {void}
144
+ */
145
+ function chmodSync(p, mode) {
146
+ return delegate('chmodSync', [p, mode]);
147
+ }
148
+
149
+ /**
150
+ * @param {string} p
151
+ * @returns {import('node:fs').Stats}
152
+ */
153
+ function lstatSync(p) {
154
+ return delegate('lstatSync', [p]);
155
+ }
156
+
157
+ /**
158
+ * @param {string} from
159
+ * @param {string} to
160
+ * @returns {void}
161
+ */
162
+ function renameSync(from, to) {
163
+ return delegate('renameSync', [from, to]);
164
+ }
165
+
140
166
  module.exports = {
141
167
  existsSync,
168
+ chmodSync,
169
+ lstatSync,
170
+ renameSync,
142
171
  readFileSync,
143
172
  readFileSyncDirect,
144
173
  writeFileSync,
@@ -14,6 +14,7 @@ const { withWorkspaceLock } = require('./workspace-lock');
14
14
  const { getIdempotentResult, setIdempotentResult } = require('./idempotency-cache');
15
15
  const { withRequestTimeout } = require('./request-timeout');
16
16
  const { isHttpRawResult } = require('./http-raw-response');
17
+ const { isHttpAcceptedResult } = require('./http-accepted-response');
17
18
  const { publicOriginFromRequest } = require('../channels/vendor-dataplane-url');
18
19
 
19
20
  /**
@@ -103,10 +104,11 @@ async function runHandler(auth, capability, params, body, query) {
103
104
  * @param {Object} options.body
104
105
  * @param {string} options.requestId
105
106
  * @param {Object} options.result
107
+ * @param {number} [options.httpStatus=200] - 202 for accepted-async operations (upload/deploy); idempotency replay preserves it.
106
108
  * @returns {{ httpStatus: number, body: Object, requestId: string, userId: string }}
107
109
  */
108
110
  function buildSuccessResponse(options) {
109
- const { auth, idempotencyKey, capability, routeKey, params, body, requestId, result } = options;
111
+ const { auth, idempotencyKey, capability, routeKey, params, body, requestId, result, httpStatus = 200 } = options;
110
112
  const responseBody = { success: true, data: result, correlationId: requestId };
111
113
  if (idempotencyKey && isMutating(capability)) {
112
114
  setIdempotentResult(
@@ -116,10 +118,10 @@ function buildSuccessResponse(options) {
116
118
  route: routeKey,
117
119
  systemKey: resolveIdempotencySystemKey(params, body)
118
120
  },
119
- { httpStatus: 200, body: responseBody, correlationId: requestId }
121
+ { httpStatus, body: responseBody, correlationId: requestId }
120
122
  );
121
123
  }
122
- return { httpStatus: 200, body: responseBody, requestId, userId: auth.userId };
124
+ return { httpStatus, body: responseBody, requestId, userId: auth.userId };
123
125
  }
124
126
 
125
127
  /**
@@ -163,6 +165,7 @@ async function executeCapabilityRequest(options) {
163
165
  if (isHttpRawResult(result)) {
164
166
  return { httpStatus: 200, raw: result, requestId, userId: auth.userId };
165
167
  }
168
+ const accepted = isHttpAcceptedResult(result);
166
169
  return buildSuccessResponse({
167
170
  auth,
168
171
  idempotencyKey: req.headers['idempotency-key'],
@@ -171,7 +174,8 @@ async function executeCapabilityRequest(options) {
171
174
  params,
172
175
  body,
173
176
  requestId,
174
- result
177
+ result: accepted ? result.data : result,
178
+ httpStatus: accepted ? 202 : 200
175
179
  });
176
180
  }
177
181
 
@@ -48,7 +48,9 @@ const CAPABILITIES = Object.freeze([
48
48
  { method: 'POST', route: '/systems/:systemKey/certify/:pillar', handlerId: 'runCertifyPillar', operationType: 'certify', mutating: true },
49
49
  { method: 'GET', route: '/systems/:systemKey/lifecycle', handlerId: 'getLifecycleReport', operationType: 'certify.read', mutating: false },
50
50
  { method: 'POST', route: '/systems/:systemKey/upload', handlerId: 'runUpload', operationType: 'apply.dev', mutating: true },
51
+ { method: 'GET', route: '/systems/:systemKey/upload/:jobId/status', handlerId: 'getUploadStatus', operationType: 'apply.dev.read', mutating: false },
51
52
  { method: 'POST', route: '/systems/:systemKey/deploy', handlerId: 'runDeploySystem', operationType: 'apply.prod', mutating: true },
53
+ { method: 'GET', route: '/systems/:systemKey/deploy/:deploymentId/status', handlerId: 'getDeployStatus', operationType: 'apply.prod.read', mutating: false },
52
54
  { method: 'POST', route: '/systems/:systemKey/download', handlerId: 'runDownload', operationType: 'workspace.write', mutating: true },
53
55
  {
54
56
  method: 'POST',
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builder-api status read for an async external system deploy (plan 191.0 task 3).
3
+ * Thin proxy over Miso's own pipeline deployment status — Builder never invents deployment state.
4
+ *
5
+ * @fileoverview GET /systems/:systemKey/deploy/:deploymentId/status
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { runWithWorkspaceContext } = require('./workspace-context');
13
+ const { buildBearerAuthConfig } = require('./auth-config');
14
+ const { getPipelineDeployment } = require('../api/pipeline.api');
15
+ const { formatApiError } = require('../utils/api-error-handler');
16
+
17
+ /**
18
+ * @param {Object} res
19
+ * @param {string} controllerUrl
20
+ * @returns {Error & { statusCode: number }}
21
+ */
22
+ function toDeployStatusError(res, controllerUrl) {
23
+ const err = new Error(formatApiError(res, controllerUrl));
24
+ const status = Number(res.status || res.statusCode);
25
+ err.statusCode = status >= 400 && status < 600 ? status : 502;
26
+ return err;
27
+ }
28
+
29
+ /**
30
+ * @param {import('./workspace-context').WorkspaceContext} ctx
31
+ * @param {string} deploymentId
32
+ * @returns {Promise<Object>} Miso deployment status (id, status, progress, updatedAt, qualification, ...)
33
+ */
34
+ async function getDeployStatus(ctx, deploymentId) {
35
+ return runWithWorkspaceContext(ctx, async() => {
36
+ const authConfig = buildBearerAuthConfig(ctx.bearerToken);
37
+ const res = await getPipelineDeployment(ctx.controllerUrl, ctx.environment, deploymentId, authConfig);
38
+ if (res?.success === false) {
39
+ throw toDeployStatusError(res, ctx.controllerUrl);
40
+ }
41
+ return res.data ?? res;
42
+ });
43
+ }
44
+
45
+ module.exports = { getDeployStatus };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Builder-api status read for an async qualification upload job (plan 191.0 task 3).
3
+ * Thin proxy over Dataplane's own publish-job status — Builder never invents job state.
4
+ *
5
+ * @fileoverview GET /systems/:systemKey/upload/:jobId/status
6
+ * @author AI Fabrix Team
7
+ * @version 1.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const { runWithWorkspaceContext } = require('./workspace-context');
13
+ const { buildProgrammaticAuthContext } = require('./auth-config');
14
+ const { getPublishJobStatus } = require('../api/pipeline.api');
15
+ const { formatApiError } = require('../utils/api-error-handler');
16
+
17
+ /**
18
+ * @param {Object} res
19
+ * @param {string} dataplaneUrl
20
+ * @returns {Error & { statusCode: number }}
21
+ */
22
+ function toJobStatusError(res, dataplaneUrl) {
23
+ const err = new Error(formatApiError(res, dataplaneUrl));
24
+ const status = Number(res.status || res.statusCode);
25
+ err.statusCode = status >= 400 && status < 600 ? status : 502;
26
+ return err;
27
+ }
28
+
29
+ /**
30
+ * @param {import('./workspace-context').WorkspaceContext} ctx
31
+ * @param {string} jobId
32
+ * @returns {Promise<Object>} Dataplane PublishJobStatusResponse (jobId, state, steps, error, result)
33
+ */
34
+ async function getUploadStatus(ctx, jobId) {
35
+ return runWithWorkspaceContext(ctx, async() => {
36
+ const { authConfig, dataplaneUrl } = buildProgrammaticAuthContext(ctx);
37
+ const res = await getPublishJobStatus(dataplaneUrl, authConfig, jobId);
38
+ if (res?.success === false) {
39
+ throw toJobStatusError(res, dataplaneUrl);
40
+ }
41
+ return res.data;
42
+ });
43
+ }
44
+
45
+ module.exports = { getUploadStatus };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Marker for builder-api handlers that accepted a durable async operation (202), instead of
3
+ * completing synchronously (200). Mirrors http-raw-response.js's escape-hatch pattern so
4
+ * executeCapabilityRequest can special-case the response status without every other handler
5
+ * needing to know about it (plan 191.0 task 3: replace 5-minute sync upload/deploy web requests
6
+ * with accepted operation responses).
7
+ *
8
+ * @fileoverview 202-accepted result marker for upload/deploy web handlers
9
+ * @author AI Fabrix Team
10
+ * @version 1.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ /**
16
+ * @param {Object} data - Operation identity/state to return in the response body (e.g. {jobId, candidateFingerprint} or {deploymentId, status}).
17
+ * @returns {{ __httpAccepted: true, data: Object }}
18
+ */
19
+ function createHttpAcceptedResult(data) {
20
+ return { __httpAccepted: true, data };
21
+ }
22
+
23
+ /**
24
+ * @param {unknown} result
25
+ * @returns {boolean}
26
+ */
27
+ function isHttpAcceptedResult(result) {
28
+ return Boolean(result && typeof result === 'object' && result.__httpAccepted === true);
29
+ }
30
+
31
+ module.exports = { createHttpAcceptedResult, isHttpAcceptedResult };
@@ -129,15 +129,25 @@ const OPERATION_DESCRIPTIONS = Object.freeze({
129
129
  tags: ['certify']
130
130
  },
131
131
  runUpload: {
132
- summary: 'Upload integration to dev Dataplane',
132
+ summary: 'Submit integration for durable Dataplane qualification (async)',
133
133
  description:
134
- 'Full dev publish: validate, sync deploy JSON, push credentials, pipeline upload. Requires external-system:publish in dev. Mutating; use Idempotency-Key; may take up to MAX_UPLOAD_TIMEOUT_MS.',
134
+ 'Validates, syncs deploy JSON, pushes credentials, then durably submits the candidate for Dataplane qualification (plan 191.0). Returns 202 immediately with {jobId, candidateFingerprint} — never blocks on the qualification ladder. Poll GET .../upload/{jobId}/status for terminal state. Requires external-system:publish in dev. Mutating; use Idempotency-Key.',
135
+ tags: ['apply']
136
+ },
137
+ getUploadStatus: {
138
+ summary: 'Read qualification job status',
139
+ description: 'Poll the durable Dataplane qualification job accepted by runUpload. Read-only proxy; terminal states are completed/failed.',
135
140
  tags: ['apply']
136
141
  },
137
142
  runDeploySystem: {
138
- summary: 'Deploy integration via controller (promote)',
143
+ summary: 'Deploy integration via controller (promote, async)',
139
144
  description:
140
- 'Controller deploy for external systems (CLI aifabrix deploy parity). Optional body.gate matches CLI --gate (operations,trust,governance,full). Certificate seal is a platform side-effect of published Deploy — not a separate certificates issue call. Mutating; use Idempotency-Key.',
145
+ 'Controller deploy for external systems (CLI aifabrix deploy parity). Optional body.gate is a no-op notice — qualification gates now run once during upload, not at deploy time. Returns 202 immediately with {deploymentId} — never blocks on the outer Miso deployment (fast when the candidate is already qualified, slower on the legacy ladder otherwise). Poll GET .../deploy/{deploymentId}/status for terminal state. Mutating; use Idempotency-Key.',
146
+ tags: ['apply']
147
+ },
148
+ getDeployStatus: {
149
+ summary: 'Read deployment status',
150
+ description: 'Poll the durable Miso deployment accepted by runDeploySystem. Read-only proxy; includes heartbeat (updatedAt) and, for qualified Connected Systems, a nested qualification/promotion summary.',
141
151
  tags: ['apply']
142
152
  },
143
153
  runDownload: {
@@ -8,27 +8,33 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ /**
12
+ * Per-handler {envVar, defaultMs}. Plan 191.0 task 3: runUpload/runDeploySystem are accept-only
13
+ * (202) — local validation + manifest build + one accept HTTP call, never the qualification
14
+ * ladder or outer deployment — so their budget shrunk from 300000 (the old
15
+ * blocking-on-the-full-ladder value). Their status-read siblings are short, single-proxy reads.
16
+ * @type {Readonly<Record<string, { envVar: string, defaultMs: number }>>}
17
+ */
18
+ const HANDLER_TIMEOUT_CONFIG = Object.freeze({
19
+ runUpload: { envVar: 'MAX_UPLOAD_TIMEOUT_MS', defaultMs: 45000 },
20
+ getUploadStatus: { envVar: 'MAX_UPLOAD_STATUS_TIMEOUT_MS', defaultMs: 15000 },
21
+ runDeploySystem: { envVar: 'MAX_DEPLOY_TIMEOUT_MS', defaultMs: 45000 },
22
+ getDeployStatus: { envVar: 'MAX_DEPLOY_STATUS_TIMEOUT_MS', defaultMs: 15000 },
23
+ runDownload: { envVar: 'MAX_DOWNLOAD_TIMEOUT_MS', defaultMs: 300000 },
24
+ runTest: { envVar: 'MAX_TEST_E2E_TIMEOUT_MS', defaultMs: 120000 },
25
+ runCertifyPillar: { envVar: 'MAX_CERTIFY_TIMEOUT_MS', defaultMs: 120000 }
26
+ });
27
+
11
28
  /**
12
29
  * @param {string} handlerId
13
30
  * @returns {number}
14
31
  */
15
32
  function getTimeoutMsForHandler(handlerId) {
16
- if (handlerId === 'runUpload') {
17
- return Number(process.env.MAX_UPLOAD_TIMEOUT_MS || 300000);
18
- }
19
- if (handlerId === 'runDeploySystem') {
20
- return Number(process.env.MAX_DEPLOY_TIMEOUT_MS || 300000);
21
- }
22
- if (handlerId === 'runDownload') {
23
- return Number(process.env.MAX_DOWNLOAD_TIMEOUT_MS || 300000);
24
- }
25
- if (handlerId === 'runTest') {
26
- return Number(process.env.MAX_TEST_E2E_TIMEOUT_MS || 120000);
27
- }
28
- if (handlerId === 'runCertifyPillar') {
29
- return Number(process.env.MAX_CERTIFY_TIMEOUT_MS || 120000);
33
+ const config = HANDLER_TIMEOUT_CONFIG[handlerId];
34
+ if (!config) {
35
+ return Number(process.env.MAX_REQUEST_TIMEOUT_MS || 120000);
30
36
  }
31
- return Number(process.env.MAX_REQUEST_TIMEOUT_MS || 120000);
37
+ return Number(process.env[config.envVar] || config.defaultMs);
32
38
  }
33
39
 
34
40
  /**
@@ -19,6 +19,8 @@ const { runRepair } = require('./run-repair');
19
19
  const { runValidate } = require('./run-validate');
20
20
  const { runPreviewUpload, runUpload } = require('./run-upload');
21
21
  const { runDeploySystem } = require('./run-deploy');
22
+ const { getUploadStatus } = require('./get-upload-status');
23
+ const { getDeployStatus } = require('./get-deploy-status');
22
24
  const { runDownload } = require('./run-download');
23
25
  const { runProtectionUpload, runProtectionCreate } = require('./run-protection');
24
26
  const { runTest } = require('./run-test');
@@ -157,6 +159,8 @@ const HANDLER_MAP = {
157
159
  probe: body.probe === true,
158
160
  minimal: true
159
161
  }),
162
+ getUploadStatus: (params, auth) =>
163
+ getUploadStatus(buildContext(auth, params.systemKey), params.jobId),
160
164
  runDeploySystem: (params, auth, body) =>
161
165
  runDeploySystem(buildContext(auth, params.systemKey), {
162
166
  gate: body.gate,
@@ -165,6 +169,8 @@ const HANDLER_MAP = {
165
169
  sync: body.sync === true,
166
170
  verbose: body.verbose === true
167
171
  }),
172
+ getDeployStatus: (params, auth) =>
173
+ getDeployStatus(buildContext(auth, params.systemKey), params.deploymentId),
168
174
  runDownload: (params, auth, body = {}) =>
169
175
  runDownload(buildContext(auth, params.systemKey), {
170
176
  force: body.force === true,
@@ -12,6 +12,7 @@ const { deployExternalSystem } = require('../external-system/deploy');
12
12
  const { runWithWorkspaceContext } = require('./workspace-context');
13
13
  const { mergeProgrammaticOptions } = require('./command-options');
14
14
  const { touchWorkspaceMeta } = require('./workspace-meta');
15
+ const { createHttpAcceptedResult } = require('./http-accepted-response');
15
16
 
16
17
  /**
17
18
  * @param {unknown} gate
@@ -39,9 +40,11 @@ function attachDeployStatus(err) {
39
40
  }
40
41
 
41
42
  /**
43
+ * Builder-api web callers never block on the outer Miso deployment (plan 191.0 task 3) — accept
44
+ * only, return 202 with the deployment id immediately; poll via getDeployStatus.
42
45
  * @param {import('./workspace-context').WorkspaceContext} ctx
43
46
  * @param {Object} [options]
44
- * @param {string} [options.gate] - Same as CLI --gate (operations,trust,governance,full)
47
+ * @param {string} [options.gate] - Same as CLI --gate (operations,trust,governance,full); now a no-op notice, kept for compatibility.
45
48
  * @param {string} [options.subjectEmail]
46
49
  * @param {boolean} [options.probe]
47
50
  * @param {boolean} [options.sync]
@@ -55,13 +58,14 @@ async function runDeploySystem(ctx, options = {}) {
55
58
  subjectEmail: options.subjectEmail,
56
59
  probe: options.probe === true,
57
60
  sync: options.sync === true,
58
- verbose: options.verbose === true
61
+ verbose: options.verbose === true,
62
+ poll: false
59
63
  });
60
64
  return runWithWorkspaceContext(ctx, async() => {
61
65
  try {
62
66
  const result = await deployExternalSystem(ctx.systemKey, merged);
63
67
  touchWorkspaceMeta(ctx, { lastOperation: 'deploy' });
64
- return result && typeof result === 'object' ? result : { deployed: true };
68
+ return createHttpAcceptedResult(result && typeof result === 'object' ? result : { deployed: true });
65
69
  } catch (err) {
66
70
  throw attachDeployStatus(err);
67
71
  }