unbound-cli 1.16.12 → 1.16.13
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/package.json +1 -1
- package/src/commands/policy.js +17 -18
- package/src/index.js +2 -5
- package/test/policy-help-model.test.js +132 -0
- package/test/policy-model-backcompat.test.js +42 -0
package/package.json
CHANGED
package/src/commands/policy.js
CHANGED
|
@@ -381,8 +381,8 @@ function registerTopLevel(policy) {
|
|
|
381
381
|
// ---- list ----
|
|
382
382
|
policy
|
|
383
383
|
.command('list')
|
|
384
|
-
.description('List Cost
|
|
385
|
-
.option('--type <type>', 'Filter by type: COST
|
|
384
|
+
.description('List Cost and Security policies. For Tool policies use `policy tool list`.')
|
|
385
|
+
.option('--type <type>', 'Filter by type: COST or SECURITY')
|
|
386
386
|
.option('--enabled', 'Show only enabled policies')
|
|
387
387
|
.option('--search <term>', 'Search policies by name')
|
|
388
388
|
.option('--json', 'Output raw JSON instead of a table')
|
|
@@ -443,7 +443,7 @@ Learn more: ${DOCS_POLICIES}
|
|
|
443
443
|
// ---- get ----
|
|
444
444
|
policy
|
|
445
445
|
.command('get <id>')
|
|
446
|
-
.description('Get details of a single Cost
|
|
446
|
+
.description('Get details of a single Cost or Security policy. For Tool policies use `policy tool get`.')
|
|
447
447
|
.option('--json', 'Output raw JSON')
|
|
448
448
|
.addHelpText('after', `
|
|
449
449
|
Examples:
|
|
@@ -468,7 +468,7 @@ Examples:
|
|
|
468
468
|
// ---- delete ----
|
|
469
469
|
policy
|
|
470
470
|
.command('delete <id>')
|
|
471
|
-
.description('Delete a Cost
|
|
471
|
+
.description('Delete a Cost or Security policy. For Tool policies use `policy tool delete`.')
|
|
472
472
|
.option('--yes', 'Skip confirmation prompt')
|
|
473
473
|
.addHelpText('after', `
|
|
474
474
|
Examples:
|
|
@@ -881,8 +881,10 @@ Learn more: ${DOCS_COST}
|
|
|
881
881
|
// ============================================================================
|
|
882
882
|
|
|
883
883
|
function registerModel(policy) {
|
|
884
|
+
// Model policies only take effect on the paused LLM-gateway path, so they are hidden
|
|
885
|
+
// from help. The commands stay functional so existing scripts keep working.
|
|
884
886
|
const model = policy
|
|
885
|
-
.command('model')
|
|
887
|
+
.command('model', { hidden: true })
|
|
886
888
|
.description('Manage Model policies. Control which AI models are available to a user group.')
|
|
887
889
|
.addHelpText('after', `
|
|
888
890
|
Model policies restrict which AI models users can invoke. Choose one of:
|
|
@@ -899,7 +901,7 @@ Subcommands:
|
|
|
899
901
|
`);
|
|
900
902
|
|
|
901
903
|
model
|
|
902
|
-
.command('list')
|
|
904
|
+
.command('list', { hidden: true })
|
|
903
905
|
.description('List all Model policies.')
|
|
904
906
|
.option('--json', 'Output raw JSON')
|
|
905
907
|
.action(async (opts) => {
|
|
@@ -930,7 +932,7 @@ Subcommands:
|
|
|
930
932
|
});
|
|
931
933
|
|
|
932
934
|
model
|
|
933
|
-
.command('create')
|
|
935
|
+
.command('create', { hidden: true })
|
|
934
936
|
.description('Create a new Model policy.')
|
|
935
937
|
.requiredOption('--name <name>', 'Policy name (required)')
|
|
936
938
|
.option('--all-models', 'Allow all models (use with --excluded for exceptions)')
|
|
@@ -1018,7 +1020,7 @@ Learn more: ${DOCS_MODEL}
|
|
|
1018
1020
|
});
|
|
1019
1021
|
|
|
1020
1022
|
model
|
|
1021
|
-
.command('update <id>')
|
|
1023
|
+
.command('update <id>', { hidden: true })
|
|
1022
1024
|
.description('Update an existing Model policy. Only provided fields are changed.')
|
|
1023
1025
|
.option('--name <name>', 'New name')
|
|
1024
1026
|
.option('--all-models', 'Replace config with "allow all"')
|
|
@@ -1319,7 +1321,7 @@ function registerTool(policy) {
|
|
|
1319
1321
|
.command('tool')
|
|
1320
1322
|
.description('Manage Tool policies: controls for shell commands (TERMINAL_COMMAND) and MCP tool calls (MCP_TOOL).')
|
|
1321
1323
|
.addHelpText('after', `
|
|
1322
|
-
Tool policies live in a separate backend table from Cost/
|
|
1324
|
+
Tool policies live in a separate backend table from Cost/Security
|
|
1323
1325
|
policies and are reached via /api/v1/command-policies/.
|
|
1324
1326
|
|
|
1325
1327
|
Learn more: ${DOCS_TOOL}
|
|
@@ -1972,9 +1974,9 @@ function registerLegacy(policy) {
|
|
|
1972
1974
|
|
|
1973
1975
|
policy
|
|
1974
1976
|
.command('create')
|
|
1975
|
-
.description('Create a Cost/
|
|
1977
|
+
.description('Create a Cost/Security policy. Prefer `policy cost|security create` for type-specific flags.')
|
|
1976
1978
|
.requiredOption('--name <name>', 'Policy name (required)')
|
|
1977
|
-
.requiredOption('--type <type>', 'Policy type: COST
|
|
1979
|
+
.requiredOption('--type <type>', 'Policy type: COST or SECURITY (required)')
|
|
1978
1980
|
.requiredOption('--config <json>', 'Policy configuration as a JSON string')
|
|
1979
1981
|
.option('--enabled', 'Enable the policy (default: true)', true)
|
|
1980
1982
|
.option('--no-enabled', 'Create the policy in disabled state')
|
|
@@ -1984,7 +1986,6 @@ function registerLegacy(policy) {
|
|
|
1984
1986
|
.addHelpText('after', `
|
|
1985
1987
|
Advanced / power-user escape hatch. For guided flag-based creation prefer:
|
|
1986
1988
|
$ unbound policy cost create --help
|
|
1987
|
-
$ unbound policy model create --help
|
|
1988
1989
|
$ unbound policy security create --help
|
|
1989
1990
|
$ unbound policy tool create-terminal --help (TOOL policies go through this path)
|
|
1990
1991
|
$ unbound policy tool create-mcp --help
|
|
@@ -2030,7 +2031,7 @@ Learn more: ${DOCS_POLICIES}
|
|
|
2030
2031
|
|
|
2031
2032
|
policy
|
|
2032
2033
|
.command('update <id>')
|
|
2033
|
-
.description('Update a Cost/
|
|
2034
|
+
.description('Update a Cost/Security policy via raw config. Prefer `policy cost|security update`.')
|
|
2034
2035
|
.option('--name <name>', 'New name')
|
|
2035
2036
|
.option('--config <json>', 'Replace config with raw JSON')
|
|
2036
2037
|
.option('--enabled', 'Enable')
|
|
@@ -2077,18 +2078,17 @@ Learn more: ${DOCS_POLICIES}
|
|
|
2077
2078
|
function register(program) {
|
|
2078
2079
|
const policy = program
|
|
2079
2080
|
.command('policy')
|
|
2080
|
-
.description('Manage policies.
|
|
2081
|
+
.description('Manage policies. Three types: cost, security, tool.')
|
|
2081
2082
|
.addHelpText('after', `
|
|
2082
|
-
Unbound has
|
|
2083
|
+
Unbound has three policy types. Each has its own subcommand for guided
|
|
2083
2084
|
flag-based create/update. Tool policies live in a separate backend table
|
|
2084
2085
|
and are reached via \`unbound policy tool\`.
|
|
2085
2086
|
|
|
2086
2087
|
cost Monthly budget limits per user group. ${DOCS_COST}
|
|
2087
|
-
model Control which AI models are available. ${DOCS_MODEL}
|
|
2088
2088
|
security Guardrails (PII, secrets), routing rules. ${DOCS_SECURITY}
|
|
2089
2089
|
tool Shell command and MCP tool controls. ${DOCS_TOOL}
|
|
2090
2090
|
|
|
2091
|
-
Generic commands (Cost/
|
|
2091
|
+
Generic commands (Cost/Security only):
|
|
2092
2092
|
list List policies, optionally filtered by type
|
|
2093
2093
|
get <id> Get policy details
|
|
2094
2094
|
delete <id> Delete a policy
|
|
@@ -2098,7 +2098,6 @@ Generic commands (Cost/Model/Security only):
|
|
|
2098
2098
|
Examples:
|
|
2099
2099
|
$ unbound policy form-data # see available names
|
|
2100
2100
|
$ unbound policy cost create --name "Eng Budget" --monthly-budget 1000 --group engg
|
|
2101
|
-
$ unbound policy model create --name "No Opus" --all-models --excluded claude-3-opus
|
|
2102
2101
|
$ unbound policy security create --name "Block PII" --sub-type guardrails --guardrail PII:BLOCK
|
|
2103
2102
|
$ unbound policy tool create-terminal --name "Block rm -rf" --command-family filesystem \\
|
|
2104
2103
|
--field command='rm -rf*' --action BLOCK --custom-message "Blocked by policy"
|
package/src/index.js
CHANGED
|
@@ -113,12 +113,12 @@ MDM AI TOOLS DISCOVERY
|
|
|
113
113
|
$ unbound discover status Show scan schedule and logs
|
|
114
114
|
|
|
115
115
|
POLICY MANAGEMENT
|
|
116
|
-
Unbound has
|
|
116
|
+
Unbound has three policy types. Each type has its own subcommand with guided flags.
|
|
117
117
|
Docs: https://docs.getunbound.ai/policies
|
|
118
118
|
|
|
119
119
|
$ unbound policy Overview and subcommand list
|
|
120
120
|
$ unbound policy form-data Reference data (user groups, models, guardrails, etc.)
|
|
121
|
-
$ unbound policy list List Cost/
|
|
121
|
+
$ unbound policy list List Cost/Security policies
|
|
122
122
|
$ unbound policy get <id> View a policy's details
|
|
123
123
|
$ unbound policy delete <id> Delete a policy
|
|
124
124
|
$ unbound policy effective <id> View effective policies for a user or group
|
|
@@ -126,9 +126,6 @@ POLICY MANAGEMENT
|
|
|
126
126
|
Cost policies — monthly budget limits per user group:
|
|
127
127
|
$ unbound policy cost create --name "Eng Budget" --monthly-budget 1000 --group engg
|
|
128
128
|
|
|
129
|
-
Model policies — control which AI models are available:
|
|
130
|
-
$ unbound policy model create --name "No Opus" --all-models --excluded claude-3-opus
|
|
131
|
-
|
|
132
129
|
Security policies — guardrails for PII/secrets and routing rules:
|
|
133
130
|
$ unbound policy security create --name "Block PII" --sub-type guardrails --guardrail PII:BLOCK
|
|
134
131
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { Command } = require('commander');
|
|
6
|
+
const config = require('../src/config');
|
|
7
|
+
const api = require('../src/api');
|
|
8
|
+
const output = require('../src/output');
|
|
9
|
+
const { register } = require('../src/commands/policy');
|
|
10
|
+
|
|
11
|
+
// Model policies only take effect on the paused LLM-gateway path, so help must
|
|
12
|
+
// advertise three policy types (cost, security, tool) and must not mention or
|
|
13
|
+
// offer model policies. The `policy model` command family stays functional
|
|
14
|
+
// (hidden, not removed) so existing scripts don't break.
|
|
15
|
+
|
|
16
|
+
function policyCommand() {
|
|
17
|
+
const program = new Command();
|
|
18
|
+
register(program);
|
|
19
|
+
return program.commands.find((c) => c.name() === 'policy');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fullHelp(cmd) {
|
|
23
|
+
let buf = '';
|
|
24
|
+
cmd.configureOutput({ writeOut: (s) => { buf += s; } });
|
|
25
|
+
cmd.outputHelp();
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Render real CLI help by running the binary, so the top-level help in
|
|
30
|
+
// src/index.js is exercised, not just the separately registered policy command.
|
|
31
|
+
const CLI = path.join(__dirname, '..', 'src', 'index.js');
|
|
32
|
+
function cliHelp(args) {
|
|
33
|
+
const r = spawnSync('node', [CLI, ...args], { encoding: 'utf8' });
|
|
34
|
+
assert.equal(r.status, 0, `\`${args.join(' ')}\` should exit 0\n${r.stderr}`);
|
|
35
|
+
return r.stdout;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('policy help advertises three policy types, not four', () => {
|
|
39
|
+
const help = fullHelp(policyCommand());
|
|
40
|
+
assert.ok(help.includes('three policy types'), 'help should say "three policy types"');
|
|
41
|
+
assert.ok(!help.includes('four policy types'), 'help must not say "four policy types"');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('policy help no longer advertises model policies', () => {
|
|
45
|
+
const help = fullHelp(policyCommand());
|
|
46
|
+
assert.ok(!help.includes('Control which AI models are available'), 'model type row must be gone');
|
|
47
|
+
assert.ok(!help.includes('policy model create'), 'model example must be gone');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('policy help does not list model as a subcommand', () => {
|
|
51
|
+
const help = fullHelp(policyCommand());
|
|
52
|
+
assert.ok(!/\n\s*model\s/.test(help), 'model must be hidden from the subcommand list');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// The generic list/get/delete/create/update commands share the `policy --help`
|
|
56
|
+
// screen, so their descriptions must not advertise model either.
|
|
57
|
+
test('policy generic command descriptions drop Model', () => {
|
|
58
|
+
const help = fullHelp(policyCommand());
|
|
59
|
+
assert.ok(!help.includes('Cost, Model'), 'generic descriptions must not say "Cost, Model"');
|
|
60
|
+
assert.ok(!help.includes('Cost/Model'), 'generic descriptions must not say "Cost/Model"');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('top-level `unbound --help` advertises three policy types, not four', () => {
|
|
64
|
+
const help = cliHelp(['--help']);
|
|
65
|
+
assert.ok(help.includes('three policy types'), 'top-level help should say "three policy types"');
|
|
66
|
+
assert.ok(!help.includes('four policy types'), 'top-level help must not say "four policy types"');
|
|
67
|
+
assert.ok(!help.includes('policy model create'), 'top-level help must not show a model example');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('rendered `policy --help` names three types and drops Model', () => {
|
|
71
|
+
const help = cliHelp(['policy', '--help']);
|
|
72
|
+
assert.ok(help.includes('Three types'), 'policy help should say "Three types"');
|
|
73
|
+
assert.ok(!help.includes('Four types'), 'policy help must not say "Four types"');
|
|
74
|
+
assert.ok(!help.includes('Cost, Model') && !help.includes('Cost/Model'), 'no Model in generic descriptions');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('policy model command family remains registered and hidden', () => {
|
|
78
|
+
const policy = policyCommand();
|
|
79
|
+
const model = policy.commands.find((c) => c.name() === 'model');
|
|
80
|
+
assert.ok(model, 'model command must still exist');
|
|
81
|
+
assert.equal(model._hidden, true, 'model command must be hidden');
|
|
82
|
+
for (const sub of ['list', 'create', 'update']) {
|
|
83
|
+
const m = model.commands.find((c) => c.name() === sub);
|
|
84
|
+
assert.ok(m, `model ${sub} must still exist`);
|
|
85
|
+
assert.equal(m._hidden, true, `model ${sub} must be hidden`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Hidden must not mean broken: a hidden model subcommand still parses and runs
|
|
90
|
+
// its action against the backend, so existing scripts keep working.
|
|
91
|
+
test('hidden `policy model list` still executes and calls the backend', async () => {
|
|
92
|
+
const orig = { isLoggedIn: config.isLoggedIn, get: api.get, info: output.info, table: output.table, error: output.error, json: output.json, exitCode: process.exitCode };
|
|
93
|
+
let reachedApi = false;
|
|
94
|
+
let sentType = null;
|
|
95
|
+
config.isLoggedIn = () => true;
|
|
96
|
+
api.get = async (_path, { query } = {}) => { reachedApi = true; sentType = query && query.type; return { policies: [] }; };
|
|
97
|
+
output.info = () => {};
|
|
98
|
+
output.table = () => {};
|
|
99
|
+
output.error = () => {};
|
|
100
|
+
output.json = () => {};
|
|
101
|
+
try {
|
|
102
|
+
const program = new Command();
|
|
103
|
+
register(program);
|
|
104
|
+
await program.parseAsync(['node', 'x', 'policy', 'model', 'list']);
|
|
105
|
+
assert.equal(reachedApi, true, 'hidden model list must reach the backend');
|
|
106
|
+
assert.equal(sentType, 'MODEL', 'model list must query type MODEL');
|
|
107
|
+
} finally {
|
|
108
|
+
config.isLoggedIn = orig.isLoggedIn;
|
|
109
|
+
api.get = orig.get;
|
|
110
|
+
output.info = orig.info;
|
|
111
|
+
output.table = orig.table;
|
|
112
|
+
output.error = orig.error;
|
|
113
|
+
output.json = orig.json;
|
|
114
|
+
process.exitCode = orig.exitCode;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// The legacy `policy create --help` escape-hatch list is a visible help surface;
|
|
119
|
+
// it must not steer a user to `policy model create`, which never applies on the
|
|
120
|
+
// hooks path — the exact failure this fix is about.
|
|
121
|
+
test('`policy create --help` escape hatch no longer points at model policies', () => {
|
|
122
|
+
const help = cliHelp(['policy', 'create', '--help']);
|
|
123
|
+
assert.ok(!help.includes('policy model create'), 'escape hatch must not suggest `policy model create`');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// `policy tool --help` overview names the other policy types; it must say
|
|
127
|
+
// Cost/Security, not Cost/Model/Security.
|
|
128
|
+
test('`policy tool --help` names Cost/Security, not Model', () => {
|
|
129
|
+
const help = cliHelp(['policy', 'tool', '--help']);
|
|
130
|
+
assert.ok(!help.includes('Cost/Model/Security'), 'tool help must not say "Cost/Model/Security"');
|
|
131
|
+
assert.ok(help.includes('Cost/Security'), 'tool help should say "Cost/Security"');
|
|
132
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const config = require('../src/config');
|
|
5
|
+
const api = require('../src/api');
|
|
6
|
+
const output = require('../src/output');
|
|
7
|
+
const { register } = require('../src/commands/policy');
|
|
8
|
+
|
|
9
|
+
// Backward-compat: `policy model ...` is hidden from help and MODEL is dropped
|
|
10
|
+
// from the advertised generic types, but the machinery must still accept MODEL.
|
|
11
|
+
// An existing script using the legacy raw `policy create --type MODEL` must keep
|
|
12
|
+
// working — de-advertising a type must not disable it.
|
|
13
|
+
|
|
14
|
+
function stub() {
|
|
15
|
+
const orig = {
|
|
16
|
+
isLoggedIn: config.isLoggedIn, post: api.post,
|
|
17
|
+
success: output.success, error: output.error, json: output.json, keyValue: output.keyValue,
|
|
18
|
+
exitCode: process.exitCode,
|
|
19
|
+
};
|
|
20
|
+
const seen = { reachedApi: false, sentType: null, errored: false };
|
|
21
|
+
config.isLoggedIn = () => true;
|
|
22
|
+
api.post = async (_p, { body }) => { seen.reachedApi = true; seen.sentType = body.type; return { id: 'p_1', ...body }; };
|
|
23
|
+
output.success = () => {};
|
|
24
|
+
output.error = () => { seen.errored = true; };
|
|
25
|
+
output.json = () => {};
|
|
26
|
+
output.keyValue = () => {};
|
|
27
|
+
return { seen, restore() { Object.assign(config, { isLoggedIn: orig.isLoggedIn }); api.post = orig.post; output.success = orig.success; output.error = orig.error; output.json = orig.json; output.keyValue = orig.keyValue; process.exitCode = orig.exitCode; } };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('legacy `policy create --type MODEL` still reaches the backend with type MODEL', async () => {
|
|
31
|
+
const { seen, restore } = stub();
|
|
32
|
+
try {
|
|
33
|
+
const program = new Command();
|
|
34
|
+
register(program);
|
|
35
|
+
await program.parseAsync(['node', 'x', 'policy', 'create', '--name', 'No Opus', '--type', 'MODEL', '--config', '{"all_models":true}']);
|
|
36
|
+
assert.equal(seen.reachedApi, true, 'MODEL create must still hit the policies endpoint');
|
|
37
|
+
assert.equal(seen.sentType, 'MODEL', 'the type sent must remain MODEL');
|
|
38
|
+
assert.equal(seen.errored, false, 'MODEL must not be rejected as an invalid type');
|
|
39
|
+
} finally {
|
|
40
|
+
restore();
|
|
41
|
+
}
|
|
42
|
+
});
|