@aifabrix/builder 2.58.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/git-identity.js +180 -0
  3. package/lib/agent-kit/init.js +4 -17
  4. package/lib/agent-kit/session-report.js +15 -10
  5. package/lib/agent-kit/setup-auth.js +131 -0
  6. package/lib/agent-kit/setup-context.js +118 -0
  7. package/lib/agent-kit/setup-github.js +70 -0
  8. package/lib/agent-kit/setup-managed.js +79 -0
  9. package/lib/agent-kit/setup-process.js +42 -0
  10. package/lib/agent-kit/setup-session.js +53 -0
  11. package/lib/agent-kit/setup-state.js +52 -0
  12. package/lib/agent-kit/setup-workspace.js +113 -0
  13. package/lib/agent-kit/setup.js +87 -0
  14. package/lib/agent-kit/start.js +48 -3
  15. package/lib/agent-kit/tmux-sessions.js +20 -3
  16. package/lib/api/dev.api.js +1 -1
  17. package/lib/api/miso-health.api.js +26 -0
  18. package/lib/api/role-assistant-test-job.api.js +60 -0
  19. package/lib/api/types/dev.types.js +3 -3
  20. package/lib/api/work-search.api.js +13 -6
  21. package/lib/app/deploy.js +8 -103
  22. package/lib/app/show-display.js +1 -0
  23. package/lib/app/show-online.js +15 -0
  24. package/lib/build/standard-docker-build.js +4 -1
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +14 -1
  27. package/lib/cli/setup-dev-path-commands.js +4 -0
  28. package/lib/cli/setup-dev.js +7 -4
  29. package/lib/cli/setup-help.js +86 -0
  30. package/lib/cli/setup-onboarding.js +37 -0
  31. package/lib/cli/setup-utility-repair.js +7 -5
  32. package/lib/cli/setup-utility.js +24 -1
  33. package/lib/commands/agent-kit-help.js +21 -7
  34. package/lib/commands/agent-kit-sessions.js +126 -0
  35. package/lib/commands/agent-kit-setup.js +44 -0
  36. package/lib/commands/agent-kit.js +5 -79
  37. package/lib/commands/dev-init.js +21 -23
  38. package/lib/commands/dev-refresh.js +2 -2
  39. package/lib/commands/governance-verify-external.js +12 -5
  40. package/lib/commands/repair-datasource.js +9 -9
  41. package/lib/commands/repair.js +45 -8
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/upload-qualification-client.js +56 -11
  44. package/lib/commands/upload-qualification-poll.js +51 -22
  45. package/lib/commands/wizard-dataplane.js +16 -52
  46. package/lib/core/env-platform-expand.js +93 -0
  47. package/lib/core/secrets-env-content.js +37 -3
  48. package/lib/datasource/datasource-validate-summary.js +8 -5
  49. package/lib/datasource/sync-defaults.js +53 -0
  50. package/lib/deployment/installation/azure-infra-stage.js +13 -1
  51. package/lib/deployment/installation/azure-preflight.js +13 -2
  52. package/lib/deployment/installation/azure-readiness.js +24 -2
  53. package/lib/deployment/installation/environment-auth.js +60 -20
  54. package/lib/deployment/installation/environment-stage.js +9 -5
  55. package/lib/deployment/installation/infra-catalog.js +10 -7
  56. package/lib/deployment/installation/infra-manifest.js +5 -0
  57. package/lib/deployment/installation/registry-auth-mode.js +33 -10
  58. package/lib/deployment/marketplace-environment.js +26 -1
  59. package/lib/generator/builders.js +17 -0
  60. package/lib/generator/helpers.js +23 -2
  61. package/lib/generator/index.js +33 -18
  62. package/lib/integration-definition/apply.js +2 -2
  63. package/lib/internal/fs-real-sync.js +4 -1
  64. package/lib/programmatic/bearer-auth.js +3 -62
  65. package/lib/programmatic/builder-api-startup.js +51 -0
  66. package/lib/programmatic/builder-miso-runtime.js +167 -0
  67. package/lib/programmatic/openapi-descriptions.js +3 -3
  68. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  69. package/lib/programmatic/openapi-spec.js +2 -1
  70. package/lib/programmatic/route-handler-map.js +3 -2
  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/schema/application-schema.json +193 -2
  75. package/lib/schema/environment-deploy-request.schema.json +1 -1
  76. package/lib/schema/external-datasource.schema.json +20 -5
  77. package/lib/schema/infra-parameter.schema.json +140 -34
  78. package/lib/schema/infra.parameter.yaml +30 -29
  79. package/lib/schema/infrastructure-schema.json +133 -14
  80. package/lib/schema/installation-environment.schema.json +1 -1
  81. package/lib/schema/wizard-config.schema.json +1 -1
  82. package/lib/utils/compose-generate-docker-compose.js +12 -0
  83. package/lib/utils/config-paths.js +24 -29
  84. package/lib/utils/config-registry-preference.js +13 -6
  85. package/lib/utils/dev-init-ssh-merge.js +5 -4
  86. package/lib/utils/dev-user-groups.js +2 -1
  87. package/lib/utils/docker-build.js +29 -8
  88. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  89. package/lib/utils/docker-manifest-public-port.js +36 -0
  90. package/lib/utils/error-formatters/validation-errors.js +25 -5
  91. package/lib/utils/help-builder.js +4 -3
  92. package/lib/utils/platform-resolution.js +226 -0
  93. package/lib/utils/resolve-docker-image-ref.js +8 -3
  94. package/lib/utils/token-manager.js +29 -36
  95. package/package.json +4 -3
  96. package/templates/README.md +2 -1
  97. package/templates/agent-kit/agent-kit.yaml +4 -1
  98. package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
  99. package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
  100. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
  101. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  102. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
  103. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
  104. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
  105. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
  106. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  107. package/templates/agent-kit/skills/shared/hosts.md +35 -7
  108. package/templates/agent-kit/skills/shared/status.md +85 -0
  109. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  110. package/templates/agent-kit/workspace/README.md +14 -1
  111. package/templates/applications/builder-api/application.yaml +1 -1
  112. package/templates/applications/dataplane/application.yaml +30 -2
  113. package/templates/applications/dataplane/env.template +37 -4
  114. package/templates/applications/miso-controller/application.yaml +24 -1
  115. package/templates/applications/miso-controller/env.template +22 -35
  116. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  117. package/templates/marketplace/main.json +281 -75
  118. package/templates/python/Dockerfile.hbs +2 -0
  119. package/templates/typescript/Dockerfile.hbs +2 -0
  120. package/templates/typescript/docker-compose.hbs +12 -0
@@ -8,8 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- /** @type {import('@aifabrix/miso-client').MisoClient | null} */
12
- let misoClientSingleton = null;
11
+ const { getBuilderMisoRuntime } = require('./builder-miso-runtime');
13
12
 
14
13
  /**
15
14
  * @param {import('http').IncomingMessage} req
@@ -36,65 +35,10 @@ function assertNoForbiddenAuthHeaders(req) {
36
35
  }
37
36
 
38
37
  /**
39
- * @returns {string}
40
- */
41
- function resolveControllerUrlFromEnv() {
42
- const raw =
43
- process.env.MISO_CONTROLLER_URL ||
44
- process.env.CONTROLLER_URL ||
45
- process.env.MISO_WEB_SERVER_URL ||
46
- '';
47
- const url = String(raw).trim().replace(/\/$/, '');
48
- if (!url) {
49
- throw new Error('MISO_CONTROLLER_URL (or CONTROLLER_URL) is required for token validation');
50
- }
51
- return url;
52
- }
53
-
54
- /**
55
- * @returns {import('@aifabrix/miso-client/dist/types/config.types').MisoClientConfig}
56
- */
57
- function buildMisoClientConfigFromEnv() {
58
- const clientId = (process.env.MISO_CLIENTID || '').trim();
59
- const clientSecret = (process.env.MISO_CLIENTSECRET || '').trim();
60
- if (!clientId || !clientSecret) {
61
- throw new Error('MISO_CLIENTID and MISO_CLIENTSECRET are required for builder-api auth');
62
- }
63
- return {
64
- clientId,
65
- clientSecret,
66
- controllerUrl: resolveControllerUrlFromEnv(),
67
- logLevel: (process.env.LOG_LEVEL || 'info').toLowerCase()
68
- };
69
- }
70
-
71
- /**
72
- * Test hook — reset cached MisoClient between Jest cases.
73
- */
74
- function resetMisoClientForTests() {
75
- misoClientSingleton = null;
76
- }
77
-
78
- /**
79
- * @returns {Promise<import('@aifabrix/miso-client').MisoClient>}
38
+ * @returns {Promise<import('@aifabrix/miso-client').MisoClient>} Initialized shared client
80
39
  */
81
40
  async function getMisoClientForAuth() {
82
- let MisoClient;
83
- try {
84
- ({ MisoClient } = require('@aifabrix/miso-client'));
85
- } catch {
86
- const err = new Error('@aifabrix/miso-client is required for builder-api auth');
87
- err.statusCode = 503;
88
- throw err;
89
- }
90
-
91
- if (!misoClientSingleton) {
92
- misoClientSingleton = new MisoClient(buildMisoClientConfigFromEnv());
93
- }
94
- if (!misoClientSingleton.isInitialized()) {
95
- await misoClientSingleton.initialize();
96
- }
97
- return misoClientSingleton;
41
+ return getBuilderMisoRuntime().getClient();
98
42
  }
99
43
 
100
44
  /**
@@ -167,9 +111,6 @@ async function authenticateRequest(req, opts = {}) {
167
111
  module.exports = {
168
112
  extractBearerToken,
169
113
  assertNoForbiddenAuthHeaders,
170
- resolveControllerUrlFromEnv,
171
- buildMisoClientConfigFromEnv,
172
- resetMisoClientForTests,
173
114
  getMisoClientForAuth,
174
115
  validateBearerToken,
175
116
  authenticateRequest
@@ -0,0 +1,51 @@
1
+ /** @fileoverview Await SDK bootstrap and token proof before accepting requests. */
2
+ 'use strict';
3
+
4
+ const { initializeBuilderMisoRuntime, closeBuilderMisoRuntime } = require('./builder-miso-runtime');
5
+ const { ensureBuilderApiRuntimeConfigDir } = require('./builder-api-runtime-config');
6
+
7
+ /** @param {Object} server HTTP server @returns {Promise<void>} Listener closed */
8
+ function closeServer(server) {
9
+ return new Promise(resolve => {
10
+ server.close(resolve);
11
+ server.closeAllConnections();
12
+ });
13
+ }
14
+
15
+ /**
16
+ * @param {Object} [options] Startup dependencies
17
+ * @returns {Promise<{server: Object, close: Function}>} Running application
18
+ */
19
+ async function startBuilderApi(options = {}) {
20
+ ensureBuilderApiRuntimeConfigDir();
21
+ let server;
22
+ let closing;
23
+ const close = () => {
24
+ if (!closing) {
25
+ closing = Promise.all([server ? closeServer(server) : undefined, closeBuilderMisoRuntime()]);
26
+ }
27
+ return closing;
28
+ };
29
+ try {
30
+ const runtime = await initializeBuilderMisoRuntime(options.initSecrets);
31
+ await runtime.health();
32
+ const createServer = options.createServer || require('./builder-api-server').createBuilderApiServer;
33
+ server = createServer(options.port ?? Number(process.env.PORT || 3005));
34
+ await new Promise((resolve, reject) => {
35
+ server.once('error', reject);
36
+ server.once('listening', () => {
37
+ server.removeListener('error', reject); resolve();
38
+ });
39
+ });
40
+ return { server, close };
41
+ } catch (error) {
42
+ await close().catch(() => {});
43
+ // Only our allowlisted health errors can cross the process boundary.
44
+ const failure = new Error('Builder API startup failed: Miso application runtime or authentication unavailable');
45
+ failure.statusCode = 503;
46
+ if (error?.statusCode === 503 && error?.errorType?.startsWith('/Errors/miso_')) throw error;
47
+ throw failure;
48
+ }
49
+ }
50
+
51
+ module.exports = { startBuilderApi };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Builder's process-owned SDK runtime and application-token health check.
3
+ * @fileoverview SDK owns providers, secrets, renewal and connections.
4
+ */
5
+ 'use strict';
6
+
7
+ const { validateMisoApplicationToken } = require('../api/miso-health.api');
8
+ let activeRuntime;
9
+
10
+ /** @param {string} code Safe category @returns {Error} Service-unavailable error */
11
+ function runtimeError(code) {
12
+ const messages = {
13
+ miso_authentication_failed: 'Miso application authentication failed',
14
+ miso_timeout: 'Miso application token check timed out',
15
+ miso_unreachable: 'Miso Controller is unavailable',
16
+ miso_runtime_unavailable: 'Miso application runtime is unavailable'
17
+ };
18
+ return Object.assign(new Error(messages[code]), {
19
+ statusCode: 503, code, errorType: `/Errors/${code}`
20
+ });
21
+ }
22
+
23
+ /** @param {unknown} error SDK error @returns {string} Safe category */
24
+ function failureCode(error) {
25
+ const status = error?.statusCode || error?.errorResponse?.status || error?.response?.status;
26
+ if ([401, 403].includes(status) || error?.code === 'authorization-denied') {
27
+ return 'miso_authentication_failed';
28
+ }
29
+ if (['ECONNABORTED', 'ETIMEDOUT', 'ERR_CANCELED'].includes(error?.code)) {
30
+ return 'miso_timeout';
31
+ }
32
+ return 'miso_unreachable';
33
+ }
34
+
35
+ /** SDK resource ownership and fail-closed application state. */
36
+ class RuntimeController {
37
+ /** @param {Object} runtime SDK runtime */
38
+ constructor(runtime) {
39
+ this.runtime = runtime;
40
+ this.unsubscribe = runtime.onInvalidated(reason => {
41
+ this.unavailable = reason === 'authorization-denied'
42
+ ? 'miso_authentication_failed' : 'miso_runtime_unavailable';
43
+ this.abort?.abort();
44
+ });
45
+ }
46
+
47
+ /** @returns {void} Throws when the SDK snapshot is unavailable */
48
+ assertAvailable() {
49
+ if (this.unavailable) throw runtimeError(this.unavailable);
50
+ try {
51
+ this.runtime.secrets.get('');
52
+ } catch {
53
+ throw runtimeError(this.unavailable || 'miso_runtime_unavailable');
54
+ }
55
+ }
56
+
57
+ /** @returns {Object} Initialized client */
58
+ getClient() {
59
+ this.assertAvailable();
60
+ return this.runtime.client;
61
+ }
62
+
63
+ /** @returns {Promise<void>} Idempotent cleanup */
64
+ close() {
65
+ if (!this.closing) {
66
+ this.unavailable = this.unavailable || 'miso_runtime_unavailable';
67
+ this.abort?.abort();
68
+ this.unsubscribe();
69
+ this.closing = Promise.resolve().then(() => this.runtime.close()).catch(() => {
70
+ throw runtimeError('miso_runtime_unavailable');
71
+ });
72
+ }
73
+ return this.closing;
74
+ }
75
+
76
+ /** @returns {Promise<void>} Fresh, coalesced token proof; never caches success */
77
+ health() {
78
+ this.assertAvailable();
79
+ if (!this.pending) {
80
+ this.abort = new AbortController();
81
+ this.pending = runProbe(this.runtime.client, this.abort,
82
+ () => this.assertAvailable(), code => {
83
+ this.unavailable = code;
84
+ void this.close().catch(() => {});
85
+ }, () => {
86
+ this.pending = undefined;
87
+ });
88
+ }
89
+ return this.pending;
90
+ }
91
+ }
92
+
93
+ /** @param {Object} runtime SDK runtime @returns {RuntimeController} Consumer controls */
94
+ function createRuntimeController(runtime) {
95
+ return new RuntimeController(runtime);
96
+ }
97
+
98
+ /**
99
+ * Coalesce until transport settles, including SDK-owned token acquisition after a deadline.
100
+ * @param {Object} client SDK client
101
+ * @param {AbortController} abort Cancellation
102
+ * @param {Function} assertAvailable Runtime guard
103
+ * @param {Function} invalidate Terminal rejection
104
+ * @param {Function} settled Release in-flight work
105
+ * @returns {Promise<void>} Bounded health result
106
+ */
107
+ function runProbe(client, abort, assertAvailable, invalidate, settled) {
108
+ let timer;
109
+ const deadline = new Promise((_, reject) => {
110
+ timer = setTimeout(() => {
111
+ abort.abort(); reject(runtimeError('miso_timeout'));
112
+ }, 3000);
113
+ });
114
+ const request = validateMisoApplicationToken(client, abort.signal).then(() => {
115
+ assertAvailable();
116
+ }).catch(error => {
117
+ let code = failureCode(error);
118
+ try {
119
+ assertAvailable();
120
+ } catch (unavailable) {
121
+ code = unavailable.code;
122
+ }
123
+ if (code === 'miso_authentication_failed') invalidate(code);
124
+ throw runtimeError(code);
125
+ }).finally(() => {
126
+ clearTimeout(timer); settled();
127
+ });
128
+ return Promise.race([request, deadline]);
129
+ }
130
+
131
+ /** @param {Function} [init] SDK factory for tests @returns {Promise<Object>} Runtime controls */
132
+ async function initializeBuilderMisoRuntime(init) {
133
+ if (activeRuntime) throw runtimeError('miso_runtime_unavailable');
134
+ const factory = init || require('@aifabrix/miso-client/bootstrap').initSecrets;
135
+ try {
136
+ activeRuntime = createRuntimeController(await factory());
137
+ return activeRuntime;
138
+ } catch {
139
+ throw runtimeError('miso_runtime_unavailable');
140
+ }
141
+ }
142
+
143
+ /** @returns {Object} Active guarded runtime */
144
+ function getBuilderMisoRuntime() {
145
+ if (!activeRuntime) throw runtimeError('miso_runtime_unavailable');
146
+ return activeRuntime;
147
+ }
148
+
149
+ /** @returns {Promise<void>} Close and release process-owned runtime */
150
+ async function closeBuilderMisoRuntime() {
151
+ const current = activeRuntime;
152
+ activeRuntime = undefined;
153
+ await current?.close();
154
+ }
155
+
156
+ /** @param {boolean} [ready] Readiness response @returns {Promise<Object>} Healthy payload */
157
+ async function checkBuilderMisoHealth(ready = false) {
158
+ await getBuilderMisoRuntime().health();
159
+ return ready
160
+ ? { status: 'ready', dataplaneUrl: process.env.DATAPLANE_URL || null }
161
+ : { status: 'ok' };
162
+ }
163
+
164
+ module.exports = {
165
+ initializeBuilderMisoRuntime, getBuilderMisoRuntime, closeBuilderMisoRuntime,
166
+ checkBuilderMisoHealth, createRuntimeController
167
+ };
@@ -213,14 +213,14 @@ const OPERATION_DESCRIPTIONS = Object.freeze({
213
213
  tags: ['repository']
214
214
  },
215
215
  health: {
216
- summary: 'Liveness probe',
216
+ summary: 'Application-token health probe',
217
217
  description:
218
- 'Returns ok when the builder-api process is running. Portal availability probe. Empty channel catalog is not a down signal. No authentication.',
218
+ 'Returns ok only after Miso accepts the application client token. Returns 503 when authentication, runtime validity or connectivity fails. No caller authentication; user credentials cannot substitute for the application token.',
219
219
  tags: ['infra']
220
220
  },
221
221
  ready: {
222
222
  summary: 'Readiness probe',
223
- description: 'Returns ready with configured dataplaneUrl. Does not verify Dataplane connectivity in V1.',
223
+ description: 'Verifies the application client token with Miso and returns ready with configured dataplaneUrl. Returns 503 for an unusable token or unavailable Miso runtime. Does not verify Dataplane connectivity.',
224
224
  tags: ['infra']
225
225
  },
226
226
  getBuilderHelp: {
@@ -113,11 +113,11 @@ const CORE_SCHEMA_DESCRIPTIONS = {
113
113
  deleted: 'Always true when the workspace folder was removed.'
114
114
  },
115
115
  HealthData: {
116
- _schema: 'Liveness probe payload when the builder-api process is running.',
117
- status: 'Always ok for successful liveness checks.'
116
+ _schema: 'Health payload after Miso accepts the application client token.',
117
+ status: 'Always ok after successful application-token validation.'
118
118
  },
119
119
  ReadyData: {
120
- _schema: 'Readiness probe payload; does not verify Dataplane connectivity in V1.',
120
+ _schema: 'Readiness after Miso application-token validation; does not verify Dataplane connectivity.',
121
121
  status: 'Always ready when the service accepts traffic.',
122
122
  dataplaneUrl: 'Configured dev Dataplane base URL, or null when unset.'
123
123
  },
@@ -68,7 +68,8 @@ function buildStandardErrorResponses() {
68
68
  408: problemResponseContent('Request exceeded capability timeout.'),
69
69
  409: problemResponseContent('Workspace lock held by another in-flight operation.'),
70
70
  413: problemResponseContent('Request body exceeds MAX_BODY_BYTES.'),
71
- 500: problemResponseContent('Unexpected server error.')
71
+ 500: problemResponseContent('Unexpected server error.'),
72
+ 503: problemResponseContent('Miso application authentication failed, token check timed out, or runtime/Controller unavailable. Safe failure category appears in type.')
72
73
  };
73
74
  }
74
75
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { checkBuilderMisoHealth } = require('./builder-miso-runtime');
11
12
  const { buildContext } = require('./route-handlers-context');
12
13
  const { listSystems, createSystem, getSystemManifest, deleteSystem } = require('./workspace-ops');
13
14
  const { persistSuppliedOpenApi } = require('./persist-supplied-openapi');
@@ -230,8 +231,8 @@ const HANDLER_MAP = {
230
231
  getRepositoryStatus: (params, auth) => getRepositoryStatus(buildContext(auth, params.systemKey)),
231
232
  runRepositoryDispatchPipeline: (params, auth, body) =>
232
233
  runRepositoryDispatchPipeline(buildContext(auth, params.systemKey), body),
233
- health: () => ({ status: 'ok' }),
234
- ready: () => ({ status: 'ready', dataplaneUrl: process.env.DATAPLANE_URL || null }),
234
+ health: () => checkBuilderMisoHealth(),
235
+ ready: () => checkBuilderMisoHealth(true),
235
236
  getBuilderHelp: (_params, _auth, _body, query) => handleGetBuilderHelp(query || {}),
236
237
  getEvidenceMetadata: (params, auth) =>
237
238
  getEvidenceMetadata(buildContext(auth, '_evidence'), params.roleKey),
@@ -142,6 +142,9 @@ function evaluateSearchExpectations(expectedSearch, observation) {
142
142
 
143
143
  function skillsResourceEntry(skillsPayload, resourceType) {
144
144
  const root = skillsEnvelope(skillsPayload) || asObject(skillsPayload);
145
+ if (String(root.resourceType || '').trim() === resourceType) {
146
+ return root;
147
+ }
145
148
  const rts = asObject(asObject(root).search).resourceTypes;
146
149
  return asObject(asObject(rts)[resourceType]);
147
150
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Run one published Role Assistant case as an explicitly selected subject.
3
+ *
4
+ * @fileoverview Async test-job isolation for the Role Assistant CLI
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { resolveControllerUrl } = require('../utils/controller-url');
10
+ const { resolveUserIdOrEmail } = require('../identity/resolve-user-id');
11
+ const {
12
+ enqueueRoleAssistantTestJob,
13
+ getRoleAssistantTestJob
14
+ } = require('../api/role-assistant-test-job.api');
15
+ const { roleAssistantSystemKey } = require('../utils/role-assistant-paths');
16
+
17
+ function selectedSubject(opts) {
18
+ const ids = Array.isArray(opts.subjectUserId)
19
+ ? opts.subjectUserId.map((value) => String(value || '').trim()).filter(Boolean)
20
+ : [];
21
+ const emails = Array.isArray(opts.subjectEmail)
22
+ ? opts.subjectEmail.map((value) => String(value || '').trim()).filter(Boolean)
23
+ : [];
24
+ if (ids.length + emails.length > 1) {
25
+ throw new Error('Role Assistant case execution accepts exactly one isolation subject.');
26
+ }
27
+ return { userId: ids[0] || '', email: emails[0] || '' };
28
+ }
29
+
30
+ function hasRoleAssistantTestSubject(opts) {
31
+ const subject = selectedSubject(opts);
32
+ return Boolean(subject.userId || subject.email);
33
+ }
34
+
35
+ async function resolveSubjectUserId(opts, authConfig) {
36
+ const subject = selectedSubject(opts);
37
+ if (subject.userId) {
38
+ return subject.userId;
39
+ }
40
+ const controllerUrl = await resolveControllerUrl();
41
+ return resolveUserIdOrEmail(controllerUrl, authConfig, subject.email);
42
+ }
43
+
44
+ function waitFor(milliseconds) {
45
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
46
+ }
47
+
48
+ async function pollJob(roleKey, authContext, jobId, opts) {
49
+ const timeoutMs = Number(opts.pollTimeoutMs) || 180000;
50
+ const intervalMs = Number(opts.pollIntervalMs) || 750;
51
+ const startedAt = Date.now();
52
+ let job = null;
53
+ while (Date.now() - startedAt < timeoutMs) {
54
+ job = await getRoleAssistantTestJob(
55
+ authContext.dataplaneUrl,
56
+ authContext.authConfig,
57
+ roleAssistantSystemKey(roleKey),
58
+ jobId
59
+ );
60
+ const status = String(job?.status || '').trim().toLowerCase();
61
+ if (['completed', 'failed', 'cancelled'].includes(status)) {
62
+ return job;
63
+ }
64
+ await waitFor(intervalMs);
65
+ }
66
+ throw new Error(`Role Assistant test job ${jobId} did not finish within ${timeoutMs} ms.`);
67
+ }
68
+
69
+ function evaluationFromJobCase(roleCase, job, row, fallbackJobId = null) {
70
+ const assertions = Array.isArray(row?.assertions)
71
+ ? row.assertions.map((assertion) => ({
72
+ ...assertion,
73
+ passed: String(assertion?.kind || '').toLowerCase() !== 'fail'
74
+ }))
75
+ : [];
76
+ return {
77
+ caseId: row?.caseId || roleCase.caseId,
78
+ sourceFile: roleCase.sourceFile || null,
79
+ passed: row?.passed === true,
80
+ lifecycleState: row?.lifecycleState || String(job?.status || 'failed'),
81
+ assertions,
82
+ failedAssertions: assertions.filter((assertion) => assertion.passed === false),
83
+ warnings: assertions.filter((assertion) => assertion.kind === 'warn'),
84
+ execution: {
85
+ processExecutionId: row?.processExecutionId || null,
86
+ finalStatus: row?.lifecycleState || job?.status || null,
87
+ waitReason: null,
88
+ interactions: [],
89
+ workResultError: job?.errorMessage || null,
90
+ testJobId: job?.jobId || job?.id || fallbackJobId || null
91
+ }
92
+ };
93
+ }
94
+
95
+ async function runPublishedCaseAsSubject(roleKey, roleCase, opts, authContext) {
96
+ const subjectUserId = await resolveSubjectUserId(opts, authContext.authConfig);
97
+ const accepted = await enqueueRoleAssistantTestJob(
98
+ authContext.dataplaneUrl,
99
+ authContext.authConfig,
100
+ roleAssistantSystemKey(roleKey),
101
+ { caseId: roleCase.caseId, subjectUserId }
102
+ );
103
+ const jobId = String(accepted?.jobId || '').trim();
104
+ if (!jobId) {
105
+ throw new Error('Role Assistant test job enqueue did not return jobId.');
106
+ }
107
+ const job = await pollJob(roleKey, authContext, jobId, opts);
108
+ const row = Array.isArray(job?.cases)
109
+ ? job.cases.find((candidate) => candidate?.caseId === roleCase.caseId)
110
+ : null;
111
+ if (!row) {
112
+ throw new Error(
113
+ job?.errorMessage ||
114
+ `Published Role Assistant case '${roleCase.caseId}' returned no result in job ${jobId}.`
115
+ );
116
+ }
117
+ return evaluationFromJobCase(roleCase, job, row, jobId);
118
+ }
119
+
120
+ module.exports = {
121
+ hasRoleAssistantTestSubject,
122
+ runPublishedCaseAsSubject,
123
+ evaluationFromJobCase,
124
+ selectedSubject
125
+ };
@@ -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,