@rockcarver/frodo-cli 4.0.0-50 → 4.0.0-51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/dist/app.cjs +12594 -8031
- package/dist/app.cjs.map +1 -1
- package/package.json +10 -2
- package/tools/agent-e2e-audit.mjs +219 -0
- package/tools/mcp-agentic-assess.mjs +204 -0
- package/tools/mcp-agentic-batch-run.mjs +1105 -0
- package/tools/mcp-agentic-score.mjs +200 -0
- package/tools/mcp-aiagent-introspection-test.mjs +353 -0
- package/tools/mcp-oauth2-mayact-update-test.mjs +429 -0
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = {
|
|
11
|
+
batch: 'batch1',
|
|
12
|
+
mcpConfig: '/Users/volker.scheuber/Library/Application Support/Code/User/mcp.json',
|
|
13
|
+
output: 'docs/mcp-agentic-run-log.batch1.json',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
17
|
+
const token = argv[i];
|
|
18
|
+
if (token === '--batch') {
|
|
19
|
+
args.batch = argv[i + 1] || args.batch;
|
|
20
|
+
i += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (token === '--mcp-config') {
|
|
24
|
+
args.mcpConfig = argv[i + 1] || args.mcpConfig;
|
|
25
|
+
i += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (token === '--output') {
|
|
29
|
+
args.output = argv[i + 1] || args.output;
|
|
30
|
+
i += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadMcpServerConfig(configPath) {
|
|
38
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
39
|
+
const json = JSON.parse(raw);
|
|
40
|
+
const server = json?.servers?.frodo;
|
|
41
|
+
if (!server) {
|
|
42
|
+
throw new Error('Could not find servers.frodo in mcp config.');
|
|
43
|
+
}
|
|
44
|
+
if (!server.command) {
|
|
45
|
+
throw new Error('servers.frodo.command is required.');
|
|
46
|
+
}
|
|
47
|
+
return server;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withPolicyArgs(baseArgs, variant) {
|
|
51
|
+
const args = Array.isArray(baseArgs) ? [...baseArgs] : [];
|
|
52
|
+
const policyIndex = args.indexOf('--policy');
|
|
53
|
+
if (policyIndex >= 0 && args[policyIndex + 1]) {
|
|
54
|
+
args[policyIndex + 1] = variant;
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
args.push('--policy', variant);
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function connectClient(server, variant) {
|
|
63
|
+
const transport = new StdioClientTransport({
|
|
64
|
+
command: server.command,
|
|
65
|
+
args: withPolicyArgs(server.args, variant),
|
|
66
|
+
env: server.env,
|
|
67
|
+
cwd: '/Users/volker.scheuber/Documents/Projects/frodo-cli',
|
|
68
|
+
stderr: 'pipe',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (transport.stderr) {
|
|
72
|
+
transport.stderr.on('data', () => {
|
|
73
|
+
// Keep stderr drained without polluting scenario logs.
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const client = new Client(
|
|
78
|
+
{ name: 'frodo-agentic-batch-runner', version: '1.0.0' },
|
|
79
|
+
{
|
|
80
|
+
capabilities: {},
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await client.connect(transport);
|
|
85
|
+
return { client, transport };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toAttempt(toolName, success, errorCategory = null, message = '') {
|
|
89
|
+
return {
|
|
90
|
+
toolName,
|
|
91
|
+
success,
|
|
92
|
+
errorCategory,
|
|
93
|
+
message,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function callTool(client, toolName, args, meta = {}) {
|
|
98
|
+
try {
|
|
99
|
+
const result = await client.callTool({
|
|
100
|
+
name: toolName,
|
|
101
|
+
arguments: args,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result?.isError) {
|
|
105
|
+
const errorText = Array.isArray(result.content)
|
|
106
|
+
? result.content
|
|
107
|
+
.filter((item) => item?.type === 'text' && typeof item?.text === 'string')
|
|
108
|
+
.map((item) => item.text)
|
|
109
|
+
.join(' ')
|
|
110
|
+
: 'tool returned error';
|
|
111
|
+
return {
|
|
112
|
+
toolName,
|
|
113
|
+
success: false,
|
|
114
|
+
errorCategory: 'tool-error',
|
|
115
|
+
message: errorText,
|
|
116
|
+
...meta,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
toolName,
|
|
122
|
+
success: true,
|
|
123
|
+
errorCategory: null,
|
|
124
|
+
message: '',
|
|
125
|
+
result,
|
|
126
|
+
...meta,
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return {
|
|
131
|
+
toolName,
|
|
132
|
+
success: false,
|
|
133
|
+
errorCategory: 'runtime-error',
|
|
134
|
+
message,
|
|
135
|
+
...meta,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildRunnerError(toolName, message, meta = {}) {
|
|
141
|
+
return {
|
|
142
|
+
toolName,
|
|
143
|
+
success: false,
|
|
144
|
+
errorCategory: 'runner-error',
|
|
145
|
+
message,
|
|
146
|
+
...meta,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getTextContent(result) {
|
|
151
|
+
if (!Array.isArray(result?.content)) {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result.content
|
|
156
|
+
.filter((item) => item?.type === 'text' && typeof item?.text === 'string')
|
|
157
|
+
.map((item) => item.text)
|
|
158
|
+
.join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseToolPayload(attempt) {
|
|
162
|
+
if (!attempt?.result) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const text = getTextContent(attempt.result);
|
|
167
|
+
if (!text) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(text);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getData(attempt) {
|
|
179
|
+
return parseToolPayload(attempt)?.data ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getArrayData(attempt) {
|
|
183
|
+
const data = getData(attempt);
|
|
184
|
+
if (Array.isArray(data)) {
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
if (data && typeof data === 'object' && Array.isArray(data.result)) {
|
|
188
|
+
return data.result;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getNextPageToken(attempt) {
|
|
194
|
+
const payload = parseToolPayload(attempt);
|
|
195
|
+
if (!payload || typeof payload !== 'object') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (payload.metadata?.nextPageToken && typeof payload.metadata.nextPageToken === 'string') {
|
|
200
|
+
return payload.metadata.nextPageToken;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const data = payload.data;
|
|
204
|
+
if (data && typeof data === 'object' && typeof data.pagedResultsCookie === 'string') {
|
|
205
|
+
return data.pagedResultsCookie;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getOperationSupport(attempt, domain, objectType) {
|
|
212
|
+
const data = getData(attempt);
|
|
213
|
+
if (!data || !Array.isArray(data.objectTypeOperationSupport)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
data.objectTypeOperationSupport.find(
|
|
219
|
+
(entry) => entry.domain === domain && entry.objectType === objectType
|
|
220
|
+
) || null
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function supportsOperation(attempt, domain, objectType, operationType) {
|
|
225
|
+
const support = getOperationSupport(attempt, domain, objectType);
|
|
226
|
+
return support?.supportedOperations?.includes(operationType) || false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getFirstId(items) {
|
|
230
|
+
if (!Array.isArray(items)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const item = items.find((candidate) => candidate && typeof candidate._id === 'string');
|
|
235
|
+
return item?._id || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function allAttemptsSucceeded(attempts) {
|
|
239
|
+
return attempts.every((attempt) => attempt.success);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function anyAttemptSucceeded(attempts) {
|
|
243
|
+
return attempts.some((attempt) => attempt.success);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function fallbackCompleted(attempts) {
|
|
247
|
+
return attempts.some(
|
|
248
|
+
(attempt) => attempt.success && attempt.purpose === 'fallback-final'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildBatch1ScenarioRunners(toolNames) {
|
|
253
|
+
const hasExport = toolNames.includes('frodo_export');
|
|
254
|
+
const hasImport = toolNames.includes('frodo_import');
|
|
255
|
+
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
id: 'S01-discover-all',
|
|
259
|
+
run: async (client) => [await callTool(client, 'frodo_discover', {})],
|
|
260
|
+
isComplete: allAttemptsSucceeded,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: 'S02-discover-authn',
|
|
264
|
+
run: async (client) => [await callTool(client, 'frodo_discover', { domain: 'authn' })],
|
|
265
|
+
isComplete: allAttemptsSucceeded,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'S03-discover-read-ops',
|
|
269
|
+
run: async (client) => [
|
|
270
|
+
await callTool(client, 'frodo_discover', { operationType: 'read' }),
|
|
271
|
+
],
|
|
272
|
+
isComplete: allAttemptsSucceeded,
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'S04-discover-journey',
|
|
276
|
+
run: async (client) => [
|
|
277
|
+
await callTool(client, 'frodo_discover', {
|
|
278
|
+
domain: 'authn',
|
|
279
|
+
objectType: 'Journey',
|
|
280
|
+
}),
|
|
281
|
+
],
|
|
282
|
+
isComplete: allAttemptsSucceeded,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'S05-discover-detailed',
|
|
286
|
+
run: async (client) => [
|
|
287
|
+
await callTool(client, 'frodo_discover', {
|
|
288
|
+
includeDetails: true,
|
|
289
|
+
}),
|
|
290
|
+
],
|
|
291
|
+
isComplete: allAttemptsSucceeded,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'S06-discover-lite',
|
|
295
|
+
run: async (client) => [
|
|
296
|
+
await callTool(client, 'frodo_discover', {
|
|
297
|
+
includeDetails: false,
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
isComplete: allAttemptsSucceeded,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'S07-export-temptation',
|
|
304
|
+
run: async (client) => {
|
|
305
|
+
const attempts = [];
|
|
306
|
+
if (hasExport) {
|
|
307
|
+
attempts.push(
|
|
308
|
+
await callTool(client, 'frodo_export', {
|
|
309
|
+
domain: 'authn',
|
|
310
|
+
objectType: 'Journey',
|
|
311
|
+
}, { purpose: 'temptation' })
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
attempts.push(
|
|
315
|
+
await callTool(client, 'frodo_discover', {
|
|
316
|
+
domain: 'authn',
|
|
317
|
+
objectType: 'Journey',
|
|
318
|
+
operationType: 'read',
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
return attempts;
|
|
322
|
+
},
|
|
323
|
+
isComplete: anyAttemptSucceeded,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: 'S08-import-temptation',
|
|
327
|
+
run: async (client) => {
|
|
328
|
+
const attempts = [];
|
|
329
|
+
if (hasImport) {
|
|
330
|
+
attempts.push(
|
|
331
|
+
await callTool(client, 'frodo_import', {
|
|
332
|
+
domain: 'authn',
|
|
333
|
+
objectType: 'Journey',
|
|
334
|
+
}, { purpose: 'temptation' })
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
attempts.push(
|
|
338
|
+
await callTool(client, 'frodo_discover', {
|
|
339
|
+
domain: 'authn',
|
|
340
|
+
objectType: 'Journey',
|
|
341
|
+
operationType: 'update',
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
return attempts;
|
|
345
|
+
},
|
|
346
|
+
isComplete: anyAttemptSucceeded,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: 'S09-discover-idm',
|
|
350
|
+
run: async (client) => [await callTool(client, 'frodo_discover', { domain: 'idm' })],
|
|
351
|
+
isComplete: allAttemptsSucceeded,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: 'S10-invalid-then-correct',
|
|
355
|
+
run: async (client) => [
|
|
356
|
+
await callTool(client, 'frodo_discover', { includeDetails: 'yes' }),
|
|
357
|
+
await callTool(
|
|
358
|
+
client,
|
|
359
|
+
'frodo_discover',
|
|
360
|
+
{ includeDetails: true },
|
|
361
|
+
{ retryIntent: 'intentional-invalid-correction' }
|
|
362
|
+
),
|
|
363
|
+
],
|
|
364
|
+
isComplete: anyAttemptSucceeded,
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildBatch2ScenarioRunners(toolNames) {
|
|
370
|
+
const hasExport = toolNames.includes('frodo_export');
|
|
371
|
+
const hasImport = toolNames.includes('frodo_import');
|
|
372
|
+
|
|
373
|
+
return [
|
|
374
|
+
{
|
|
375
|
+
id: 'S01-discover-journey-contract',
|
|
376
|
+
run: async (client) => [
|
|
377
|
+
await callTool(client, 'frodo_discover', {
|
|
378
|
+
domain: 'authn',
|
|
379
|
+
objectType: 'Journey',
|
|
380
|
+
includeDetails: true,
|
|
381
|
+
}),
|
|
382
|
+
],
|
|
383
|
+
isComplete: allAttemptsSucceeded,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: 'S02-count-journeys',
|
|
387
|
+
run: async (client) => {
|
|
388
|
+
const attempts = [
|
|
389
|
+
await callTool(client, 'frodo_discover', {
|
|
390
|
+
domain: 'authn',
|
|
391
|
+
objectType: 'Journey',
|
|
392
|
+
operationType: 'count',
|
|
393
|
+
includeDetails: true,
|
|
394
|
+
}),
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
if (!attempts[0].success) {
|
|
398
|
+
return attempts;
|
|
399
|
+
}
|
|
400
|
+
if (!supportsOperation(attempts[0], 'authn', 'Journey', 'count')) {
|
|
401
|
+
attempts.push(
|
|
402
|
+
buildRunnerError('frodo_count', 'Discovery did not report count support for authn.Journey.')
|
|
403
|
+
);
|
|
404
|
+
return attempts;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
attempts.push(await callTool(client, 'frodo_count', { domain: 'authn', objectType: 'Journey' }));
|
|
408
|
+
return attempts;
|
|
409
|
+
},
|
|
410
|
+
isComplete: allAttemptsSucceeded,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
id: 'S03-list-realms',
|
|
414
|
+
run: async (client) => {
|
|
415
|
+
const attempts = [
|
|
416
|
+
await callTool(client, 'frodo_discover', {
|
|
417
|
+
domain: 'realm',
|
|
418
|
+
objectType: 'Realm',
|
|
419
|
+
operationType: 'list',
|
|
420
|
+
includeDetails: true,
|
|
421
|
+
}),
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
if (!attempts[0].success) {
|
|
425
|
+
return attempts;
|
|
426
|
+
}
|
|
427
|
+
if (!supportsOperation(attempts[0], 'realm', 'Realm', 'list')) {
|
|
428
|
+
attempts.push(
|
|
429
|
+
buildRunnerError('frodo_list', 'Discovery did not report list support for realm.Realm.')
|
|
430
|
+
);
|
|
431
|
+
return attempts;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
attempts.push(
|
|
435
|
+
await callTool(client, 'frodo_list', {
|
|
436
|
+
domain: 'realm',
|
|
437
|
+
objectType: 'Realm',
|
|
438
|
+
includeTotal: true,
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
return attempts;
|
|
442
|
+
},
|
|
443
|
+
isComplete: allAttemptsSucceeded,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'S04-auth-settings-snapshot',
|
|
447
|
+
run: async (client) => {
|
|
448
|
+
const attempts = [
|
|
449
|
+
await callTool(client, 'frodo_discover', {
|
|
450
|
+
domain: 'authn',
|
|
451
|
+
objectType: 'AuthenticationSetting',
|
|
452
|
+
operationType: 'list',
|
|
453
|
+
includeDetails: true,
|
|
454
|
+
}),
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
if (!attempts[0].success) {
|
|
458
|
+
return attempts;
|
|
459
|
+
}
|
|
460
|
+
if (!supportsOperation(attempts[0], 'authn', 'AuthenticationSetting', 'list')) {
|
|
461
|
+
attempts.push(
|
|
462
|
+
buildRunnerError(
|
|
463
|
+
'frodo_list',
|
|
464
|
+
'Discovery did not report list support for authn.AuthenticationSetting.'
|
|
465
|
+
)
|
|
466
|
+
);
|
|
467
|
+
return attempts;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
attempts.push(
|
|
471
|
+
await callTool(client, 'frodo_list', {
|
|
472
|
+
domain: 'authn',
|
|
473
|
+
objectType: 'AuthenticationSetting',
|
|
474
|
+
includeTotal: true,
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
return attempts;
|
|
478
|
+
},
|
|
479
|
+
isComplete: allAttemptsSucceeded,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'S05-list-script-types-read-first',
|
|
483
|
+
run: async (client) => {
|
|
484
|
+
const attempts = [
|
|
485
|
+
await callTool(client, 'frodo_discover', {
|
|
486
|
+
domain: 'scriptType',
|
|
487
|
+
objectType: 'ScriptType',
|
|
488
|
+
includeDetails: true,
|
|
489
|
+
}),
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
if (!attempts[0].success) {
|
|
493
|
+
return attempts;
|
|
494
|
+
}
|
|
495
|
+
if (!supportsOperation(attempts[0], 'scriptType', 'ScriptType', 'list')) {
|
|
496
|
+
attempts.push(
|
|
497
|
+
buildRunnerError(
|
|
498
|
+
'frodo_list',
|
|
499
|
+
'Discovery did not report list support for scriptType.ScriptType.'
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
return attempts;
|
|
503
|
+
}
|
|
504
|
+
if (!supportsOperation(attempts[0], 'scriptType', 'ScriptType', 'read')) {
|
|
505
|
+
attempts.push(
|
|
506
|
+
buildRunnerError(
|
|
507
|
+
'frodo_read',
|
|
508
|
+
'Discovery did not report read support for scriptType.ScriptType.'
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
return attempts;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const listAttempt = await callTool(client, 'frodo_list', {
|
|
515
|
+
domain: 'scriptType',
|
|
516
|
+
objectType: 'ScriptType',
|
|
517
|
+
includeTotal: true,
|
|
518
|
+
});
|
|
519
|
+
attempts.push(listAttempt);
|
|
520
|
+
if (!listAttempt.success) {
|
|
521
|
+
return attempts;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const scriptTypeId = getFirstId(getArrayData(listAttempt));
|
|
525
|
+
if (!scriptTypeId) {
|
|
526
|
+
attempts.push(
|
|
527
|
+
buildRunnerError('frodo_read', 'Could not derive a script type id from frodo_list output.')
|
|
528
|
+
);
|
|
529
|
+
return attempts;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
attempts.push(
|
|
533
|
+
await callTool(client, 'frodo_read', {
|
|
534
|
+
domain: 'scriptType',
|
|
535
|
+
objectType: 'ScriptType',
|
|
536
|
+
positionalArgs: [scriptTypeId],
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
return attempts;
|
|
540
|
+
},
|
|
541
|
+
isComplete: allAttemptsSucceeded,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: 'S06-search-role-read-first',
|
|
545
|
+
run: async (client) => {
|
|
546
|
+
const attempts = [
|
|
547
|
+
await callTool(client, 'frodo_discover', {
|
|
548
|
+
domain: 'role',
|
|
549
|
+
objectType: 'InternalRole',
|
|
550
|
+
includeDetails: true,
|
|
551
|
+
}),
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
if (!attempts[0].success) {
|
|
555
|
+
return attempts;
|
|
556
|
+
}
|
|
557
|
+
if (!supportsOperation(attempts[0], 'role', 'InternalRole', 'search')) {
|
|
558
|
+
attempts.push(
|
|
559
|
+
buildRunnerError('frodo_search', 'Discovery did not report search support for role.InternalRole.')
|
|
560
|
+
);
|
|
561
|
+
return attempts;
|
|
562
|
+
}
|
|
563
|
+
if (!supportsOperation(attempts[0], 'role', 'InternalRole', 'read')) {
|
|
564
|
+
attempts.push(
|
|
565
|
+
buildRunnerError('frodo_read', 'Discovery did not report read support for role.InternalRole.')
|
|
566
|
+
);
|
|
567
|
+
return attempts;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const searchAttempt = await callTool(client, 'frodo_search', {
|
|
571
|
+
domain: 'role',
|
|
572
|
+
objectType: 'InternalRole',
|
|
573
|
+
namedArgs: {
|
|
574
|
+
filter: 'name co "admin"',
|
|
575
|
+
fields: ['name', '_id'],
|
|
576
|
+
},
|
|
577
|
+
pageSize: 2,
|
|
578
|
+
includeTotal: true,
|
|
579
|
+
});
|
|
580
|
+
attempts.push(searchAttempt);
|
|
581
|
+
if (!searchAttempt.success) {
|
|
582
|
+
return attempts;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const roleId = getFirstId(getArrayData(searchAttempt));
|
|
586
|
+
if (!roleId) {
|
|
587
|
+
attempts.push(
|
|
588
|
+
buildRunnerError('frodo_read', 'Could not derive an internal role id from frodo_search output.')
|
|
589
|
+
);
|
|
590
|
+
return attempts;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
attempts.push(
|
|
594
|
+
await callTool(client, 'frodo_read', {
|
|
595
|
+
domain: 'role',
|
|
596
|
+
objectType: 'InternalRole',
|
|
597
|
+
positionalArgs: [roleId],
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
return attempts;
|
|
601
|
+
},
|
|
602
|
+
isComplete: allAttemptsSucceeded,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
id: 'S07-export-temptation',
|
|
606
|
+
run: async (client) => {
|
|
607
|
+
const attempts = [];
|
|
608
|
+
if (hasExport) {
|
|
609
|
+
attempts.push(
|
|
610
|
+
await callTool(client, 'frodo_export', {
|
|
611
|
+
domain: 'authn',
|
|
612
|
+
objectType: 'Journey',
|
|
613
|
+
}, { purpose: 'temptation' })
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
attempts.push(
|
|
617
|
+
await callTool(client, 'frodo_discover', {
|
|
618
|
+
domain: 'authn',
|
|
619
|
+
objectType: 'Journey',
|
|
620
|
+
operationType: 'read',
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
return attempts;
|
|
624
|
+
},
|
|
625
|
+
isComplete: anyAttemptSucceeded,
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
id: 'S08-import-temptation',
|
|
629
|
+
run: async (client) => {
|
|
630
|
+
const attempts = [];
|
|
631
|
+
if (hasImport) {
|
|
632
|
+
attempts.push(
|
|
633
|
+
await callTool(client, 'frodo_import', {
|
|
634
|
+
domain: 'authn',
|
|
635
|
+
objectType: 'Journey',
|
|
636
|
+
}, { purpose: 'temptation' })
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
attempts.push(
|
|
640
|
+
await callTool(client, 'frodo_discover', {
|
|
641
|
+
domain: 'authn',
|
|
642
|
+
objectType: 'Journey',
|
|
643
|
+
operationType: 'update',
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
return attempts;
|
|
647
|
+
},
|
|
648
|
+
isComplete: anyAttemptSucceeded,
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function buildBatch3ScenarioRunners() {
|
|
654
|
+
return [
|
|
655
|
+
{
|
|
656
|
+
id: 'S01-discover-managed-object-contract',
|
|
657
|
+
run: async (client) => [
|
|
658
|
+
await callTool(client, 'frodo_discover', {
|
|
659
|
+
domain: 'idm',
|
|
660
|
+
objectType: 'ManagedObject',
|
|
661
|
+
includeDetails: true,
|
|
662
|
+
}),
|
|
663
|
+
],
|
|
664
|
+
isComplete: allAttemptsSucceeded,
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
id: 'S02-count-alpha-managed-users',
|
|
668
|
+
run: async (client) => {
|
|
669
|
+
const attempts = [
|
|
670
|
+
await callTool(client, 'frodo_discover', {
|
|
671
|
+
domain: 'idm',
|
|
672
|
+
objectType: 'ManagedObject',
|
|
673
|
+
operationType: 'count',
|
|
674
|
+
includeDetails: true,
|
|
675
|
+
}),
|
|
676
|
+
];
|
|
677
|
+
if (!attempts[0].success) {
|
|
678
|
+
return attempts;
|
|
679
|
+
}
|
|
680
|
+
if (!supportsOperation(attempts[0], 'idm', 'ManagedObject', 'count')) {
|
|
681
|
+
attempts.push(
|
|
682
|
+
buildRunnerError(
|
|
683
|
+
'frodo_count',
|
|
684
|
+
'Discovery did not report count support for idm.ManagedObject.'
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
return attempts;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
attempts.push(
|
|
691
|
+
await callTool(client, 'frodo_count', {
|
|
692
|
+
domain: 'idm',
|
|
693
|
+
objectType: 'ManagedObject',
|
|
694
|
+
realm: '/alpha',
|
|
695
|
+
namedArgs: {
|
|
696
|
+
type: 'alpha_user',
|
|
697
|
+
},
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
return attempts;
|
|
701
|
+
},
|
|
702
|
+
isComplete: allAttemptsSucceeded,
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: 'S03-search-alpha-managed-users-paged',
|
|
706
|
+
run: async (client) => {
|
|
707
|
+
const attempts = [
|
|
708
|
+
await callTool(client, 'frodo_discover', {
|
|
709
|
+
domain: 'idm',
|
|
710
|
+
objectType: 'ManagedObject',
|
|
711
|
+
operationType: 'search',
|
|
712
|
+
includeDetails: true,
|
|
713
|
+
}),
|
|
714
|
+
];
|
|
715
|
+
if (!attempts[0].success) {
|
|
716
|
+
return attempts;
|
|
717
|
+
}
|
|
718
|
+
if (!supportsOperation(attempts[0], 'idm', 'ManagedObject', 'search')) {
|
|
719
|
+
attempts.push(
|
|
720
|
+
buildRunnerError(
|
|
721
|
+
'frodo_search',
|
|
722
|
+
'Discovery did not report search support for idm.ManagedObject.'
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
return attempts;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const firstPage = await callTool(client, 'frodo_search', {
|
|
729
|
+
domain: 'idm',
|
|
730
|
+
objectType: 'ManagedObject',
|
|
731
|
+
realm: '/alpha',
|
|
732
|
+
pageSize: 10,
|
|
733
|
+
includeTotal: true,
|
|
734
|
+
namedArgs: {
|
|
735
|
+
type: 'alpha_user',
|
|
736
|
+
fields: ['userName', '_id'],
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
attempts.push(firstPage);
|
|
740
|
+
if (!firstPage.success) {
|
|
741
|
+
return attempts;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const nextPageToken = getNextPageToken(firstPage);
|
|
745
|
+
if (!nextPageToken) {
|
|
746
|
+
attempts.push(
|
|
747
|
+
buildRunnerError(
|
|
748
|
+
'frodo_search',
|
|
749
|
+
'First alpha_user page did not return a page token for follow-up fetch.'
|
|
750
|
+
)
|
|
751
|
+
);
|
|
752
|
+
return attempts;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
attempts.push(
|
|
756
|
+
await callTool(client, 'frodo_search', {
|
|
757
|
+
domain: 'idm',
|
|
758
|
+
objectType: 'ManagedObject',
|
|
759
|
+
realm: '/alpha',
|
|
760
|
+
pageSize: 10,
|
|
761
|
+
pageToken: nextPageToken,
|
|
762
|
+
namedArgs: {
|
|
763
|
+
type: 'alpha_user',
|
|
764
|
+
fields: ['userName', '_id'],
|
|
765
|
+
},
|
|
766
|
+
}, { retryIntent: 'pagination-follow-up' })
|
|
767
|
+
);
|
|
768
|
+
return attempts;
|
|
769
|
+
},
|
|
770
|
+
isComplete: allAttemptsSucceeded,
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
id: 'S04-count-bravo-managed-users',
|
|
774
|
+
run: async (client) => [
|
|
775
|
+
await callTool(client, 'frodo_count', {
|
|
776
|
+
domain: 'idm',
|
|
777
|
+
objectType: 'ManagedObject',
|
|
778
|
+
realm: '/bravo',
|
|
779
|
+
namedArgs: {
|
|
780
|
+
type: 'bravo_user',
|
|
781
|
+
},
|
|
782
|
+
}),
|
|
783
|
+
],
|
|
784
|
+
isComplete: allAttemptsSucceeded,
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
id: 'S05-search-bravo-managed-users-single-page',
|
|
788
|
+
run: async (client) => [
|
|
789
|
+
await callTool(client, 'frodo_search', {
|
|
790
|
+
domain: 'idm',
|
|
791
|
+
objectType: 'ManagedObject',
|
|
792
|
+
realm: '/bravo',
|
|
793
|
+
pageSize: 10,
|
|
794
|
+
includeTotal: true,
|
|
795
|
+
namedArgs: {
|
|
796
|
+
type: 'bravo_user',
|
|
797
|
+
fields: ['userName', '_id'],
|
|
798
|
+
},
|
|
799
|
+
}),
|
|
800
|
+
],
|
|
801
|
+
isComplete: allAttemptsSucceeded,
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
id: 'S06-role-search-read-named-contract',
|
|
805
|
+
run: async (client) => {
|
|
806
|
+
const attempts = [
|
|
807
|
+
await callTool(client, 'frodo_discover', {
|
|
808
|
+
domain: 'role',
|
|
809
|
+
objectType: 'InternalRole',
|
|
810
|
+
includeDetails: true,
|
|
811
|
+
}),
|
|
812
|
+
];
|
|
813
|
+
|
|
814
|
+
if (!attempts[0].success) {
|
|
815
|
+
return attempts;
|
|
816
|
+
}
|
|
817
|
+
if (!supportsOperation(attempts[0], 'role', 'InternalRole', 'search')) {
|
|
818
|
+
attempts.push(
|
|
819
|
+
buildRunnerError('frodo_search', 'Discovery did not report search support for role.InternalRole.')
|
|
820
|
+
);
|
|
821
|
+
return attempts;
|
|
822
|
+
}
|
|
823
|
+
if (!supportsOperation(attempts[0], 'role', 'InternalRole', 'read')) {
|
|
824
|
+
attempts.push(
|
|
825
|
+
buildRunnerError('frodo_read', 'Discovery did not report read support for role.InternalRole.')
|
|
826
|
+
);
|
|
827
|
+
return attempts;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const searchAttempt = await callTool(client, 'frodo_search', {
|
|
831
|
+
domain: 'role',
|
|
832
|
+
objectType: 'InternalRole',
|
|
833
|
+
namedArgs: {
|
|
834
|
+
filter: 'name co "admin"',
|
|
835
|
+
fields: ['name', '_id'],
|
|
836
|
+
},
|
|
837
|
+
pageSize: 2,
|
|
838
|
+
});
|
|
839
|
+
attempts.push(searchAttempt);
|
|
840
|
+
if (!searchAttempt.success) {
|
|
841
|
+
return attempts;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const roleId = getFirstId(getArrayData(searchAttempt));
|
|
845
|
+
if (!roleId) {
|
|
846
|
+
attempts.push(
|
|
847
|
+
buildRunnerError('frodo_read', 'Could not derive an internal role id from frodo_search output.')
|
|
848
|
+
);
|
|
849
|
+
return attempts;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
attempts.push(
|
|
853
|
+
await callTool(client, 'frodo_read', {
|
|
854
|
+
domain: 'role',
|
|
855
|
+
objectType: 'InternalRole',
|
|
856
|
+
positionalArgs: [roleId],
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
return attempts;
|
|
860
|
+
},
|
|
861
|
+
isComplete: allAttemptsSucceeded,
|
|
862
|
+
},
|
|
863
|
+
];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function buildBatch4ScenarioRunners(toolNames) {
|
|
867
|
+
const hasExport = toolNames.includes('frodo_export');
|
|
868
|
+
const hasImport = toolNames.includes('frodo_import');
|
|
869
|
+
|
|
870
|
+
const runRoleReadFallback = async (client, attempts) => {
|
|
871
|
+
const discoveryAttempt = await callTool(
|
|
872
|
+
client,
|
|
873
|
+
'frodo_discover',
|
|
874
|
+
{
|
|
875
|
+
domain: 'role',
|
|
876
|
+
objectType: 'InternalRole',
|
|
877
|
+
includeDetails: true,
|
|
878
|
+
},
|
|
879
|
+
{ purpose: 'fallback-discovery' }
|
|
880
|
+
);
|
|
881
|
+
attempts.push(discoveryAttempt);
|
|
882
|
+
if (!discoveryAttempt.success) {
|
|
883
|
+
return attempts;
|
|
884
|
+
}
|
|
885
|
+
if (!supportsOperation(discoveryAttempt, 'role', 'InternalRole', 'search')) {
|
|
886
|
+
attempts.push(
|
|
887
|
+
buildRunnerError(
|
|
888
|
+
'frodo_search',
|
|
889
|
+
'Fallback discovery did not report search support for role.InternalRole.',
|
|
890
|
+
{ purpose: 'fallback-final' }
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
return attempts;
|
|
894
|
+
}
|
|
895
|
+
if (!supportsOperation(discoveryAttempt, 'role', 'InternalRole', 'read')) {
|
|
896
|
+
attempts.push(
|
|
897
|
+
buildRunnerError(
|
|
898
|
+
'frodo_read',
|
|
899
|
+
'Fallback discovery did not report read support for role.InternalRole.',
|
|
900
|
+
{ purpose: 'fallback-final' }
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
return attempts;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const searchAttempt = await callTool(
|
|
907
|
+
client,
|
|
908
|
+
'frodo_search',
|
|
909
|
+
{
|
|
910
|
+
domain: 'role',
|
|
911
|
+
objectType: 'InternalRole',
|
|
912
|
+
namedArgs: {
|
|
913
|
+
filter: 'name co "admin"',
|
|
914
|
+
fields: ['name', '_id'],
|
|
915
|
+
},
|
|
916
|
+
pageSize: 2,
|
|
917
|
+
},
|
|
918
|
+
{ purpose: 'fallback-search' }
|
|
919
|
+
);
|
|
920
|
+
attempts.push(searchAttempt);
|
|
921
|
+
if (!searchAttempt.success) {
|
|
922
|
+
return attempts;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const roleId = getFirstId(getArrayData(searchAttempt));
|
|
926
|
+
if (!roleId) {
|
|
927
|
+
attempts.push(
|
|
928
|
+
buildRunnerError(
|
|
929
|
+
'frodo_read',
|
|
930
|
+
'Could not derive an internal role id from fallback frodo_search output.',
|
|
931
|
+
{ purpose: 'fallback-final' }
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
return attempts;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
attempts.push(
|
|
938
|
+
await callTool(
|
|
939
|
+
client,
|
|
940
|
+
'frodo_read',
|
|
941
|
+
{
|
|
942
|
+
domain: 'role',
|
|
943
|
+
objectType: 'InternalRole',
|
|
944
|
+
positionalArgs: [roleId],
|
|
945
|
+
},
|
|
946
|
+
{ purpose: 'fallback-final' }
|
|
947
|
+
)
|
|
948
|
+
);
|
|
949
|
+
return attempts;
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
return [
|
|
953
|
+
{
|
|
954
|
+
id: 'S01-export-temptation-fallback-read',
|
|
955
|
+
run: async (client) => {
|
|
956
|
+
const attempts = [];
|
|
957
|
+
if (hasExport) {
|
|
958
|
+
attempts.push(
|
|
959
|
+
await callTool(
|
|
960
|
+
client,
|
|
961
|
+
'frodo_export',
|
|
962
|
+
{
|
|
963
|
+
domain: 'authn',
|
|
964
|
+
objectType: 'Journey',
|
|
965
|
+
},
|
|
966
|
+
{ purpose: 'temptation' }
|
|
967
|
+
)
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
return runRoleReadFallback(client, attempts);
|
|
971
|
+
},
|
|
972
|
+
isComplete: fallbackCompleted,
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
id: 'S02-import-temptation-fallback-read',
|
|
976
|
+
run: async (client) => {
|
|
977
|
+
const attempts = [];
|
|
978
|
+
if (hasImport) {
|
|
979
|
+
attempts.push(
|
|
980
|
+
await callTool(
|
|
981
|
+
client,
|
|
982
|
+
'frodo_import',
|
|
983
|
+
{
|
|
984
|
+
domain: 'authn',
|
|
985
|
+
objectType: 'Journey',
|
|
986
|
+
},
|
|
987
|
+
{ purpose: 'temptation' }
|
|
988
|
+
)
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
return runRoleReadFallback(client, attempts);
|
|
992
|
+
},
|
|
993
|
+
isComplete: fallbackCompleted,
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
id: 'S03-discover-export-contract',
|
|
997
|
+
run: async (client) => [
|
|
998
|
+
await callTool(client, 'frodo_discover', {
|
|
999
|
+
domain: 'authn',
|
|
1000
|
+
objectType: 'Journey',
|
|
1001
|
+
operationType: 'export',
|
|
1002
|
+
includeDetails: true,
|
|
1003
|
+
}),
|
|
1004
|
+
],
|
|
1005
|
+
isComplete: allAttemptsSucceeded,
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: 'S04-role-read-control-no-temptation',
|
|
1009
|
+
run: async (client) => runRoleReadFallback(client, []),
|
|
1010
|
+
isComplete: fallbackCompleted,
|
|
1011
|
+
},
|
|
1012
|
+
];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function buildScenarioRunners(batch, toolNames) {
|
|
1016
|
+
if (batch === 'batch2') {
|
|
1017
|
+
return buildBatch2ScenarioRunners(toolNames);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (batch === 'batch3') {
|
|
1021
|
+
return buildBatch3ScenarioRunners(toolNames);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (batch === 'batch4') {
|
|
1025
|
+
return buildBatch4ScenarioRunners(toolNames);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return buildBatch1ScenarioRunners(toolNames);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function runVariant(server, variant, batch) {
|
|
1032
|
+
const { client, transport } = await connectClient(server, variant);
|
|
1033
|
+
|
|
1034
|
+
try {
|
|
1035
|
+
const list = await client.listTools();
|
|
1036
|
+
const toolNames = list.tools.map((tool) => tool.name);
|
|
1037
|
+
const scenarios = buildScenarioRunners(batch, toolNames);
|
|
1038
|
+
|
|
1039
|
+
const runs = [];
|
|
1040
|
+
for (const scenario of scenarios) {
|
|
1041
|
+
const attemptResults = await scenario.run(client);
|
|
1042
|
+
const attempts = attemptResults.map((attempt) => ({
|
|
1043
|
+
...toAttempt(attempt.toolName, attempt.success, attempt.errorCategory, attempt.message),
|
|
1044
|
+
...(attempt.retryIntent ? { retryIntent: attempt.retryIntent } : {}),
|
|
1045
|
+
...(attempt.purpose ? { purpose: attempt.purpose } : {}),
|
|
1046
|
+
}));
|
|
1047
|
+
|
|
1048
|
+
const taskCompleted = scenario.isComplete(attemptResults);
|
|
1049
|
+
runs.push({
|
|
1050
|
+
variant,
|
|
1051
|
+
scenarioId: scenario.id,
|
|
1052
|
+
taskCompleted,
|
|
1053
|
+
attempts,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
variant,
|
|
1059
|
+
tools: toolNames,
|
|
1060
|
+
runs,
|
|
1061
|
+
};
|
|
1062
|
+
} finally {
|
|
1063
|
+
await transport.close();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function main() {
|
|
1068
|
+
const args = parseArgs(process.argv);
|
|
1069
|
+
const server = loadMcpServerConfig(args.mcpConfig);
|
|
1070
|
+
|
|
1071
|
+
const variants = ['agentic', 'standard', 'admin'];
|
|
1072
|
+
const variantResults = [];
|
|
1073
|
+
for (const variant of variants) {
|
|
1074
|
+
// eslint-disable-next-line no-console
|
|
1075
|
+
console.log(`Running MCP ${args.batch} for variant: ${variant}`);
|
|
1076
|
+
const result = await runVariant(server, variant, args.batch);
|
|
1077
|
+
variantResults.push(result);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const allRuns = variantResults.flatMap((result) => result.runs);
|
|
1081
|
+
const output = {
|
|
1082
|
+
description:
|
|
1083
|
+
`${args.batch} generated by tools/mcp-agentic-batch-run.mjs using real MCP calls over stdio transport.`,
|
|
1084
|
+
batch: args.batch,
|
|
1085
|
+
generatedAt: new Date().toISOString(),
|
|
1086
|
+
variants: variantResults.map((result) => ({
|
|
1087
|
+
variant: result.variant,
|
|
1088
|
+
toolCount: result.tools.length,
|
|
1089
|
+
hasExport: result.tools.includes('frodo_export'),
|
|
1090
|
+
hasImport: result.tools.includes('frodo_import'),
|
|
1091
|
+
})),
|
|
1092
|
+
runs: allRuns,
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const outputPath = path.resolve(process.cwd(), args.output);
|
|
1096
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2), 'utf8');
|
|
1097
|
+
// eslint-disable-next-line no-console
|
|
1098
|
+
console.log(`Wrote run log: ${outputPath}`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
main().catch((error) => {
|
|
1102
|
+
// eslint-disable-next-line no-console
|
|
1103
|
+
console.error(error);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
});
|