unbound-cli 0.3.1 → 0.5.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/LOCAL_DEV.md +46 -2
- package/README.md +97 -7
- package/package.json +1 -1
- package/src/commands/discover.js +32 -10
- package/src/commands/onboard.js +134 -0
- package/src/commands/policy.js +1704 -212
- package/src/commands/setup.js +53 -3
- package/src/index.js +32 -7
package/LOCAL_DEV.md
CHANGED
|
@@ -68,11 +68,44 @@ node src/index.js login --base-url http://localhost:8000 --api-key <your-key>
|
|
|
68
68
|
node src/index.js whoami
|
|
69
69
|
node src/index.js status
|
|
70
70
|
|
|
71
|
-
# Policies
|
|
71
|
+
# Policies — overview
|
|
72
|
+
node src/index.js policy # overview + docs links
|
|
73
|
+
node src/index.js policy form-data # reference data (must produce output — bugfix)
|
|
72
74
|
node src/index.js policy list
|
|
73
75
|
node src/index.js policy list --type SECURITY --json
|
|
74
76
|
node src/index.js policy get 1
|
|
75
|
-
node src/index.js policy
|
|
77
|
+
node src/index.js policy effective 1
|
|
78
|
+
|
|
79
|
+
# Policy type subcommands
|
|
80
|
+
node src/index.js policy cost --help
|
|
81
|
+
node src/index.js policy model --help
|
|
82
|
+
node src/index.js policy security --help
|
|
83
|
+
node src/index.js policy tool --help
|
|
84
|
+
|
|
85
|
+
# Cost policies
|
|
86
|
+
node src/index.js policy cost list
|
|
87
|
+
node src/index.js policy cost create --name "Test Cost" --monthly-budget 500 --group engg
|
|
88
|
+
node src/index.js policy cost update 5 --monthly-budget 750
|
|
89
|
+
|
|
90
|
+
# Model policies
|
|
91
|
+
node src/index.js policy model create --name "No Opus" --all-models --excluded claude-3-opus
|
|
92
|
+
node src/index.js policy model create --name "Sonnet Only" --allowed claude-3-5-sonnet
|
|
93
|
+
|
|
94
|
+
# Security policies
|
|
95
|
+
node src/index.js policy security create --name "Block PII" --sub-type guardrails \
|
|
96
|
+
--guardrail PII:BLOCK --guardrail Secrets:REDACT
|
|
97
|
+
node src/index.js policy security create --name "Route 429s" --sub-type error-code-routing \
|
|
98
|
+
--error-route 429:gpt-4:claude-3-5-sonnet
|
|
99
|
+
|
|
100
|
+
# Tool policies (separate backend: /api/v1/command-policies/)
|
|
101
|
+
node src/index.js policy tool list
|
|
102
|
+
node src/index.js policy tool families
|
|
103
|
+
node src/index.js policy tool mcp-servers
|
|
104
|
+
node src/index.js policy tool create-terminal --name "Block rm -rf" \
|
|
105
|
+
--command-family filesystem --field command='rm -rf*' --action BLOCK \
|
|
106
|
+
--custom-message "Destructive command blocked."
|
|
107
|
+
node src/index.js policy tool create-mcp --name "Audit Linear writes" \
|
|
108
|
+
--mcp-server Linear --mcp-action-type write --action AUDIT
|
|
76
109
|
|
|
77
110
|
# Users
|
|
78
111
|
node src/index.js users list
|
|
@@ -90,6 +123,17 @@ node src/index.js tools approved
|
|
|
90
123
|
# Setup
|
|
91
124
|
node src/index.js setup cursor
|
|
92
125
|
node src/index.js setup claude-code
|
|
126
|
+
|
|
127
|
+
# Setup the default bundle (Cursor + Claude Code hooks + Codex hooks)
|
|
128
|
+
node src/index.js setup --all
|
|
129
|
+
node src/index.js setup --all --api-key <key>
|
|
130
|
+
|
|
131
|
+
# Onboarding (one command: login + setup --all + discover)
|
|
132
|
+
node src/index.js onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
133
|
+
sudo node src/index.js onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
134
|
+
|
|
135
|
+
# MDM onboarding (admin device enrollment, requires root)
|
|
136
|
+
sudo node src/index.js onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
93
137
|
```
|
|
94
138
|
|
|
95
139
|
## Unlink when done
|
package/README.md
CHANGED
|
@@ -30,6 +30,20 @@ unbound setup
|
|
|
30
30
|
unbound setup cursor
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Onboarding (one command)
|
|
34
|
+
|
|
35
|
+
For new users, the fastest path from install to a fully-configured device is the `onboard` command. It logs you in, installs the default tool bundle (Cursor, Claude Code hooks, Codex hooks), and runs device discovery — all in one step.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# End user
|
|
39
|
+
npm install -g unbound-cli
|
|
40
|
+
unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
41
|
+
|
|
42
|
+
# Admin enrolling a device via MDM (requires root)
|
|
43
|
+
sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The user API key and discovery API key are separate — discovery uses its own key that the CLI does not store. Run with `sudo` to let the discovery step scan all users on the device; without it, only the current user is scanned.
|
|
33
47
|
|
|
34
48
|
## Commands
|
|
35
49
|
|
|
@@ -51,6 +65,7 @@ Interactive batch setup:
|
|
|
51
65
|
| Command | Description |
|
|
52
66
|
|---------|-------------|
|
|
53
67
|
| `unbound setup` | Select and install multiple tools interactively |
|
|
68
|
+
| `unbound setup --all` | Install the default bundle: Cursor + Claude Code hooks + Codex hooks |
|
|
54
69
|
|
|
55
70
|
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
56
71
|
|
|
@@ -112,17 +127,92 @@ Scan a device for installed AI coding tools and report findings to Unbound. Uses
|
|
|
112
127
|
|
|
113
128
|
### Policies (Admin only)
|
|
114
129
|
|
|
130
|
+
Unbound has **four** policy types. Each has its own subcommand with guided flag-based create/update. Tool policies live on a separate backend endpoint but are reachable under the same `policy` command tree.
|
|
131
|
+
|
|
132
|
+
Docs: https://docs.getunbound.ai/policies
|
|
133
|
+
|
|
134
|
+
| Type | Subcommand | Purpose | Docs |
|
|
135
|
+
|---|---|---|---|
|
|
136
|
+
| **Cost** | `unbound policy cost` | Monthly budget limits per user group | https://docs.getunbound.ai/policies/cost-policies |
|
|
137
|
+
| **Model** | `unbound policy model` | Control which AI models are available | https://docs.getunbound.ai/policies/model-policies |
|
|
138
|
+
| **Security** | `unbound policy security` | Guardrails (PII, secrets), routing rules | https://docs.getunbound.ai/policies/security-policies |
|
|
139
|
+
| **Tool** | `unbound policy tool` | Shell command and MCP tool controls | https://docs.getunbound.ai/policies/tool-policies |
|
|
140
|
+
|
|
141
|
+
Generic commands (Cost/Model/Security only):
|
|
142
|
+
|
|
115
143
|
| Command | Description |
|
|
116
144
|
|---------|-------------|
|
|
117
|
-
| `unbound policy
|
|
118
|
-
| `unbound policy
|
|
119
|
-
| `unbound policy
|
|
120
|
-
| `unbound policy update <id>` | Update a policy |
|
|
145
|
+
| `unbound policy` | Overview of types and subcommands |
|
|
146
|
+
| `unbound policy list [--type COST\|MODEL\|SECURITY]` | List policies |
|
|
147
|
+
| `unbound policy get <id>` | Get a policy's details |
|
|
121
148
|
| `unbound policy delete <id>` | Delete a policy |
|
|
122
|
-
| `unbound policy form-data` |
|
|
123
|
-
| `unbound policy effective <id
|
|
149
|
+
| `unbound policy form-data` | Reference data: user groups, models, guardrails, tool types, command policies |
|
|
150
|
+
| `unbound policy effective <id> [--user\|--group]` | View effective policies for a user or group |
|
|
151
|
+
|
|
152
|
+
Cost policy examples:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Create a $1,000/month cap for the engg user group
|
|
156
|
+
unbound policy cost create --name "Eng Budget" --monthly-budget 1000 --group engg
|
|
157
|
+
|
|
158
|
+
# Change the budget
|
|
159
|
+
unbound policy cost update 5 --monthly-budget 1500
|
|
160
|
+
|
|
161
|
+
# List just cost policies
|
|
162
|
+
unbound policy cost list
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Model policy examples:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Allow only specific models
|
|
169
|
+
unbound policy model create --name "Sonnet Only" --allowed claude-3-5-sonnet
|
|
170
|
+
|
|
171
|
+
# Allow everything except specific models
|
|
172
|
+
unbound policy model create --name "No Opus" --all-models --excluded claude-3-opus
|
|
173
|
+
|
|
174
|
+
# List all model policies
|
|
175
|
+
unbound policy model list
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Security policy examples:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Block PII, redact secrets, for the engg group
|
|
182
|
+
unbound policy security create --name "Block PII" --sub-type guardrails \
|
|
183
|
+
--guardrail PII:BLOCK --guardrail Secrets:REDACT --group engg
|
|
184
|
+
|
|
185
|
+
# Default-route gpt-4 traffic to claude-3-5-sonnet
|
|
186
|
+
unbound policy security create --name "Prefer Sonnet" --sub-type default-routing \
|
|
187
|
+
--route gpt-4:claude-3-5-sonnet
|
|
188
|
+
|
|
189
|
+
# Fall back on 429s
|
|
190
|
+
unbound policy security create --name "429 Fallback" --sub-type error-code-routing \
|
|
191
|
+
--error-route 429:gpt-4:claude-3-5-sonnet
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Tool policy examples:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# See what command families and MCP servers are available
|
|
198
|
+
unbound policy tool families
|
|
199
|
+
unbound policy tool mcp-servers
|
|
200
|
+
|
|
201
|
+
# Block destructive shell commands
|
|
202
|
+
unbound policy tool create-terminal --name "Block rm -rf" --command-family filesystem \
|
|
203
|
+
--field command='rm -rf*' --action BLOCK --custom-message "Destructive command blocked."
|
|
204
|
+
|
|
205
|
+
# Audit Linear write operations via MCP
|
|
206
|
+
unbound policy tool create-mcp --name "Audit Linear writes" --mcp-server Linear \
|
|
207
|
+
--mcp-action-type write --action AUDIT
|
|
208
|
+
|
|
209
|
+
# List, get, delete
|
|
210
|
+
unbound policy tool list
|
|
211
|
+
unbound policy tool get <id>
|
|
212
|
+
unbound policy tool delete <id>
|
|
213
|
+
```
|
|
124
214
|
|
|
125
|
-
|
|
215
|
+
Before creating any policy, run `unbound policy form-data` to see available user group names, model names, guardrail names, and existing command policies. The CLI accepts names (e.g. `engg`, `claude-3-opus`, `PII`) and resolves them to backend IDs automatically. You can also pass numeric IDs directly.
|
|
126
216
|
|
|
127
217
|
### Users
|
|
128
218
|
|
package/package.json
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -13,6 +13,15 @@ function isRoot() {
|
|
|
13
13
|
return typeof process.getuid === 'function' && process.getuid() === 0;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Escapes a string for safe embedding in a shell command (single-quote wrap).
|
|
18
|
+
* Mirrors the helper in setup.js. Required because runDiscoveryScript uses
|
|
19
|
+
* spawn(cmd, { shell: true }) which routes the full command through bash.
|
|
20
|
+
*/
|
|
21
|
+
function shellEscape(str) {
|
|
22
|
+
return "'" + String(str).replace(/'/g, "'\\''") + "'";
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Downloads a bash script from the discovery repo and executes it with arguments.
|
|
18
27
|
* Uses stdio: 'inherit' so the script's output is shown live.
|
|
@@ -36,6 +45,26 @@ function runDiscoveryScript(scriptName, args) {
|
|
|
36
45
|
});
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Runs the main discovery scan (install.sh) with the given discovery key and domain.
|
|
50
|
+
* Emits a warning if not running as root (scan will be limited to the current user).
|
|
51
|
+
* Throws on failure. Used by both the `discover` command and the `onboard` command.
|
|
52
|
+
*/
|
|
53
|
+
async function runDiscoveryScan({ apiKey, domain = DEFAULT_DOMAIN }) {
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
throw new Error('Discovery API key is required.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isRoot()) {
|
|
59
|
+
output.warn('Running without root. Only scanning current user\'s tools.');
|
|
60
|
+
output.info('Run with sudo to scan all users on this device.');
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const args = `--api-key ${shellEscape(apiKey)} --domain ${shellEscape(domain)}`;
|
|
65
|
+
await runDiscoveryScript('install.sh', args);
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
function register(program) {
|
|
40
69
|
const discover = program
|
|
41
70
|
.command('discover')
|
|
@@ -72,14 +101,7 @@ Examples:
|
|
|
72
101
|
return;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
|
|
76
|
-
output.warn('Running without root. Only scanning current user\'s tools.');
|
|
77
|
-
output.info('Run with sudo to scan all users on this device.');
|
|
78
|
-
console.log('');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const args = `--api-key "${opts.apiKey}" --domain "${opts.domain}"`;
|
|
82
|
-
await runDiscoveryScript('install.sh', args);
|
|
104
|
+
await runDiscoveryScan({ apiKey: opts.apiKey, domain: opts.domain });
|
|
83
105
|
|
|
84
106
|
console.log('');
|
|
85
107
|
output.success('Discovery complete');
|
|
@@ -125,7 +147,7 @@ Examples:
|
|
|
125
147
|
return;
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
const args = `--api-key
|
|
150
|
+
const args = `--api-key ${shellEscape(opts.apiKey)} --domain ${shellEscape(opts.domain)}`;
|
|
129
151
|
await runDiscoveryScript('setup-scheduled-scan.sh', args);
|
|
130
152
|
} catch (err) {
|
|
131
153
|
output.error(err.message);
|
|
@@ -207,4 +229,4 @@ Examples:
|
|
|
207
229
|
});
|
|
208
230
|
}
|
|
209
231
|
|
|
210
|
-
module.exports = { register };
|
|
232
|
+
module.exports = { register, runDiscoveryScan };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { Option } = require('commander');
|
|
2
|
+
const config = require('../config');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
const { ensureLoggedIn } = require('../auth');
|
|
5
|
+
const { runSetupAllBundle, runMdmSetupAllBundle, checkRoot, ALL_TOOLS, MDM_ALL_TOOLS } = require('./setup');
|
|
6
|
+
const { runDiscoveryScan } = require('./discover');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DOMAIN = 'https://backend.getunbound.ai';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds the recovery-command suffix for partial-failure hints.
|
|
12
|
+
* Includes `--domain <value>` only when the user supplied a non-default domain,
|
|
13
|
+
* so users running against a custom backend get a copy-pastable command.
|
|
14
|
+
*/
|
|
15
|
+
function domainHintSuffix(domain) {
|
|
16
|
+
if (!domain || domain === DEFAULT_DOMAIN) return '';
|
|
17
|
+
return ` --domain ${domain}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function register(program) {
|
|
21
|
+
program
|
|
22
|
+
.command('onboard')
|
|
23
|
+
.description(
|
|
24
|
+
'One-step user onboarding: install the default AI tools bundle and run device discovery. ' +
|
|
25
|
+
'Runs `setup --all` followed by `discover` in a single command.'
|
|
26
|
+
)
|
|
27
|
+
.requiredOption('--api-key <key>', 'User API key (for tool setup and login)')
|
|
28
|
+
.requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
|
|
29
|
+
.option('--domain <url>', 'Backend URL for discovery', DEFAULT_DOMAIN)
|
|
30
|
+
.addHelpText('after', `
|
|
31
|
+
Runs the full onboarding flow for an end user:
|
|
32
|
+
1. Logs in with --api-key and stores credentials.
|
|
33
|
+
2. Installs the default tool bundle: ${ALL_TOOLS.join(', ')}.
|
|
34
|
+
3. Runs device discovery with --discovery-key.
|
|
35
|
+
|
|
36
|
+
The user API key and discovery API key are separate keys obtained from
|
|
37
|
+
different parts of the Unbound dashboard. Discovery uses its own key
|
|
38
|
+
that is not stored in the CLI config.
|
|
39
|
+
|
|
40
|
+
Run with sudo to let the discovery step scan all users on the device.
|
|
41
|
+
Without sudo, discovery only scans the current user.
|
|
42
|
+
|
|
43
|
+
For admin device enrollment via MDM, use \`unbound onboard-mdm\` instead.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
$ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
47
|
+
$ sudo unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
48
|
+
`)
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
let setupSucceeded = false;
|
|
51
|
+
try {
|
|
52
|
+
await ensureLoggedIn({ apiKey: opts.apiKey });
|
|
53
|
+
const apiKey = config.getApiKey();
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
output.info('Step 1/2: Installing tool bundle');
|
|
57
|
+
const ok = await runSetupAllBundle(apiKey);
|
|
58
|
+
if (!ok) return;
|
|
59
|
+
setupSucceeded = true;
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
output.info('Step 2/2: Running device discovery');
|
|
63
|
+
console.log('');
|
|
64
|
+
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: opts.domain });
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
output.success('Onboarding complete');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (!err.displayed) output.error(err.message);
|
|
70
|
+
if (setupSucceeded) {
|
|
71
|
+
const suffix = domainHintSuffix(opts.domain);
|
|
72
|
+
console.error(' Tool setup completed successfully — only discovery failed.');
|
|
73
|
+
console.error(` Re-run discovery only with: unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
74
|
+
}
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- MDM onboard (separate top-level command, mirrors `unbound onboard`) ---
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('onboard-mdm')
|
|
83
|
+
.description(
|
|
84
|
+
'One-step MDM onboarding: install the default MDM tool bundle and run device discovery. ' +
|
|
85
|
+
'Requires root. Used by organization admins to enroll devices via MDM.'
|
|
86
|
+
)
|
|
87
|
+
.requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
|
|
88
|
+
.requiredOption('--discovery-key <key>', 'Discovery API key (for device scan)')
|
|
89
|
+
.option('--domain <url>', 'Backend URL for discovery', DEFAULT_DOMAIN)
|
|
90
|
+
.addOption(new Option('--url <url>', 'Override backend URL for setup scripts').hideHelp())
|
|
91
|
+
.addHelpText('after', `
|
|
92
|
+
Runs the full MDM onboarding flow for device enrollment:
|
|
93
|
+
1. Installs the MDM tool bundle: ${MDM_ALL_TOOLS.join(', ')}.
|
|
94
|
+
2. Runs device discovery against all users on the device.
|
|
95
|
+
|
|
96
|
+
Both steps require root. The admin API key and discovery API key are
|
|
97
|
+
separate keys obtained from different parts of the Unbound admin dashboard.
|
|
98
|
+
|
|
99
|
+
For end-user onboarding (non-MDM), use \`unbound onboard\` instead.
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
$ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
103
|
+
`)
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
let setupSucceeded = false;
|
|
106
|
+
try {
|
|
107
|
+
checkRoot('onboard-mdm');
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
output.info('Step 1/2: Installing MDM tool bundle');
|
|
111
|
+
const ok = await runMdmSetupAllBundle(opts.adminApiKey, { url: opts.url });
|
|
112
|
+
if (!ok) return;
|
|
113
|
+
setupSucceeded = true;
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
output.info('Step 2/2: Running device discovery');
|
|
117
|
+
console.log('');
|
|
118
|
+
await runDiscoveryScan({ apiKey: opts.discoveryKey, domain: opts.domain });
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
output.success('MDM onboarding complete');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (!err.displayed) output.error(err.message);
|
|
124
|
+
if (setupSucceeded) {
|
|
125
|
+
const suffix = domainHintSuffix(opts.domain);
|
|
126
|
+
console.error(' MDM tool setup completed successfully — only discovery failed.');
|
|
127
|
+
console.error(` Re-run discovery only with: sudo unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
128
|
+
}
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { register };
|