@aifabrix/builder 2.59.0 → 2.61.2

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 (145) hide show
  1. package/README.md +14 -11
  2. package/docs/README.md +80 -0
  3. package/docs/builder-help/evidence-patterns.json +155 -0
  4. package/docs/builder-help/golden-examples/crm-company.json +30 -0
  5. package/docs/builder-help/golden-examples/crm-deal.json +29 -0
  6. package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
  7. package/docs/builder-help/golden-examples/document-storage.json +30 -0
  8. package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
  9. package/docs/builder-help/golden-examples/repository-template.json +29 -0
  10. package/docs/builder-help/golden-examples/service-ticket.json +29 -0
  11. package/docs/builder-help/platform-roles.json +98 -0
  12. package/docs/builder-help/resource-type-catalog.json +402 -0
  13. package/lib/agent-kit/git-identity.js +180 -0
  14. package/lib/agent-kit/setup.js +3 -0
  15. package/lib/agent-kit/start.js +45 -1
  16. package/lib/api/configuration.api.js +131 -0
  17. package/lib/api/role-assistant-test-job.api.js +60 -0
  18. package/lib/api/system-secrets.api.js +72 -0
  19. package/lib/api/work-search.api.js +13 -6
  20. package/lib/app/deploy.js +8 -1
  21. package/lib/app/run-docker-fallback.js +6 -1
  22. package/lib/app/run-helpers.js +2 -1
  23. package/lib/app/run-parameter-sync.js +142 -0
  24. package/lib/app/show-display.js +1 -0
  25. package/lib/app/show-online.js +15 -0
  26. package/lib/build/docker-build-args.js +5 -2
  27. package/lib/build/index.js +3 -2
  28. package/lib/build/standard-docker-build.js +6 -3
  29. package/lib/cli/setup-app.js +19 -1
  30. package/lib/cli/setup-environment.js +156 -0
  31. package/lib/cli/setup-utility.js +24 -1
  32. package/lib/commands/datasource-capability-upsert-cli.js +112 -0
  33. package/lib/commands/datasource-capability.js +4 -2
  34. package/lib/commands/env-secret-context.js +113 -0
  35. package/lib/commands/env-secret-list.js +200 -0
  36. package/lib/commands/env-secret-push-confirm.js +54 -0
  37. package/lib/commands/env-secret-push-run.js +227 -0
  38. package/lib/commands/env-secret-push.js +168 -0
  39. package/lib/commands/repair-datasource-apply.js +2 -0
  40. package/lib/commands/repair-datasource-keyed-document.js +122 -0
  41. package/lib/commands/repair-datasource-run.js +1 -0
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/setup-modes.js +1 -8
  44. package/lib/commands/setup-prompts.js +2 -180
  45. package/lib/commands/verify-operations-skip-e2e.js +25 -1
  46. package/lib/commands/verify-operations-steps.js +13 -1
  47. package/lib/commands/wizard-config-normalizer.js +7 -4
  48. package/lib/commands/wizard-core.js +5 -157
  49. package/lib/commands/wizard-file-saving.js +163 -0
  50. package/lib/core/env-platform-expand.js +97 -0
  51. package/lib/core/secrets-env-content.js +42 -4
  52. package/lib/core/secrets-env-write.js +10 -3
  53. package/lib/core/secrets-load.js +4 -2
  54. package/lib/datasource/binary-documents-validator.js +190 -0
  55. package/lib/datasource/capability/run-capability-upsert.js +202 -0
  56. package/lib/datasource/capability/upsert-ingredients.js +291 -0
  57. package/lib/datasource/capability/upsert-operations.js +138 -0
  58. package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
  59. package/lib/datasource/validate.js +12 -5
  60. package/lib/deployment/installation/azure-infra-stage.js +3 -1
  61. package/lib/deployment/installation/infra-catalog.js +2 -5
  62. package/lib/generator/builders.js +17 -0
  63. package/lib/generator/helpers.js +23 -2
  64. package/lib/generator/index.js +13 -6
  65. package/lib/lifecycle/product-model.js +4 -3
  66. package/lib/lifecycle/report-display.js +3 -2
  67. package/lib/parameters/infra-parameter-catalog.js +1 -1
  68. package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
  69. package/lib/programmatic/builder-help-governance.js +1 -1
  70. package/lib/programmatic/builder-help.js +1 -1
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
  75. package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
  76. package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
  77. package/lib/role-assistant/test-runner-workhub.js +28 -19
  78. package/lib/schema/application-schema.json +205 -2
  79. package/lib/schema/external-datasource.schema.json +23 -3
  80. package/lib/schema/infra-parameter.schema.json +139 -33
  81. package/lib/schema/infra.parameter.yaml +416 -66
  82. package/lib/schema/infrastructure-schema.json +10 -35
  83. package/lib/utils/compose-generate-docker-compose.js +16 -9
  84. package/lib/utils/datasource-binary-evidence.js +92 -0
  85. package/lib/utils/datasource-test-run-capability-scope.js +44 -1
  86. package/lib/utils/datasource-test-run-debug-display.js +2 -0
  87. package/lib/utils/datasource-test-run-display.js +8 -2
  88. package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
  89. package/lib/utils/datasource-test-run-tty-log.js +2 -0
  90. package/lib/utils/docker-build.js +29 -8
  91. package/lib/utils/docker-manifest-public-port.js +36 -0
  92. package/lib/utils/env-copy.js +11 -10
  93. package/lib/utils/external-system-system-test-tty.js +3 -2
  94. package/lib/utils/image-tags.js +2 -2
  95. package/lib/utils/platform-kv-ref.js +1 -1
  96. package/lib/utils/platform-resolution.js +226 -0
  97. package/lib/utils/prepare-local-data-mount.js +58 -0
  98. package/lib/utils/resolve-docker-image-ref.js +8 -3
  99. package/lib/utils/secrets-helpers.js +0 -1
  100. package/lib/utils/system-secret-mapping.js +125 -0
  101. package/lib/utils/test-log-writer.js +2 -1
  102. package/lib/validation/external-manifest-validator.js +5 -0
  103. package/lib/validation/openapi-contract-surface-validator.js +3 -1
  104. package/lib/validation/validate-external-file.js +5 -1
  105. package/package.json +5 -4
  106. package/templates/README.md +2 -1
  107. package/templates/agent-kit/agent-kit.yaml +4 -1
  108. package/templates/agent-kit/instructions/AGENTKIT.md +1 -1
  109. package/templates/agent-kit/instructions/root.AGENTS.md +2 -0
  110. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +35 -2
  111. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
  112. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  113. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +6 -2
  114. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +28 -2
  115. package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
  116. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +28 -1
  117. package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
  118. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  119. package/templates/agent-kit/skills/shared/hosts.md +13 -3
  120. package/templates/agent-kit/skills/shared/interaction.md +83 -0
  121. package/templates/agent-kit/skills/shared/status.md +76 -0
  122. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  123. package/templates/applications/builder-api/application.yaml +1 -1
  124. package/templates/applications/builder-api/env.template +5 -1
  125. package/templates/applications/dataplane/application.yaml +30 -2
  126. package/templates/applications/dataplane/env.template +36 -5
  127. package/templates/applications/keycloak/application.yaml +6 -1
  128. package/templates/applications/miso-controller/application.yaml +96 -1
  129. package/templates/applications/miso-controller/env.template +63 -37
  130. package/templates/applications/miso-controller/rbac.yaml +17 -0
  131. package/templates/external-system/external-datasource.yaml.hbs +7 -1
  132. package/templates/marketplace/main.json +73 -225
  133. package/templates/python/Dockerfile.hbs +2 -0
  134. package/templates/python/docker-compose.hbs +1 -1
  135. package/templates/typescript/Dockerfile.hbs +2 -0
  136. package/templates/typescript/docker-compose.hbs +12 -0
  137. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -34
  138. /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
  139. /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
  140. /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
  141. /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
  142. /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
  143. /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
  144. /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
  145. /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
@@ -17,7 +17,7 @@ const {
17
17
  wantsSkillsOnlyPath
18
18
  } = require('./test-runner-workhub-helpers');
19
19
 
20
- function operationKeys(spec) {
20
+ function directOperationKeys(spec) {
21
21
  const raw = Array.isArray(spec && spec.operations) ? spec.operations : [];
22
22
  const keys = [];
23
23
  for (const item of raw) {
@@ -33,6 +33,42 @@ function operationKeys(spec) {
33
33
  return keys;
34
34
  }
35
35
 
36
+ function operationClassKeys(spec) {
37
+ const classes = spec && typeof spec.operationClasses === 'object'
38
+ ? spec.operationClasses
39
+ : {};
40
+ const keys = [];
41
+ for (const rows of Object.values(classes)) {
42
+ if (!Array.isArray(rows)) {
43
+ continue;
44
+ }
45
+ for (const row of rows) {
46
+ const key = String(row || '').trim();
47
+ if (key && !keys.includes(key)) {
48
+ keys.push(key);
49
+ }
50
+ }
51
+ }
52
+ return keys;
53
+ }
54
+
55
+ function operationKeys(spec) {
56
+ return [...new Set([...directOperationKeys(spec), ...operationClassKeys(spec)])];
57
+ }
58
+
59
+ function skillsRequestOptions(testCase) {
60
+ if (!wantsSkillsOnlyPath(testCase)) {
61
+ return undefined;
62
+ }
63
+ const input = resolveInput(testCase);
64
+ const resourceType = String(input.resourceType || '').trim();
65
+ const operation = String(input.operation || '').trim();
66
+ if (operation) {
67
+ return { detail: 'operation', resourceType, operation };
68
+ }
69
+ return resourceType ? { detail: 'resource', resourceType } : undefined;
70
+ }
71
+
36
72
  function pickResourceType(skillsPayload, operation) {
37
73
  const rts = resourceTypesMap(skillsPayload);
38
74
  const keys = Object.keys(rts);
@@ -139,7 +175,12 @@ async function runSearchCaseLifecycle(roleKey, testCase, runtimeContext) {
139
175
  throw new Error('Role assistant search case requires dataplane auth context.');
140
176
  }
141
177
  const roleRef = resolveRoleRef(roleKey, testCase);
142
- const skillsRes = await getWorkSkills(dataplaneUrl, authConfig, roleRef);
178
+ const skillsRes = await getWorkSkills(
179
+ dataplaneUrl,
180
+ authConfig,
181
+ roleRef,
182
+ skillsRequestOptions(testCase)
183
+ );
143
184
  if (wantsSkillsOnlyPath(testCase)) {
144
185
  return searchLifecycleResult(roleKey, testCase, roleRef, skillsRes, null, null);
145
186
  }
@@ -157,6 +198,7 @@ module.exports = {
157
198
  wantsSearchPath,
158
199
  wantsSkillsOnlyPath,
159
200
  operationKeys,
201
+ skillsRequestOptions,
160
202
  pickResourceType,
161
203
  resolveRoleRef,
162
204
  buildSearchArguments,
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const { coversMissingFields } = require('./test-runner-workhub-missing-fields');
9
+
8
10
  const {
9
11
  resolveTargetSystemKey,
10
12
  shouldStampSuiteBid,
@@ -409,7 +411,8 @@ function resolveRespondValue(testCase, wait, statusRow, objective) {
409
411
  if (fromCase !== undefined) {
410
412
  return fromCase;
411
413
  }
412
- return synthesizeWaitAnswer(wait, statusRow, objective, testCase);
414
+ const synthesized = synthesizeWaitAnswer(wait, statusRow, objective, testCase);
415
+ return coversMissingFields(wait, synthesized) ? synthesized : undefined;
413
416
  }
414
417
 
415
418
  function waitRespondFingerprint(wait) {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview Missing-field coverage for synthesized Work Hub answers.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ function asPlainObject(value) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
9
+ return {};
10
+ }
11
+ return value;
12
+ }
13
+
14
+ /**
15
+ * Fields the runtime named as missing (missingRequestBody waits). A synthesized
16
+ * answer that lacks any of them only makes the runtime ask the same question
17
+ * again, so it must not be sent.
18
+ * @param {Object} wait
19
+ * @returns {string[]}
20
+ */
21
+ function waitMissingFields(wait) {
22
+ const raw = wait?.objectRef && typeof wait.objectRef === 'object' ? wait.objectRef.missingFields : null;
23
+ const list = Array.isArray(raw) ? raw : String(raw || '').split(',');
24
+ return list.map((row) => String(row || '').trim()).filter(Boolean);
25
+ }
26
+
27
+ function coversMissingFields(wait, value) {
28
+ if (value === undefined) {
29
+ return false;
30
+ }
31
+ const missing = waitMissingFields(wait);
32
+ if (missing.length === 0) {
33
+ return true;
34
+ }
35
+ const obj = asPlainObject(value);
36
+ if (!obj) {
37
+ return false;
38
+ }
39
+ return missing.every((field) => obj[field] !== undefined && obj[field] !== null && obj[field] !== '');
40
+ }
41
+
42
+ module.exports = { coversMissingFields, waitMissingFields };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview Work Hub poll stop rules for waits the runner cannot resolve.
3
+ *
4
+ * A wait with no matching case answer, a question the runtime re-issues
5
+ * unchanged after our answer, or a failed respond call never resolves by
6
+ * polling. Stopping at once keeps a stalled case at seconds, not the 180 s
7
+ * poll timeout (dev01 sales ladder, 2026-09-26: six cases, 18 min waiting).
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const UNANSWERED_WAIT_TYPES = new Set(['respond-skipped', 'respond-repeated', 'respond-error']);
13
+
14
+ /**
15
+ * The runtime asked the same question again after our answer: the answer did
16
+ * not resolve it, and repeating it cannot. Record once so the poll can stop.
17
+ */
18
+ function recordRepeatedResponse(interactions, peId, fingerprint, wait) {
19
+ const already = interactions.some(
20
+ (row) => row && row.type === 'respond-repeated' && row.fingerprint === fingerprint
21
+ );
22
+ if (already) {
23
+ return;
24
+ }
25
+ interactions.push({
26
+ type: 'respond-repeated',
27
+ processExecutionId: peId,
28
+ fingerprint,
29
+ questionKey: wait.questionKey || wait.requestKey || null,
30
+ objectRefKind: wait.objectRef?.kind || null,
31
+ reason: 'same-question-after-answer'
32
+ });
33
+ }
34
+
35
+ function recordRespondError(interactions, peId, error) {
36
+ interactions.push({
37
+ type: 'respond-error',
38
+ processExecutionId: peId,
39
+ error: (error && error.message) || String(error)
40
+ });
41
+ }
42
+
43
+ /**
44
+ * A wait we could not answer (no matching case answer, the runtime re-asked
45
+ * after our answer, or respond failed) will not change by polling: stop now
46
+ * instead of waiting for the poll timeout.
47
+ * @param {Array<Object>} interactions
48
+ * @returns {string|null} stop reason or null to keep polling
49
+ */
50
+ function unansweredWaitStopReason(interactions) {
51
+ const last = interactions.length > 0 ? interactions[interactions.length - 1] : null;
52
+ if (!last || !UNANSWERED_WAIT_TYPES.has(last.type)) {
53
+ return null;
54
+ }
55
+ return last.type === 'respond-error' ? 'respondFailed' : 'unansweredQuestion';
56
+ }
57
+
58
+ function recordSkippedResponse(interactions, peId, fingerprint, wait) {
59
+ interactions.push({
60
+ type: 'respond-skipped',
61
+ processExecutionId: peId,
62
+ fingerprint,
63
+ questionKey: wait.questionKey || wait.requestKey || null,
64
+ objectRefKind: wait.objectRef?.kind || null,
65
+ reason: 'no-matching-answer-or-synthesis'
66
+ });
67
+ }
68
+
69
+ const ALREADY_TERMINAL_RE = /already terminal/i;
70
+
71
+ /**
72
+ * work.respond resumes the Decision Engine inline; by the time work.continue
73
+ * lands the execution may already be terminal. That is a finished run, not a
74
+ * failed answer.
75
+ */
76
+ async function continueAfterRespond(dataplaneUrl, authConfig, peId) {
77
+ const { continueWorkExecution } = require('../api/work.api');
78
+ try {
79
+ await continueWorkExecution(dataplaneUrl, authConfig, peId, {});
80
+ } catch (err) {
81
+ if (!ALREADY_TERMINAL_RE.test((err && err.message) || String(err))) {
82
+ throw err;
83
+ }
84
+ }
85
+ }
86
+
87
+ module.exports = {
88
+ continueAfterRespond,
89
+ UNANSWERED_WAIT_TYPES,
90
+ recordSkippedResponse,
91
+ recordRepeatedResponse,
92
+ recordRespondError,
93
+ unansweredWaitStopReason
94
+ };
@@ -13,8 +13,7 @@ const {
13
13
  getWorkTask,
14
14
  approveWorkTask,
15
15
  getWorkExecutionStatus,
16
- respondWorkExecution,
17
- continueWorkExecution
16
+ respondWorkExecution
18
17
  } = require('../api/work.api');
19
18
  const {
20
19
  synthesizeWaitAnswer,
@@ -29,6 +28,13 @@ const {
29
28
  persistCaseOpenApiViaBuilderApi
30
29
  } = require('./test-runner-bid-apply');
31
30
  const { resolveTargetSystemKey } = require('./test-runner-bid-document');
31
+ const {
32
+ recordSkippedResponse,
33
+ recordRepeatedResponse,
34
+ recordRespondError,
35
+ unansweredWaitStopReason,
36
+ continueAfterRespond
37
+ } = require('./test-runner-workhub-wait-stop');
32
38
  const {
33
39
  TERMINAL_TASK,
34
40
  delayMs,
@@ -178,17 +184,6 @@ async function maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx) {
178
184
  await _attemptApproveTask(dataplaneUrl, authConfig, taskId, ctx);
179
185
  }
180
186
 
181
- function recordSkippedResponse(interactions, peId, fingerprint, wait) {
182
- interactions.push({
183
- type: 'respond-skipped',
184
- processExecutionId: peId,
185
- fingerprint,
186
- questionKey: wait.questionKey || wait.requestKey || null,
187
- objectRefKind: wait.objectRef?.kind || null,
188
- reason: 'no-matching-answer-or-synthesis'
189
- });
190
- }
191
-
192
187
  function recordBidEmit(testCase, interactions, peId) {
193
188
  const { loadCaseBidDocument } = require('./test-runner-workhub-answers');
194
189
  const emittedBid = loadCaseBidDocument(testCase);
@@ -237,6 +232,7 @@ async function respondToWaitingExecution(
237
232
  const wait = statusRow.wait && typeof statusRow.wait === 'object' ? statusRow.wait : {};
238
233
  const fingerprint = waitRespondFingerprint(wait);
239
234
  if (alreadyResponded(interactions, fingerprint)) {
235
+ recordRepeatedResponse(interactions, peId, fingerprint, wait);
240
236
  return;
241
237
  }
242
238
  const payload = buildRespondPayload(
@@ -260,7 +256,7 @@ async function respondToWaitingExecution(
260
256
  }
261
257
  }
262
258
  await respondWorkExecution(dataplaneUrl, authConfig, peId, payload);
263
- await continueWorkExecution(dataplaneUrl, authConfig, peId, {});
259
+ await continueAfterRespond(dataplaneUrl, authConfig, peId);
264
260
  interactions.push({ type: 'respond', processExecutionId: peId, fingerprint });
265
261
  if (shouldApplyBidAfterWait(testCase, wait)) {
266
262
  recordBidEmit(testCase, interactions, peId);
@@ -291,8 +287,9 @@ async function maybeRespondWaiting(dataplaneUrl, authConfig, taskRow, testCase,
291
287
  interactions,
292
288
  statusRow
293
289
  );
294
- } catch (_err) {
295
- // Soft: continue polling task.
290
+ } catch (err) {
291
+ // A failed respond/continue never resolves itself; record it so the poll stops.
292
+ recordRespondError(interactions, peId, err);
296
293
  }
297
294
  }
298
295
 
@@ -301,7 +298,13 @@ function assertTerminalWorkHubTask(last, taskId, timeoutMs, options = {}) {
301
298
  throw new Error(`Work Hub task ${taskId} produced no status rows.`);
302
299
  }
303
300
  const finalStatusToken = String(last?.status || last?.taskStatus || '').trim().toLowerCase();
304
- if (!TERMINAL_TASK.has(finalStatusToken) && options.recordTimeout === true) {
301
+ if (TERMINAL_TASK.has(finalStatusToken)) {
302
+ return last;
303
+ }
304
+ if (options.stoppedReason) {
305
+ return { ...last, pollStopped: options.stoppedReason, lastStatus: finalStatusToken || 'unknown' };
306
+ }
307
+ if (options.recordTimeout === true) {
305
308
  last = {
306
309
  ...last,
307
310
  pollTimedOut: true,
@@ -353,14 +356,18 @@ async function pollWorkHubTask(dataplaneUrl, authConfig, taskId, testCase, optio
353
356
  applyWorkHubPollStatus(ctx, last, status);
354
357
  await maybeApproveTask(dataplaneUrl, authConfig, taskId, ctx);
355
358
  await maybeRespondWaiting(dataplaneUrl, authConfig, last, testCase, ctx.interactions);
356
- if (shouldStopPendingApprovalPoll(status, ctx)) {
359
+ ctx.stoppedReason = unansweredWaitStopReason(ctx.interactions);
360
+ if (ctx.stoppedReason || shouldStopPendingApprovalPoll(status, ctx)) {
357
361
  break;
358
362
  }
359
363
  await delayMs(intervalMs);
360
364
  }
361
365
 
362
366
  return {
363
- finalTask: assertTerminalWorkHubTask(last, taskId, timeoutMs, { recordTimeout: true }),
367
+ finalTask: assertTerminalWorkHubTask(last, taskId, timeoutMs, {
368
+ recordTimeout: !ctx.stoppedReason,
369
+ stoppedReason: ctx.stoppedReason || null
370
+ }),
364
371
  timeline,
365
372
  interactions: ctx.interactions
366
373
  };
@@ -478,6 +485,8 @@ async function runWorkHubCaseLifecycle(roleKey, testCase, runtimeContext, option
478
485
  module.exports = {
479
486
  wantsWorkHubPath,
480
487
  runWorkHubCaseLifecycle,
488
+ pollWorkHubTask,
489
+ unansweredWaitStopReason,
481
490
  planAlreadyActivated,
482
491
  isPlanLostError,
483
492
  extractPeFromTask,
@@ -7,12 +7,12 @@
7
7
  "key": "application-schema",
8
8
  "name": "Application Configuration Schema",
9
9
  "description": "JSON schema for validating AI Fabrix application configuration files",
10
- "version": "1.7.0",
10
+ "version": "1.10.0",
11
11
  "type": "schema",
12
12
  "category": "infrastructure",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2024-01-01T00:00:00Z",
15
- "updatedAt": "2026-09-16T00:00:00Z",
15
+ "updatedAt": "2026-09-25T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "1.0.0",
18
18
  "maxVersion": "2.0.0",
@@ -30,6 +30,35 @@
30
30
  "external-datasource.schema.json"
31
31
  ],
32
32
  "changelog": [
33
+ {
34
+ "version": "1.10.0",
35
+ "date": "2026-09-25T00:00:00Z",
36
+ "changes": [
37
+ "Added optional top-level webSocketsEnabled: request Azure App Service WebSocket support for applications whose manifest opts in. Absent or false preserves existing deployment behavior."
38
+ ],
39
+ "breaking": false
40
+ },
41
+ {
42
+ "version": "1.9.0",
43
+ "date": "2026-09-23T00:00:00Z",
44
+ "breaking": false,
45
+ "changes": [
46
+ "Added optional platforms: the target Azure resource group is matched against resource patterns to yield {platform} and {platform.name}, and that entry's configuration resolves from them.",
47
+ "Added optional environments: per-environment configuration layered over the platform match, overriding by name.",
48
+ "Added optional portalInput.visible (default true) so a resolved or derived value stays out of the portal form.",
49
+ "Both sections are optional and independent; manifests without them are unchanged."
50
+ ]
51
+ },
52
+ {
53
+ "version": "1.8.0",
54
+ "date": "2026-09-21T00:00:00Z",
55
+ "changes": [
56
+ "Added optional build.target: the multi-stage Dockerfile stage to build (docker build --target). Omitted, Docker builds the final stage as before, so existing manifests are unaffected. Lets one Dockerfile and one source tree produce more than one image.",
57
+ "Added optional top-level runAsRoot: run the container as the image user (root) rather than the invoking host user. Absent or false keeps the existing behavior. Only for an application that administers its own operating system, such as creating Linux accounts or running sshd.",
58
+ "Added optional top-level additionalPorts: extra container ports to publish beside the main port, for an application serving a second protocol such as SSH from the same container. Each host port is derived as hostPort + developerId * 100, the same formula as the main port. A base below 1024 is permitted so a well-known port keeps its number. Absent keeps the existing single-port behavior."
59
+ ],
60
+ "breaking": false
61
+ },
33
62
  {
34
63
  "version": "1.7.0",
35
64
  "date": "2026-09-16T00:00:00Z",
@@ -210,6 +239,128 @@
210
239
  "type": "boolean",
211
240
  "description": "Whether application requires storage.Physical storage is mapped to /mnt/data/. Blob storage is mapped to SMB File share /data/{app-key}/."
212
241
  },
242
+ "webSocketsEnabled": {
243
+ "type": "boolean",
244
+ "description": "Whether the hosting provider must enable WebSocket upgrades for this application. Optional and false by default."
245
+ },
246
+ "platforms": {
247
+ "type": "array",
248
+ "description": "Platform resolution: the target Azure resource group is matched against these entries, yielding {platform} and {platform.name}. Optional and independent of environments.",
249
+ "items": {
250
+ "type": "object",
251
+ "required": ["resource", "name", "configuration"],
252
+ "additionalProperties": false,
253
+ "properties": {
254
+ "resource": {
255
+ "type": "string",
256
+ "description": "Resource-group pattern; {platform} marks the platform segment and * captures a name segment",
257
+ "minLength": 1
258
+ },
259
+ "name": {
260
+ "type": "string",
261
+ "description": "Platform name: a literal, or * to take the captured segment",
262
+ "minLength": 1
263
+ },
264
+ "options": {
265
+ "type": "array",
266
+ "description": "Allowed platform values; required when resource contains {platform}",
267
+ "minItems": 1,
268
+ "items": { "type": "string", "minLength": 1 }
269
+ },
270
+ "platform": {
271
+ "type": "string",
272
+ "description": "Declared platform for a resource pattern with no {platform} token; mutually exclusive with options",
273
+ "minLength": 1
274
+ },
275
+ "configuration": {
276
+ "type": "array",
277
+ "description": "Values resolved when this platform matches",
278
+ "items": {
279
+ "type": "object",
280
+ "required": ["name", "value"],
281
+ "additionalProperties": false,
282
+ "properties": {
283
+ "name": {
284
+ "type": "string",
285
+ "description": "Configuration variable name",
286
+ "pattern": "^[A-Z_][A-Z0-9_]*$"
287
+ },
288
+ "value": {
289
+ "type": "string",
290
+ "description": "Literal, or a value containing {platform}, {platform.name} or {resourceGroup} tokens"
291
+ },
292
+ "location": {
293
+ "type": "string",
294
+ "description": "Where the value is stored",
295
+ "enum": ["variable", "keyvault"]
296
+ },
297
+ "required": {
298
+ "type": "boolean",
299
+ "description": "Whether this configuration is required"
300
+ },
301
+ "category": {
302
+ "type": "string",
303
+ "description": "Optional UI grouping label for portal/deployment wizard Configuration tab",
304
+ "minLength": 1,
305
+ "maxLength": 100
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ }
312
+ },
313
+ "environments": {
314
+ "type": "array",
315
+ "description": "Per-environment values layered over the platform match, overriding by name. Optional and independent of platforms.",
316
+ "items": {
317
+ "type": "object",
318
+ "required": ["environment", "configuration"],
319
+ "additionalProperties": false,
320
+ "properties": {
321
+ "environment": {
322
+ "type": "string",
323
+ "description": "Deployment environment key (for example miso, dev, tst, pro)",
324
+ "minLength": 1
325
+ },
326
+ "configuration": {
327
+ "type": "array",
328
+ "description": "Values resolved for this environment",
329
+ "items": {
330
+ "type": "object",
331
+ "required": ["name", "value"],
332
+ "additionalProperties": false,
333
+ "properties": {
334
+ "name": {
335
+ "type": "string",
336
+ "description": "Configuration variable name",
337
+ "pattern": "^[A-Z_][A-Z0-9_]*$"
338
+ },
339
+ "value": {
340
+ "type": "string",
341
+ "description": "Literal, or a value containing {platform}, {platform.name} or {resourceGroup} tokens"
342
+ },
343
+ "location": {
344
+ "type": "string",
345
+ "description": "Where the value is stored",
346
+ "enum": ["variable", "keyvault"]
347
+ },
348
+ "required": {
349
+ "type": "boolean",
350
+ "description": "Whether this configuration is required"
351
+ },
352
+ "category": {
353
+ "type": "string",
354
+ "description": "Optional UI grouping label for portal/deployment wizard Configuration tab",
355
+ "minLength": 1,
356
+ "maxLength": 100
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ },
213
364
  "configuration": {
214
365
  "type": "array",
215
366
  "description": "Core application configuration",
@@ -274,6 +425,10 @@
274
425
  "type": "boolean",
275
426
  "description": "Whether to mask the input (for passwords)"
276
427
  },
428
+ "visible": {
429
+ "type": "boolean",
430
+ "description": "Whether the portal shows this input. Default true; false keeps a resolved or derived value out of the form."
431
+ },
277
432
  "validation": {
278
433
  "type": "object",
279
434
  "properties": {
@@ -413,6 +568,10 @@
413
568
  "type": "boolean",
414
569
  "description": "Whether to mask the input (for passwords)"
415
570
  },
571
+ "visible": {
572
+ "type": "boolean",
573
+ "description": "Whether the portal shows this input. Default true; false keeps a resolved or derived value out of the form."
574
+ },
416
575
  "validation": {
417
576
  "type": "object",
418
577
  "properties": {
@@ -806,6 +965,43 @@
806
965
  "minLength": 1,
807
966
  "maxLength": 200
808
967
  },
968
+ "additionalPorts": {
969
+ "type": "array",
970
+ "description": "Extra container ports to publish beside the main port. For an application that serves a second protocol from the same container, such as SSH alongside HTTP. Each host port is derived the same way as the main one: hostPort + developerId * 100 (developer id 0 keeps hostPort).",
971
+ "maxItems": 5,
972
+ "items": {
973
+ "type": "object",
974
+ "required": ["name", "containerPort", "hostPort"],
975
+ "additionalProperties": false,
976
+ "properties": {
977
+ "name": {
978
+ "type": "string",
979
+ "description": "What the port is for, e.g. 'ssh'. Used in generated compose comments and diagnostics.",
980
+ "pattern": "^[a-z][a-z0-9-]*$",
981
+ "minLength": 1,
982
+ "maxLength": 30
983
+ },
984
+ "containerPort": {
985
+ "type": "integer",
986
+ "description": "Port the process listens on inside the container.",
987
+ "minimum": 1,
988
+ "maximum": 65535
989
+ },
990
+ "hostPort": {
991
+ "type": "integer",
992
+ "description": "Base host port. The published port is hostPort + developerId * 100, so developers on one host do not collide. A base below 1024 is allowed so a well-known port can keep its number (SSH as 22, giving 122 for developer 1); the container engine binds it, not the application.",
993
+ "minimum": 1,
994
+ "maximum": 65535
995
+ }
996
+ }
997
+ }
998
+ },
999
+ "runAsRoot": {
1000
+ "type": "boolean",
1001
+ "default": false,
1002
+ "description": "Run the container as the image user (root) instead of the invoking host user. Only for an application that administers its own operating system, such as creating Linux accounts or running sshd; files it writes to a bind mount will be root-owned. Absent or false keeps the existing behavior.",
1003
+ "examples": [false]
1004
+ },
809
1005
  "runtimeVersion": {
810
1006
  "type": "object",
811
1007
  "description": "Runtime version configuration",
@@ -886,6 +1082,13 @@
886
1082
  "description": "Dockerfile name (empty or missing = use auto-generated template)",
887
1083
  "pattern": "^[^/].*"
888
1084
  },
1085
+ "target": {
1086
+ "type": "string",
1087
+ "description": "Multi-stage Dockerfile stage to build (docker build --target). Omit to build the final stage, which is the default. Lets one Dockerfile and one source tree produce more than one image, e.g. an application shipped in two installation variants.",
1088
+ "minLength": 1,
1089
+ "maxLength": 100,
1090
+ "pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
1091
+ },
889
1092
  "remoteSyncPath": {
890
1093
  "type": "string",
891
1094
  "description": "Relative path under user-mutagen-folder for remote sync and Docker -v; when unset, defaults to dev/<appKey>. No leading slash.",
@@ -7,7 +7,7 @@
7
7
  "key":"external-datasource-schema",
8
8
  "name":"External Data Source Configuration Schema",
9
9
  "description":"JSON schema for validating ExternalDataSource configuration files",
10
- "version":"2.9.12",
10
+ "version":"2.9.13",
11
11
  "type":"schema",
12
12
  "category":"integration",
13
13
  "author":"AI Fabrix Team",
@@ -31,6 +31,15 @@
31
31
  "external-system.schema.json"
32
32
  ],
33
33
  "changelog":[
34
+ {
35
+ "version":"2.9.13",
36
+ "date":"2026-09-24T00:00:00Z",
37
+ "changes":[
38
+ "Added fetch.responseType ('json' | 'binary', default json): a file body becomes one binary record with headers and request arguments exposed to fieldMappings while bytes stay out of traces.",
39
+ "Added output.mode 'documents' for entityType documentStorage: binary records are persisted through the document pipeline and emitted as document references; documentStorage.binaryOperationRef may name the datasource's own get for keyed retrieval."
40
+ ],
41
+ "breaking":false
42
+ },
34
43
  {
35
44
  "version":"2.9.12",
36
45
  "date":"2026-09-17T00:00:00Z",
@@ -2739,6 +2748,15 @@
2739
2748
  },
2740
2749
  "httpResponseNormalization":{
2741
2750
  "$ref":"#/$defs/httpResponseNormalization"
2751
+ },
2752
+ "responseType":{
2753
+ "type":"string",
2754
+ "enum":[
2755
+ "json",
2756
+ "binary"
2757
+ ],
2758
+ "default":"json",
2759
+ "description":"Body contract of the vendor response. 'binary' turns a file body (application/octet-stream, PDF, image ...) into one binary record whose bytes stay out of traces: {_binary:{contentType,size,filename,sha256}, headers:{...}, params:{...}} for map expressions such as raw.headers.AttachmentName or raw.params.PurchaseOrderNumber. A binary Content-Type is promoted automatically when responseType is omitted; declaring 'binary' fails closed when the body is JSON."
2742
2760
  }
2743
2761
  },
2744
2762
  "allOf":[
@@ -3174,9 +3192,11 @@
3174
3192
  "mode":{
3175
3193
  "type":"string",
3176
3194
  "enum":[
3177
- "records"
3195
+ "records",
3196
+ "documents"
3178
3197
  ],
3179
- "default":"records"
3198
+ "default":"records",
3199
+ "description":"'records' emits normalized records. 'documents' (entityType documentStorage only) stores each binary record through the document pipeline (document store, DocumentRecord/registry rows, FK dimensions, ingestion per documentStorage.ingestAfterSync) and emits document references {key, filename, contentType, size, sha256, downloadRef}; IDE simulation never persists."
3180
3200
  },
3181
3201
  "limit":{
3182
3202
  "type":"integer",