unbound-cli 0.4.0 → 0.5.1
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 +76 -2
- package/README.md +82 -7
- package/package.json +1 -1
- package/src/commands/onboard.js +6 -3
- package/src/commands/policy.js +1704 -212
- package/src/commands/setup.js +38 -20
- package/src/index.js +26 -7
package/src/commands/setup.js
CHANGED
|
@@ -90,8 +90,11 @@ function buildSetupCommand(scriptPath, args) {
|
|
|
90
90
|
/**
|
|
91
91
|
* Runs a Python setup script from the setup repo with inherited stdio (live output).
|
|
92
92
|
*/
|
|
93
|
-
function runSetupScript(scriptPath, apiKey, { clear = false } = {}) {
|
|
94
|
-
|
|
93
|
+
function runSetupScript(scriptPath, apiKey, { clear = false, backendUrl, frontendUrl } = {}) {
|
|
94
|
+
let args = `--api-key ${shellEscape(apiKey)}`;
|
|
95
|
+
if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
|
|
96
|
+
if (frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
|
|
97
|
+
if (clear) args += ' --clear';
|
|
95
98
|
console.log('');
|
|
96
99
|
try {
|
|
97
100
|
execSync(buildSetupCommand(scriptPath, args), { stdio: 'inherit' });
|
|
@@ -177,6 +180,8 @@ function register(program) {
|
|
|
177
180
|
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
178
181
|
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
179
182
|
.option('--all', 'Set up the default bundle: Cursor, Claude Code (hooks), Codex (hooks)')
|
|
183
|
+
.addOption(new Option('--backend-url <url>', 'Override backend URL for setup scripts (dev only)').hideHelp())
|
|
184
|
+
.addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
|
|
180
185
|
.addHelpText('after', `
|
|
181
186
|
Available tools:
|
|
182
187
|
cursor Cursor IDE
|
|
@@ -252,8 +257,11 @@ automatically to authenticate before proceeding.
|
|
|
252
257
|
const selectedTools = SETUP_TOOLS.filter(t => selected.includes(t.value));
|
|
253
258
|
console.log('');
|
|
254
259
|
|
|
260
|
+
let interactiveArgs = `--api-key ${shellEscape(apiKey)}`;
|
|
261
|
+
if (opts.backendUrl) interactiveArgs += ` --backend-url ${shellEscape(opts.backendUrl)}`;
|
|
262
|
+
if (opts.frontendUrl) interactiveArgs += ` --domain ${shellEscape(opts.frontendUrl)}`;
|
|
255
263
|
const ok = await runBatch(selectedTools, (tool) =>
|
|
256
|
-
runScriptPiped(tool.script,
|
|
264
|
+
runScriptPiped(tool.script, interactiveArgs)
|
|
257
265
|
);
|
|
258
266
|
if (!ok) return;
|
|
259
267
|
|
|
@@ -318,13 +326,13 @@ automatically to authenticate before proceeding.
|
|
|
318
326
|
const toolName = tools[0];
|
|
319
327
|
|
|
320
328
|
if (SETUP_TOOL_MAP[toolName]) {
|
|
321
|
-
runSetupScript(SETUP_TOOL_MAP[toolName].script, apiKey, { clear: opts.clear });
|
|
329
|
+
runSetupScript(SETUP_TOOL_MAP[toolName].script, apiKey, { clear: opts.clear, backendUrl: opts.backendUrl, frontendUrl: opts.frontendUrl });
|
|
322
330
|
} else if (MODE_TOOLS[toolName]) {
|
|
323
331
|
const mode = MODE_TOOLS[toolName];
|
|
324
332
|
if (opts.clear) {
|
|
325
333
|
// Clear both modes
|
|
326
|
-
runSetupScript(SETUP_TOOL_MAP[mode.subscription].script, apiKey, { clear: true });
|
|
327
|
-
runSetupScript(SETUP_TOOL_MAP[mode.gateway].script, apiKey, { clear: true });
|
|
334
|
+
runSetupScript(SETUP_TOOL_MAP[mode.subscription].script, apiKey, { clear: true, backendUrl: opts.backendUrl, frontendUrl: opts.frontendUrl });
|
|
335
|
+
runSetupScript(SETUP_TOOL_MAP[mode.gateway].script, apiKey, { clear: true, backendUrl: opts.backendUrl, frontendUrl: opts.frontendUrl });
|
|
328
336
|
} else {
|
|
329
337
|
let useSubscription = opts.subscription;
|
|
330
338
|
if (!opts.subscription && !opts.gateway) {
|
|
@@ -332,7 +340,7 @@ automatically to authenticate before proceeding.
|
|
|
332
340
|
useSubscription = choice === 'subscription';
|
|
333
341
|
}
|
|
334
342
|
const resolved = useSubscription ? mode.subscription : mode.gateway;
|
|
335
|
-
runSetupScript(SETUP_TOOL_MAP[resolved].script, apiKey, {});
|
|
343
|
+
runSetupScript(SETUP_TOOL_MAP[resolved].script, apiKey, { backendUrl: opts.backendUrl, frontendUrl: opts.frontendUrl });
|
|
336
344
|
}
|
|
337
345
|
} else if (INSTRUCTION_TOOLS[toolName]) {
|
|
338
346
|
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
|
|
@@ -370,7 +378,10 @@ automatically to authenticate before proceeding.
|
|
|
370
378
|
// Run automated tools with spinners
|
|
371
379
|
if (resolvedScripts.length > 0) {
|
|
372
380
|
console.log('');
|
|
373
|
-
|
|
381
|
+
let args = `--api-key ${shellEscape(apiKey)}`;
|
|
382
|
+
if (opts.backendUrl) args += ` --backend-url ${shellEscape(opts.backendUrl)}`;
|
|
383
|
+
if (opts.frontendUrl) args += ` --domain ${shellEscape(opts.frontendUrl)}`;
|
|
384
|
+
if (opts.clear) args += ' --clear';
|
|
374
385
|
const ok = await runBatch(resolvedScripts, (tool) =>
|
|
375
386
|
runScriptPiped(tool.script, args)
|
|
376
387
|
, { clear: opts.clear });
|
|
@@ -409,7 +420,6 @@ automatically to authenticate before proceeding.
|
|
|
409
420
|
.requiredOption('--admin-api-key <key>', 'Admin API key for MDM enrollment')
|
|
410
421
|
.option('--clear', 'Remove Unbound configuration for the specified tools')
|
|
411
422
|
.option('--all', 'Set up all available tools')
|
|
412
|
-
.addOption(new Option('--url <url>', 'Override backend URL').hideHelp())
|
|
413
423
|
.addHelpText('after', `
|
|
414
424
|
Available tools:
|
|
415
425
|
cursor Cursor IDE
|
|
@@ -429,18 +439,22 @@ Examples:
|
|
|
429
439
|
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
430
440
|
$ sudo unbound setup mdm --admin-api-key KEY --clear cursor codex-subscription
|
|
431
441
|
`)
|
|
432
|
-
.action(async (tools, opts) => {
|
|
442
|
+
.action(async (tools, opts, command) => {
|
|
433
443
|
try {
|
|
434
444
|
checkRoot();
|
|
445
|
+
// --all and --clear are defined on both this command and the parent `setup` command;
|
|
446
|
+
// --backend-url and --frontend-url are defined only on the parent `setup` command.
|
|
447
|
+
// Use optsWithGlobals() so all four work regardless of position relative to `mdm`.
|
|
448
|
+
const globalOpts = command.optsWithGlobals();
|
|
435
449
|
|
|
436
|
-
if (
|
|
450
|
+
if (globalOpts.all && tools.length > 0) {
|
|
437
451
|
output.error('Cannot combine --all with specific tool names. Use one or the other.');
|
|
438
452
|
process.exitCode = 1;
|
|
439
453
|
return;
|
|
440
454
|
}
|
|
441
455
|
|
|
442
456
|
let toolNames;
|
|
443
|
-
if (
|
|
457
|
+
if (globalOpts.all) {
|
|
444
458
|
toolNames = MDM_ALL_TOOLS;
|
|
445
459
|
} else if (tools.length > 0) {
|
|
446
460
|
toolNames = tools;
|
|
@@ -476,20 +490,21 @@ Examples:
|
|
|
476
490
|
|
|
477
491
|
const mdmArgs = (tool) => {
|
|
478
492
|
let args = `--api-key ${shellEscape(opts.adminApiKey)}`;
|
|
479
|
-
if (
|
|
480
|
-
if (
|
|
493
|
+
if (globalOpts.backendUrl) args += ` --backend-url ${shellEscape(globalOpts.backendUrl)}`;
|
|
494
|
+
if (globalOpts.frontendUrl) args += ` --domain ${shellEscape(globalOpts.frontendUrl)}`;
|
|
495
|
+
if (globalOpts.clear) args += ' --clear';
|
|
481
496
|
return args;
|
|
482
497
|
};
|
|
483
498
|
|
|
484
499
|
const ok = await runBatch(
|
|
485
500
|
resolvedTools,
|
|
486
501
|
(tool) => runScriptPiped(tool.script, mdmArgs(tool)),
|
|
487
|
-
{ clear:
|
|
502
|
+
{ clear: globalOpts.clear }
|
|
488
503
|
);
|
|
489
504
|
if (!ok) return;
|
|
490
505
|
|
|
491
506
|
console.log('');
|
|
492
|
-
output.success(
|
|
507
|
+
output.success(globalOpts.clear ? 'All tools cleared' : 'All tools configured');
|
|
493
508
|
} catch (err) {
|
|
494
509
|
output.error(err.message);
|
|
495
510
|
process.exitCode = 1;
|
|
@@ -502,9 +517,11 @@ Examples:
|
|
|
502
517
|
* Assumes the caller has already ensured the user is logged in.
|
|
503
518
|
* Returns true on success, false on failure.
|
|
504
519
|
*/
|
|
505
|
-
async function runSetupAllBundle(apiKey) {
|
|
520
|
+
async function runSetupAllBundle(apiKey, { backendUrl, frontendUrl } = {}) {
|
|
506
521
|
const resolvedTools = ALL_TOOLS.map(name => ({ name, ...SETUP_TOOL_MAP[name] }));
|
|
507
|
-
|
|
522
|
+
let args = `--api-key ${shellEscape(apiKey)}`;
|
|
523
|
+
if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
|
|
524
|
+
if (frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
|
|
508
525
|
return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
|
|
509
526
|
}
|
|
510
527
|
|
|
@@ -513,10 +530,11 @@ async function runSetupAllBundle(apiKey) {
|
|
|
513
530
|
* Caller must ensure the process is running as root.
|
|
514
531
|
* Returns true on success, false on failure.
|
|
515
532
|
*/
|
|
516
|
-
async function runMdmSetupAllBundle(adminApiKey, {
|
|
533
|
+
async function runMdmSetupAllBundle(adminApiKey, { backendUrl, frontendUrl } = {}) {
|
|
517
534
|
const resolvedTools = MDM_ALL_TOOLS.map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
518
535
|
let args = `--api-key ${shellEscape(adminApiKey)}`;
|
|
519
|
-
if (
|
|
536
|
+
if (backendUrl) args += ` --backend-url ${shellEscape(backendUrl)}`;
|
|
537
|
+
if (frontendUrl) args += ` --domain ${shellEscape(frontendUrl)}`;
|
|
520
538
|
return runBatch(resolvedTools, (tool) => runScriptPiped(tool.script, args));
|
|
521
539
|
}
|
|
522
540
|
|
package/src/index.js
CHANGED
|
@@ -73,14 +73,33 @@ MDM AI TOOLS DISCOVERY
|
|
|
73
73
|
$ unbound discover status Show scan schedule and logs
|
|
74
74
|
|
|
75
75
|
POLICY MANAGEMENT
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
$ unbound policy
|
|
80
|
-
$ unbound policy
|
|
76
|
+
Unbound has four policy types. Each type has its own subcommand with guided flags.
|
|
77
|
+
Docs: https://docs.getunbound.ai/policies
|
|
78
|
+
|
|
79
|
+
$ unbound policy Overview and subcommand list
|
|
80
|
+
$ unbound policy form-data Reference data (user groups, models, guardrails, etc.)
|
|
81
|
+
$ unbound policy list List Cost/Model/Security policies
|
|
82
|
+
$ unbound policy get <id> View a policy's details
|
|
81
83
|
$ unbound policy delete <id> Delete a policy
|
|
82
|
-
$ unbound policy effective <id> View effective policies for a user
|
|
83
|
-
|
|
84
|
+
$ unbound policy effective <id> View effective policies for a user or group
|
|
85
|
+
|
|
86
|
+
Cost policies — monthly budget limits per user group:
|
|
87
|
+
$ unbound policy cost create --name "Eng Budget" --monthly-budget 1000 --group engg
|
|
88
|
+
|
|
89
|
+
Model policies — control which AI models are available:
|
|
90
|
+
$ unbound policy model create --name "No Opus" --all-models --excluded claude-3-opus
|
|
91
|
+
|
|
92
|
+
Security policies — guardrails for PII/secrets and routing rules:
|
|
93
|
+
$ unbound policy security create --name "Block PII" --sub-type guardrails --guardrail PII:BLOCK
|
|
94
|
+
|
|
95
|
+
Tool policies — shell command and MCP tool controls (separate backend):
|
|
96
|
+
$ unbound policy tool list
|
|
97
|
+
$ unbound policy tool create-terminal --name "Block rm -rf" --command-family filesystem \\
|
|
98
|
+
--field command='rm -rf*' --action BLOCK --custom-message "Destructive command blocked."
|
|
99
|
+
$ unbound policy tool create-mcp --name "Audit Linear writes" --mcp-server Linear \\
|
|
100
|
+
--mcp-action-type write --action AUDIT
|
|
101
|
+
$ unbound policy tool families Discover terminal command families
|
|
102
|
+
$ unbound policy tool mcp-servers Discover MCP servers and their tools
|
|
84
103
|
|
|
85
104
|
USER & GROUP MANAGEMENT
|
|
86
105
|
$ unbound users list List organization members
|