unbound-cli 1.3.2 → 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/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
- # Block destructive shell commands
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
- # Audit Linear write operations via MCP
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1429,18 +1429,27 @@ Subcommands:
1429
1429
  tool
1430
1430
  .command('create-terminal')
1431
1431
  .description('Create a TERMINAL_COMMAND policy: monitor or block shell commands run by AI coding tools.')
1432
- .requiredOption('--name <name>', 'Policy name (required)')
1433
- .requiredOption('--command-family <family>', 'Command family (e.g. git, filesystem, network). See `policy tool families`.')
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`.')
1434
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])
1435
- .requiredOption('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
1436
+ .option('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
1436
1437
  .option('--description <text>', 'Human-readable description')
1437
1438
  .option('--custom-message <text>', 'Message shown to the user when the policy fires. Required when --action is BLOCK or WARN.')
1438
1439
  .option('--group <names|ids>', 'Comma-separated user group names or IDs to scope this policy to')
1439
1440
  .option('--disabled', 'Create the policy in disabled state')
1440
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)')
1441
1443
  .option('--json', 'Output raw JSON of the created policy')
1442
1444
  .addHelpText('after', `
1443
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
+
1444
1453
  $ unbound policy tool create-terminal --name "Block rm -rf" \\
1445
1454
  --command-family filesystem --field command='rm -rf*' --action BLOCK \\
1446
1455
  --custom-message "Destructive commands are blocked."
@@ -1460,6 +1469,83 @@ Learn more: ${DOCS_TOOL}
1460
1469
  .action(async (opts) => {
1461
1470
  try {
1462
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
+
1463
1549
  const action = normalizeAction(opts.action, TOOL_ACTIONS, '--action');
1464
1550
  if ((action === 'BLOCK' || action === 'WARN') && !opts.customMessage) {
1465
1551
  throw new Error('--custom-message is required when --action is BLOCK or WARN.');
@@ -1515,24 +1601,35 @@ Learn more: ${DOCS_TOOL}
1515
1601
  tool
1516
1602
  .command('create-mcp')
1517
1603
  .description('Create an MCP_TOOL policy: monitor or block MCP server tool calls.')
1518
- .requiredOption('--name <name>', 'Policy name (required)')
1519
- .requiredOption('--mcp-server <server>', 'MCP server name as shown by `policy tool mcp-servers` (e.g. linear, github). Resolved to its canonical group automatically.')
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.')
1520
1607
  .option('--mcp-tool <tool>', 'Specific tool name on the MCP server (e.g. create_issue). Mutually exclusive with --mcp-action-type.')
1521
1608
  .option('--mcp-action-type <type>', `Match by tool action type: ${MCP_ACTION_TYPES.join(' | ')}. Mutually exclusive with --mcp-tool.`)
1522
- .requiredOption('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
1523
- .option('--description <text>', 'Human-readable description')
1524
- .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.')
1525
1612
  .option('--group <names|ids>', 'Comma-separated user group names or IDs to scope this policy to')
1526
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)')
1527
1616
  .option('--json', 'Output raw JSON of the created policy')
1528
1617
  .addHelpText('after', `
1529
1618
  Examples:
1530
- Match a specific tool on a server:
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):
1531
1628
  $ unbound policy tool create-mcp --name "Block Linear writes" \\
1532
1629
  --mcp-server linear --mcp-tool create_issue --action BLOCK \\
1533
1630
  --custom-message "Issue creation is blocked. Contact admin."
1534
1631
 
1535
- Match all destructive tools on a server:
1632
+ # Match all destructive tools on a server:
1536
1633
  $ unbound policy tool create-mcp --name "Audit all destructive Slack" \\
1537
1634
  --mcp-server slack --mcp-action-type destructive --action AUDIT
1538
1635
 
@@ -1545,6 +1642,79 @@ Learn more: ${DOCS_TOOL}
1545
1642
  .action(async (opts) => {
1546
1643
  try {
1547
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
+
1548
1718
  const action = normalizeAction(opts.action, TOOL_ACTIONS, '--action');
1549
1719
  if ((action === 'BLOCK' || action === 'WARN') && !opts.customMessage) {
1550
1720
  throw new Error('--custom-message is required when --action is BLOCK or WARN.');
@@ -1575,6 +1745,9 @@ Learn more: ${DOCS_TOOL}
1575
1745
  enabled: !opts.disabled,
1576
1746
  config: {},
1577
1747
  };
1748
+ if (opts.config) {
1749
+ body.config = parseJsonOrThrow(opts.config);
1750
+ }
1578
1751
  if (hasTool) body.mcp_tool = opts.mcpTool;
1579
1752
  if (hasActionType) body.mcp_tool_action_type = opts.mcpActionType;
1580
1753
  if (opts.customMessage) body.custom_message = opts.customMessage;
@@ -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
- return runBatch(resolvedTools, (tool) => {
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
- return runBatch(resolvedTools, (tool) => {
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 = {