unbound-cli 1.3.1 → 1.4.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.
- package/PLAN-web-4887.md +477 -0
- package/PLAN.md +117 -0
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/commands/policy.js +203 -16
- package/src/commands/setup.js +4 -2
- package/src/lib/policy-ai-assist.js +503 -0
- package/test/eval/README.md +45 -0
- package/test/eval/policy-prompts.json +122 -0
- package/test/eval/run-eval.js +57 -0
- package/test/policy-ai-assist-mcp.test.js +606 -0
- package/test/policy-ai-assist-preflight.test.js +66 -0
- package/test/policy-ai-assist.test.js +884 -0
- package/test/policy-conditions.test.js +28 -0
package/README.md
CHANGED
|
@@ -196,15 +196,21 @@ unbound policy security create --name "429 Fallback" --sub-type error-code-routi
|
|
|
196
196
|
Tool policy examples:
|
|
197
197
|
|
|
198
198
|
```bash
|
|
199
|
+
# AI-assisted (preferred): describe the policy in natural language.
|
|
200
|
+
unbound policy tool create-terminal --prompt "block rm -rf"
|
|
201
|
+
|
|
202
|
+
# AI-assisted MCP policy: describe the service and intent in natural language.
|
|
203
|
+
unbound policy tool create-mcp --prompt "audit all Linear writes"
|
|
204
|
+
|
|
199
205
|
# See what command families and MCP servers are available
|
|
200
206
|
unbound policy tool families
|
|
201
207
|
unbound policy tool mcp-servers
|
|
202
208
|
|
|
203
|
-
#
|
|
209
|
+
# Flag-based fallback: block destructive shell commands explicitly
|
|
204
210
|
unbound policy tool create-terminal --name "Block rm -rf" --command-family filesystem \
|
|
205
211
|
--field command='rm -rf*' --action BLOCK --custom-message "Destructive command blocked."
|
|
206
212
|
|
|
207
|
-
#
|
|
213
|
+
# Flag-based MCP fallback: audit Linear write operations
|
|
208
214
|
unbound policy tool create-mcp --name "Audit Linear writes" --mcp-server Linear \
|
|
209
215
|
--mcp-action-type write --action AUDIT
|
|
210
216
|
|
package/package.json
CHANGED
package/src/commands/policy.js
CHANGED
|
@@ -311,6 +311,16 @@ function parseFieldSpec(spec) {
|
|
|
311
311
|
return [spec.slice(0, eq), spec.slice(eq + 1)];
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// "ANY" is a wildcard across all fields; it can't be ANDed with other conditions.
|
|
315
|
+
// Mirrors the backend rule so the CLI fails fast before the API call. The check is
|
|
316
|
+
// case-insensitive so `--field Any=*`/`--field any=*` are caught too.
|
|
317
|
+
function assertAnyExclusive(configObj) {
|
|
318
|
+
const keys = Object.keys(configObj || {});
|
|
319
|
+
if (keys.length > 1 && keys.some((k) => k.toUpperCase() === 'ANY')) {
|
|
320
|
+
throw new Error("The 'ANY' field cannot be combined with other --field conditions (ANY already matches every field).");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
314
324
|
/**
|
|
315
325
|
* Convert a `--sub-type` flag (kebab-case) to the backend's snake_case form.
|
|
316
326
|
*/
|
|
@@ -1419,18 +1429,27 @@ Subcommands:
|
|
|
1419
1429
|
tool
|
|
1420
1430
|
.command('create-terminal')
|
|
1421
1431
|
.description('Create a TERMINAL_COMMAND policy: monitor or block shell commands run by AI coding tools.')
|
|
1422
|
-
.
|
|
1423
|
-
.
|
|
1424
|
-
.option('--
|
|
1425
|
-
.
|
|
1432
|
+
.option('--prompt <text>', 'Natural-language description; routes through Unbound AI assist. Mutually exclusive with --command-family/--field/--config. Compatible with --name/--description/--action overrides.')
|
|
1433
|
+
.option('--name <name>', 'Policy name (required unless --prompt is used)')
|
|
1434
|
+
.option('--command-family <family>', 'Command family (e.g. git, filesystem, network). See `policy tool families`.')
|
|
1435
|
+
.option('--field <key=pattern>', 'Match field: <key>=<pattern>. Repeatable — multiple --field are ANDed (all must match), one per field, for a more specific policy. At least one required unless --config is used.', (val, prev = []) => [...prev, val])
|
|
1436
|
+
.option('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
|
|
1426
1437
|
.option('--description <text>', 'Human-readable description')
|
|
1427
1438
|
.option('--custom-message <text>', 'Message shown to the user when the policy fires. Required when --action is BLOCK or WARN.')
|
|
1428
1439
|
.option('--group <names|ids>', 'Comma-separated user group names or IDs to scope this policy to')
|
|
1429
1440
|
.option('--disabled', 'Create the policy in disabled state')
|
|
1430
1441
|
.option('--config <json>', 'Advanced: raw config JSON (skips --field builder)')
|
|
1442
|
+
.option('--yes', 'Skip the confirmation prompt for --prompt flows (preview is still printed)')
|
|
1431
1443
|
.option('--json', 'Output raw JSON of the created policy')
|
|
1432
1444
|
.addHelpText('after', `
|
|
1433
1445
|
Examples:
|
|
1446
|
+
# AI-assisted (preferred): describe the policy in natural language.
|
|
1447
|
+
$ unbound policy tool create-terminal --prompt "block rm -rf"
|
|
1448
|
+
|
|
1449
|
+
# AI-assisted, with manual overrides for name and action:
|
|
1450
|
+
$ unbound policy tool create-terminal --prompt "block rm -rf" \\
|
|
1451
|
+
--name "Block recursive deletes" --action WARN
|
|
1452
|
+
|
|
1434
1453
|
$ unbound policy tool create-terminal --name "Block rm -rf" \\
|
|
1435
1454
|
--command-family filesystem --field command='rm -rf*' --action BLOCK \\
|
|
1436
1455
|
--custom-message "Destructive commands are blocked."
|
|
@@ -1438,9 +1457,11 @@ Examples:
|
|
|
1438
1457
|
$ unbound policy tool create-terminal --name "Audit git push" \\
|
|
1439
1458
|
--command-family git --field command='git push*' --action AUDIT
|
|
1440
1459
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
--
|
|
1460
|
+
# Multiple --field are ANDed — this fires only on a push to main on origin:
|
|
1461
|
+
$ unbound policy tool create-terminal --name "Block push to main on origin" \\
|
|
1462
|
+
--command-family git_action \\
|
|
1463
|
+
--field operation=push --field branch=main --field remote='*origin*' \\
|
|
1464
|
+
--action BLOCK --custom-message "Pushing to main on origin is blocked."
|
|
1444
1465
|
|
|
1445
1466
|
Discover valid command families and their fields: unbound policy tool families
|
|
1446
1467
|
Learn more: ${DOCS_TOOL}
|
|
@@ -1448,6 +1469,83 @@ Learn more: ${DOCS_TOOL}
|
|
|
1448
1469
|
.action(async (opts) => {
|
|
1449
1470
|
try {
|
|
1450
1471
|
requireLogin();
|
|
1472
|
+
|
|
1473
|
+
if (opts.prompt !== undefined) {
|
|
1474
|
+
if (typeof opts.prompt !== 'string' || opts.prompt.trim() === '') {
|
|
1475
|
+
output.error('--prompt cannot be empty.');
|
|
1476
|
+
process.exitCode = 2;
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
// --name, --description, --action are now allowed as AI overrides (merged in mergeAiAndFlags).
|
|
1480
|
+
// --command-family, --field, --config remain mutex with --prompt because they're AI
|
|
1481
|
+
// classification territory; if the AI gets them wrong, the right answer is re-prompting,
|
|
1482
|
+
// not flag overrides (which would need catalog validation we don't want to add here).
|
|
1483
|
+
const mutex = ['commandFamily', 'field', 'config'];
|
|
1484
|
+
for (const k of mutex) {
|
|
1485
|
+
if (opts[k]) {
|
|
1486
|
+
throw new Error('Pass --prompt for AI-assist or the field flags for explicit creation, not both.');
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (opts.group) {
|
|
1490
|
+
let formData;
|
|
1491
|
+
try {
|
|
1492
|
+
formData = await loadFormData();
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
const { routeBackendError } = require('../lib/policy-ai-assist');
|
|
1495
|
+
const routed = routeBackendError(err);
|
|
1496
|
+
output.error(routed.message);
|
|
1497
|
+
process.exitCode = routed.exitCode;
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
opts.scopeUserGroupIds = resolveByName(formData.user_groups || [], opts.group, { kind: 'user group' });
|
|
1501
|
+
}
|
|
1502
|
+
const { runTerminalPromptCreate } = require('../lib/policy-ai-assist');
|
|
1503
|
+
let result;
|
|
1504
|
+
try {
|
|
1505
|
+
result = await runTerminalPromptCreate(opts);
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
if (err.exitCode === 0) {
|
|
1508
|
+
output.warn(err.message);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
output.error(err.message);
|
|
1512
|
+
process.exitCode = err.exitCode || 1;
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (!result.confirmed) {
|
|
1516
|
+
output.warn('Aborted.');
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const body = result.body;
|
|
1520
|
+
let data;
|
|
1521
|
+
try {
|
|
1522
|
+
data = await api.post('/api/v1/command-policies/', { body });
|
|
1523
|
+
} catch (err) {
|
|
1524
|
+
const { routeBackendError } = require('../lib/policy-ai-assist');
|
|
1525
|
+
const routed = routeBackendError(err);
|
|
1526
|
+
output.error(routed.message);
|
|
1527
|
+
process.exitCode = routed.exitCode;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (opts.json) {
|
|
1531
|
+
output.json(data);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
output.success(`Terminal policy${body.name ? ` "${body.name}"` : ''} created.`);
|
|
1535
|
+
displayToolPolicy(unwrapToolPolicy(data));
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
if (!opts.name) {
|
|
1540
|
+
throw new Error('--name is required (or use --prompt for AI assist).');
|
|
1541
|
+
}
|
|
1542
|
+
if (!opts.commandFamily) {
|
|
1543
|
+
throw new Error('--command-family is required (or use --prompt for AI assist).');
|
|
1544
|
+
}
|
|
1545
|
+
if (!opts.action) {
|
|
1546
|
+
throw new Error('--action is required (or use --prompt for AI assist).');
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1451
1549
|
const action = normalizeAction(opts.action, TOOL_ACTIONS, '--action');
|
|
1452
1550
|
if ((action === 'BLOCK' || action === 'WARN') && !opts.customMessage) {
|
|
1453
1551
|
throw new Error('--custom-message is required when --action is BLOCK or WARN.');
|
|
@@ -1469,6 +1567,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1469
1567
|
configObj[k] = v;
|
|
1470
1568
|
}
|
|
1471
1569
|
}
|
|
1570
|
+
assertAnyExclusive(configObj);
|
|
1472
1571
|
|
|
1473
1572
|
const body = {
|
|
1474
1573
|
name: opts.name,
|
|
@@ -1502,24 +1601,35 @@ Learn more: ${DOCS_TOOL}
|
|
|
1502
1601
|
tool
|
|
1503
1602
|
.command('create-mcp')
|
|
1504
1603
|
.description('Create an MCP_TOOL policy: monitor or block MCP server tool calls.')
|
|
1505
|
-
.
|
|
1506
|
-
.
|
|
1604
|
+
.option('--prompt <text>', 'Natural-language description; routes through Unbound AI assist. Mutually exclusive with --mcp-server/--mcp-tool/--mcp-action-type/--config. Compatible with --name/--description/--action overrides.')
|
|
1605
|
+
.option('--name <name>', 'Policy name (required unless --prompt is used). Override AI suggestion when used with --prompt.')
|
|
1606
|
+
.option('--mcp-server <server>', 'MCP server name as shown by `policy tool mcp-servers` (e.g. linear, github). Resolved to its canonical group automatically.')
|
|
1507
1607
|
.option('--mcp-tool <tool>', 'Specific tool name on the MCP server (e.g. create_issue). Mutually exclusive with --mcp-action-type.')
|
|
1508
1608
|
.option('--mcp-action-type <type>', `Match by tool action type: ${MCP_ACTION_TYPES.join(' | ')}. Mutually exclusive with --mcp-tool.`)
|
|
1509
|
-
.
|
|
1510
|
-
.option('--description <text>', 'Human-readable description')
|
|
1511
|
-
.option('--custom-message <text>', 'Message shown to the user when the policy fires. Required when --action is BLOCK or WARN.')
|
|
1609
|
+
.option('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}. Override AI suggestion when used with --prompt.`)
|
|
1610
|
+
.option('--description <text>', 'Human-readable description. Override AI suggestion when used with --prompt.')
|
|
1611
|
+
.option('--custom-message <text>', 'Message shown to the user when the policy fires. Required when --action is BLOCK or WARN. Override AI suggestion when used with --prompt.')
|
|
1512
1612
|
.option('--group <names|ids>', 'Comma-separated user group names or IDs to scope this policy to')
|
|
1513
1613
|
.option('--disabled', 'Create the policy in disabled state')
|
|
1614
|
+
.option('--config <json>', 'Advanced: raw config JSON')
|
|
1615
|
+
.option('--yes', 'Skip the confirmation prompt for --prompt flows (preview is still printed)')
|
|
1514
1616
|
.option('--json', 'Output raw JSON of the created policy')
|
|
1515
1617
|
.addHelpText('after', `
|
|
1516
1618
|
Examples:
|
|
1517
|
-
|
|
1619
|
+
# AI-assisted (preferred): describe the policy in natural language.
|
|
1620
|
+
$ unbound policy tool create-mcp --prompt "audit all Linear writes"
|
|
1621
|
+
|
|
1622
|
+
# AI-assisted, with manual overrides for name and action:
|
|
1623
|
+
$ unbound policy tool create-mcp --prompt "audit all Linear writes" \\
|
|
1624
|
+
--name "Audit Linear writes" --action WARN \\
|
|
1625
|
+
--custom-message "Linear writes are monitored."
|
|
1626
|
+
|
|
1627
|
+
# Match a specific tool on a server (flag-based fallback):
|
|
1518
1628
|
$ unbound policy tool create-mcp --name "Block Linear writes" \\
|
|
1519
1629
|
--mcp-server linear --mcp-tool create_issue --action BLOCK \\
|
|
1520
1630
|
--custom-message "Issue creation is blocked. Contact admin."
|
|
1521
1631
|
|
|
1522
|
-
Match all destructive tools on a server:
|
|
1632
|
+
# Match all destructive tools on a server:
|
|
1523
1633
|
$ unbound policy tool create-mcp --name "Audit all destructive Slack" \\
|
|
1524
1634
|
--mcp-server slack --mcp-action-type destructive --action AUDIT
|
|
1525
1635
|
|
|
@@ -1532,6 +1642,79 @@ Learn more: ${DOCS_TOOL}
|
|
|
1532
1642
|
.action(async (opts) => {
|
|
1533
1643
|
try {
|
|
1534
1644
|
requireLogin();
|
|
1645
|
+
|
|
1646
|
+
if (opts.prompt !== undefined) {
|
|
1647
|
+
if (typeof opts.prompt !== 'string' || opts.prompt.trim() === '') {
|
|
1648
|
+
output.error('--prompt cannot be empty.');
|
|
1649
|
+
process.exitCode = 2;
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const mutex = ['mcpServer', 'mcpTool', 'mcpActionType', 'config'];
|
|
1653
|
+
for (const k of mutex) {
|
|
1654
|
+
if (opts[k]) {
|
|
1655
|
+
throw new Error('Pass --prompt for AI-assist or the field flags for explicit creation, not both.');
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (opts.group) {
|
|
1659
|
+
let formData;
|
|
1660
|
+
try {
|
|
1661
|
+
formData = await loadFormData();
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
const { routeBackendError } = require('../lib/policy-ai-assist');
|
|
1664
|
+
const routed = routeBackendError(err);
|
|
1665
|
+
output.error(routed.message);
|
|
1666
|
+
process.exitCode = routed.exitCode;
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
opts.scopeUserGroupIds = resolveByName(formData.user_groups || [], opts.group, { kind: 'user group' });
|
|
1670
|
+
}
|
|
1671
|
+
const { runMcpPromptCreate } = require('../lib/policy-ai-assist');
|
|
1672
|
+
let result;
|
|
1673
|
+
try {
|
|
1674
|
+
result = await runMcpPromptCreate(opts);
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
if (err.exitCode === 0) {
|
|
1677
|
+
output.warn(err.message);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
output.error(err.message);
|
|
1681
|
+
process.exitCode = err.exitCode || 1;
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (!result.confirmed) {
|
|
1685
|
+
output.warn('Aborted.');
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const body = result.body;
|
|
1689
|
+
let data;
|
|
1690
|
+
try {
|
|
1691
|
+
data = await api.post('/api/v1/command-policies/', { body });
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
const { routeBackendError } = require('../lib/policy-ai-assist');
|
|
1694
|
+
const routed = routeBackendError(err);
|
|
1695
|
+
output.error(routed.message);
|
|
1696
|
+
process.exitCode = routed.exitCode;
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (opts.json) {
|
|
1700
|
+
output.json(data);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
output.success(`MCP policy${body.name ? ` "${body.name}"` : ''} created.`);
|
|
1704
|
+
displayToolPolicy(unwrapToolPolicy(data));
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
if (!opts.name) {
|
|
1709
|
+
throw new Error('--name is required (or use --prompt for AI assist).');
|
|
1710
|
+
}
|
|
1711
|
+
if (!opts.mcpServer) {
|
|
1712
|
+
throw new Error('--mcp-server is required (or use --prompt for AI assist).');
|
|
1713
|
+
}
|
|
1714
|
+
if (!opts.action) {
|
|
1715
|
+
throw new Error('--action is required (or use --prompt for AI assist).');
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1535
1718
|
const action = normalizeAction(opts.action, TOOL_ACTIONS, '--action');
|
|
1536
1719
|
if ((action === 'BLOCK' || action === 'WARN') && !opts.customMessage) {
|
|
1537
1720
|
throw new Error('--custom-message is required when --action is BLOCK or WARN.');
|
|
@@ -1562,6 +1745,9 @@ Learn more: ${DOCS_TOOL}
|
|
|
1562
1745
|
enabled: !opts.disabled,
|
|
1563
1746
|
config: {},
|
|
1564
1747
|
};
|
|
1748
|
+
if (opts.config) {
|
|
1749
|
+
body.config = parseJsonOrThrow(opts.config);
|
|
1750
|
+
}
|
|
1565
1751
|
if (hasTool) body.mcp_tool = opts.mcpTool;
|
|
1566
1752
|
if (hasActionType) body.mcp_tool_action_type = opts.mcpActionType;
|
|
1567
1753
|
if (opts.customMessage) body.custom_message = opts.customMessage;
|
|
@@ -1595,7 +1781,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1595
1781
|
.option('--enabled', 'Enable the policy')
|
|
1596
1782
|
.option('--disabled', 'Disable the policy')
|
|
1597
1783
|
.option('--command-family <family>', '(TERMINAL only) new command family')
|
|
1598
|
-
.option('--field <key=pattern>', '(TERMINAL only) replace config
|
|
1784
|
+
.option('--field <key=pattern>', '(TERMINAL only) replace config fields. Repeatable — multiple --field are ANDed (all must match), one per field.', (val, prev = []) => [...prev, val])
|
|
1599
1785
|
.option('--mcp-server <server>', '(MCP only) new MCP server')
|
|
1600
1786
|
.option('--mcp-tool <tool>', '(MCP only) new MCP tool')
|
|
1601
1787
|
.option('--mcp-action-type <type>', '(MCP only) new MCP action type')
|
|
@@ -1627,6 +1813,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1627
1813
|
} else if (opts.config) {
|
|
1628
1814
|
body.config = parseJsonOrThrow(opts.config);
|
|
1629
1815
|
}
|
|
1816
|
+
if (body.config) assertAnyExclusive(body.config);
|
|
1630
1817
|
|
|
1631
1818
|
if (opts.mcpServer) body.mcp_server = opts.mcpServer;
|
|
1632
1819
|
if (opts.mcpTool) body.mcp_tool = opts.mcpTool;
|
|
@@ -1888,4 +2075,4 @@ Learn more: ${DOCS_POLICIES}
|
|
|
1888
2075
|
registerLegacy(policy);
|
|
1889
2076
|
}
|
|
1890
2077
|
|
|
1891
|
-
module.exports = { register };
|
|
2078
|
+
module.exports = { register, assertAnyExclusive };
|
package/src/commands/setup.js
CHANGED
|
@@ -946,13 +946,14 @@ async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl, gatewayUrl,
|
|
|
946
946
|
// actually supports it (Claude Code hooks, Codex hooks, Copilot hooks).
|
|
947
947
|
// Cursor would print "not supported"; passing the flag to gateway-mode
|
|
948
948
|
// scripts would error out — `scriptSupportsBackfill` checks for both.
|
|
949
|
-
|
|
949
|
+
const result = await runBatch(resolvedTools, (tool) => {
|
|
950
950
|
const args = buildScriptArgs(apiKey, {
|
|
951
951
|
backendUrl, frontendUrl, gatewayUrl,
|
|
952
952
|
backfill: backfill && scriptSupportsBackfill(tool.script),
|
|
953
953
|
});
|
|
954
954
|
return runScriptPiped(tool.script, args);
|
|
955
955
|
});
|
|
956
|
+
return result;
|
|
956
957
|
}
|
|
957
958
|
|
|
958
959
|
/**
|
|
@@ -967,13 +968,14 @@ async function runMdmSetupAllBundle(adminApiKey, { backendUrl, frontendUrl, gate
|
|
|
967
968
|
if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
|
|
968
969
|
}
|
|
969
970
|
}
|
|
970
|
-
|
|
971
|
+
const result = await runBatch(resolvedTools, (tool) => {
|
|
971
972
|
const args = buildScriptArgs(adminApiKey, {
|
|
972
973
|
backendUrl, frontendUrl, gatewayUrl, mdm: true,
|
|
973
974
|
backfill: backfill && scriptSupportsBackfill(tool.script),
|
|
974
975
|
});
|
|
975
976
|
return runScriptPiped(tool.script, args);
|
|
976
977
|
});
|
|
978
|
+
return result;
|
|
977
979
|
}
|
|
978
980
|
|
|
979
981
|
module.exports = {
|