unbound-cli 0.9.9 → 1.1.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/package.json +1 -1
- package/src/commands/policy.js +6 -3
- package/src/commands/setup.js +136 -1
- package/src/index.js +7 -2
- package/test/setup-args.test.js +44 -1
package/package.json
CHANGED
package/src/commands/policy.js
CHANGED
|
@@ -1503,7 +1503,7 @@ Learn more: ${DOCS_TOOL}
|
|
|
1503
1503
|
.command('create-mcp')
|
|
1504
1504
|
.description('Create an MCP_TOOL policy: monitor or block MCP server tool calls.')
|
|
1505
1505
|
.requiredOption('--name <name>', 'Policy name (required)')
|
|
1506
|
-
.requiredOption('--mcp-server <server>', 'MCP server name (e.g.
|
|
1506
|
+
.requiredOption('--mcp-server <server>', 'MCP server name as shown by `policy tool mcp-servers` (e.g. linear, github). Resolved to its canonical group automatically.')
|
|
1507
1507
|
.option('--mcp-tool <tool>', 'Specific tool name on the MCP server (e.g. create_issue). Mutually exclusive with --mcp-action-type.')
|
|
1508
1508
|
.option('--mcp-action-type <type>', `Match by tool action type: ${MCP_ACTION_TYPES.join(' | ')}. Mutually exclusive with --mcp-tool.`)
|
|
1509
1509
|
.requiredOption('--action <action>', `Action to take: ${TOOL_ACTIONS.join(' | ')}`)
|
|
@@ -1516,12 +1516,15 @@ Learn more: ${DOCS_TOOL}
|
|
|
1516
1516
|
Examples:
|
|
1517
1517
|
Match a specific tool on a server:
|
|
1518
1518
|
$ unbound policy tool create-mcp --name "Block Linear writes" \\
|
|
1519
|
-
--mcp-server
|
|
1519
|
+
--mcp-server linear --mcp-tool create_issue --action BLOCK \\
|
|
1520
1520
|
--custom-message "Issue creation is blocked. Contact admin."
|
|
1521
1521
|
|
|
1522
1522
|
Match all destructive tools on a server:
|
|
1523
1523
|
$ unbound policy tool create-mcp --name "Audit all destructive Slack" \\
|
|
1524
|
-
--mcp-server
|
|
1524
|
+
--mcp-server slack --mcp-action-type destructive --action AUDIT
|
|
1525
|
+
|
|
1526
|
+
The server name is resolved to its canonical group automatically, so pass the
|
|
1527
|
+
name exactly as listed by \`policy tool mcp-servers\` (matching is case-insensitive).
|
|
1525
1528
|
|
|
1526
1529
|
Discover valid MCP servers and their tools: unbound policy tool mcp-servers
|
|
1527
1530
|
Learn more: ${DOCS_TOOL}
|
package/src/commands/setup.js
CHANGED
|
@@ -7,6 +7,7 @@ const https = require('https');
|
|
|
7
7
|
const config = require('../config');
|
|
8
8
|
const output = require('../output');
|
|
9
9
|
const { ensureLoggedIn } = require('../auth');
|
|
10
|
+
const { confirm } = require('../utils');
|
|
10
11
|
|
|
11
12
|
const SETUP_BASE_URL = 'https://raw.githubusercontent.com/websentry-ai/setup/refs/heads/main';
|
|
12
13
|
|
|
@@ -59,6 +60,17 @@ const SETUP_TOOL_MAP = {
|
|
|
59
60
|
'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/setup.py' },
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the tool list for `setup --all`. Installing uses the subscription
|
|
65
|
+
* bundle (one mode per tool, no Gemini CLI), but clearing must remove EVERY
|
|
66
|
+
* tool Unbound can configure — both modes plus Gemini CLI — so a prior
|
|
67
|
+
* gateway-mode or Gemini setup isn't silently left behind. Mirrors the
|
|
68
|
+
* `setup mdm --all` split.
|
|
69
|
+
*/
|
|
70
|
+
function resolveSetupAllTools(clear) {
|
|
71
|
+
return clear ? Object.keys(SETUP_TOOL_MAP) : [...SETUP_ALL_TOOLS];
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
// Instruction-only tools (display config values, no setup scripts)
|
|
63
75
|
const INSTRUCTION_TOOLS = {
|
|
64
76
|
'roo-code': { label: 'Roo Code', values: (apiKey) => [['API Provider', 'unbound'], ['API Key', apiKey]] },
|
|
@@ -341,6 +353,30 @@ async function runBatch(tools, runFn, { clear = false } = {}) {
|
|
|
341
353
|
return true;
|
|
342
354
|
}
|
|
343
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Clears a set of tools best-effort: a single tool failing does not abort the
|
|
358
|
+
* rest (unlike runBatch, which stops on first failure). Used by `uninstall`,
|
|
359
|
+
* where the goal is to remove as much as possible. Returns the labels that failed.
|
|
360
|
+
*/
|
|
361
|
+
async function clearToolsBestEffort(layer, tools, { mdm, backendUrl, gatewayUrl, frontendUrl } = {}) {
|
|
362
|
+
const failed = [];
|
|
363
|
+
for (const tool of tools) {
|
|
364
|
+
const s = output.spinner(`Clearing ${layer}: ${tool.label}...`);
|
|
365
|
+
try {
|
|
366
|
+
// frontendUrl is forwarded for parity with `setup --clear`; buildScriptArgs
|
|
367
|
+
// only appends --domain for non-MDM tools, so MDM clears stay unaffected.
|
|
368
|
+
const args = buildScriptArgs(null, { backendUrl, gatewayUrl, frontendUrl, clear: true, mdm });
|
|
369
|
+
await runScriptPiped(tool.script, args);
|
|
370
|
+
s.succeed(`${layer}: ${tool.label}`);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const reason = (err.message || 'failed').split('\n')[0].slice(0, 80);
|
|
373
|
+
s.fail(`${layer}: ${tool.label} (${reason})`);
|
|
374
|
+
failed.push(tool.label);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return failed;
|
|
378
|
+
}
|
|
379
|
+
|
|
344
380
|
function register(program) {
|
|
345
381
|
const setup = program
|
|
346
382
|
.command('setup')
|
|
@@ -403,6 +439,10 @@ Examples:
|
|
|
403
439
|
$ unbound setup copilot --clear Remove GitHub Copilot config
|
|
404
440
|
$ unbound setup claude-code --clear Remove BOTH Claude Code modes (subscription + gateway)
|
|
405
441
|
$ unbound setup codex --clear Remove BOTH Codex modes (subscription + gateway)
|
|
442
|
+
$ unbound setup --all --clear Remove EVERY tool's config (both modes + Gemini CLI)
|
|
443
|
+
|
|
444
|
+
To also remove MDM config for all users and wipe credentials, use:
|
|
445
|
+
$ sudo unbound nuke
|
|
406
446
|
|
|
407
447
|
Interactive:
|
|
408
448
|
$ unbound setup Select tools interactively
|
|
@@ -447,7 +487,7 @@ requires authentication.
|
|
|
447
487
|
process.exitCode = 1;
|
|
448
488
|
return;
|
|
449
489
|
}
|
|
450
|
-
tools =
|
|
490
|
+
tools = resolveSetupAllTools(opts.clear);
|
|
451
491
|
}
|
|
452
492
|
|
|
453
493
|
// No tools specified → interactive multi-select (existing flow)
|
|
@@ -792,6 +832,100 @@ Clear examples (no API key required):
|
|
|
792
832
|
process.exitCode = 1;
|
|
793
833
|
}
|
|
794
834
|
});
|
|
835
|
+
|
|
836
|
+
// --- Full uninstall ---
|
|
837
|
+
|
|
838
|
+
program
|
|
839
|
+
.command('nuke')
|
|
840
|
+
.alias('uninstall')
|
|
841
|
+
.description(
|
|
842
|
+
'Remove Unbound and start fresh. By default clears every user-level AND ' +
|
|
843
|
+
'MDM (system-level) tool configuration plus stored credentials (requires ' +
|
|
844
|
+
"root). Use --user to clear only this user's tools and credentials (no root)."
|
|
845
|
+
)
|
|
846
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
847
|
+
.option('--user', "Clear only THIS user's tool config and credentials — skip MDM (no root required)")
|
|
848
|
+
.addHelpText('after', `
|
|
849
|
+
Two modes:
|
|
850
|
+
Default (requires root) Clears every user-level and MDM (system-level) tool
|
|
851
|
+
config for all users on the device, then deletes
|
|
852
|
+
credentials.
|
|
853
|
+
--user (no root) Clears only the current user's tool config, then
|
|
854
|
+
deletes credentials. Leaves MDM config untouched
|
|
855
|
+
(removing system-level config needs root).
|
|
856
|
+
|
|
857
|
+
What it clears:
|
|
858
|
+
- USER-level tool config — Cursor, GitHub Copilot, Claude Code (subscription +
|
|
859
|
+
gateway), Gemini CLI, Codex (subscription + gateway). [both modes]
|
|
860
|
+
- MDM (system-level) tool config across all users on the device. [default only]
|
|
861
|
+
- Stored credentials and settings (~/.unbound/config.json). [both modes]
|
|
862
|
+
|
|
863
|
+
After it finishes, run \`unbound login\` (or \`unbound onboard\`) to set things up
|
|
864
|
+
fresh. Clearing never contacts the Unbound API, so no API key is needed.
|
|
865
|
+
|
|
866
|
+
Examples:
|
|
867
|
+
$ sudo unbound nuke Remove everything on the device (asks to confirm)
|
|
868
|
+
$ sudo unbound nuke --yes Remove everything, no confirmation
|
|
869
|
+
$ unbound nuke --user Clear only your tools + credentials (no sudo)
|
|
870
|
+
$ unbound nuke --user --yes Same, no confirmation
|
|
871
|
+
$ sudo unbound uninstall 'uninstall' is an alias for 'nuke'
|
|
872
|
+
`)
|
|
873
|
+
.action(async (opts) => {
|
|
874
|
+
try {
|
|
875
|
+
// Root is only required for the MDM clears (system-level, all users).
|
|
876
|
+
// --user mode skips them, so it doesn't need root. On native Windows the
|
|
877
|
+
// Python MDM scripts do their own admin check, so skip the uid check there.
|
|
878
|
+
if (!opts.user && process.platform !== 'win32' && (typeof process.getuid !== 'function' || process.getuid() !== 0)) {
|
|
879
|
+
output.error('nuke removes system-level MDM config for all users, so it must run as root. Run with: sudo unbound nuke — or use `unbound nuke --user` to clear just your own tools and credentials without root.');
|
|
880
|
+
process.exitCode = 1;
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!opts.yes) {
|
|
885
|
+
output.warn(opts.user
|
|
886
|
+
? 'This removes your user-level Unbound tool configuration and deletes your stored credentials.'
|
|
887
|
+
: 'This removes ALL Unbound tool configuration on this device (user-level and MDM) and deletes your stored credentials.');
|
|
888
|
+
const ok = await confirm('Continue?');
|
|
889
|
+
if (!ok) {
|
|
890
|
+
output.info('Cancelled. Nothing was changed.');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Resolve URLs before wiping config so the clear scripts get consistent
|
|
896
|
+
// flags. Clearing is URL-independent, but this matches the other clear paths.
|
|
897
|
+
const backendUrl = config.getBaseUrl();
|
|
898
|
+
const gatewayUrl = config.getGatewayUrl();
|
|
899
|
+
const frontendUrl = config.getFrontendUrl();
|
|
900
|
+
|
|
901
|
+
console.log('');
|
|
902
|
+
const userTools = Object.keys(SETUP_TOOL_MAP).map(name => ({ name, ...SETUP_TOOL_MAP[name] }));
|
|
903
|
+
const userFailed = await clearToolsBestEffort('user', userTools, { mdm: false, backendUrl, gatewayUrl, frontendUrl });
|
|
904
|
+
|
|
905
|
+
// MDM clears are skipped in --user mode (they need root and touch all users).
|
|
906
|
+
let mdmFailed = [];
|
|
907
|
+
if (!opts.user) {
|
|
908
|
+
const mdmTools = Object.keys(MDM_TOOLS).map(name => ({ name, ...MDM_TOOLS[name] }));
|
|
909
|
+
mdmFailed = await clearToolsBestEffort('mdm', mdmTools, { mdm: true, backendUrl, gatewayUrl });
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Wipe credentials + settings last, regardless of tool-clear outcomes.
|
|
913
|
+
config.clearConfig();
|
|
914
|
+
output.success('Stored credentials and settings removed.');
|
|
915
|
+
|
|
916
|
+
console.log('');
|
|
917
|
+
const failed = [...userFailed, ...mdmFailed];
|
|
918
|
+
const scope = opts.user ? 'for your user' : 'on this device';
|
|
919
|
+
if (failed.length === 0) {
|
|
920
|
+
output.success(`Unbound removed ${scope}. The CLI is back to a fresh state — run "unbound login" to start over.`);
|
|
921
|
+
} else {
|
|
922
|
+
output.warn(`Done ${scope}, but ${failed.length} tool clear(s) reported issues: ${failed.join(', ')}. Credentials were still removed.`);
|
|
923
|
+
}
|
|
924
|
+
} catch (err) {
|
|
925
|
+
output.error(err.message);
|
|
926
|
+
process.exitCode = 1;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
795
929
|
}
|
|
796
930
|
|
|
797
931
|
/**
|
|
@@ -849,4 +983,5 @@ module.exports = {
|
|
|
849
983
|
MDM_ALL_TOOLS,
|
|
850
984
|
buildScriptArgs,
|
|
851
985
|
scriptSupportsBackfill,
|
|
986
|
+
resolveSetupAllTools,
|
|
852
987
|
};
|
package/src/index.js
CHANGED
|
@@ -73,18 +73,23 @@ TOOL SETUP
|
|
|
73
73
|
$ unbound setup kilo-code Show Kilo Code config values
|
|
74
74
|
$ unbound setup custom-access Show API key and base URL
|
|
75
75
|
|
|
76
|
-
Remove configuration:
|
|
76
|
+
Remove configuration (no API key needed to clear):
|
|
77
77
|
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
78
78
|
$ unbound setup copilot --clear Remove Unbound config for GitHub Copilot
|
|
79
79
|
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
80
80
|
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
81
81
|
$ unbound setup codex --clear Remove Unbound config for Codex
|
|
82
|
+
$ unbound setup --all --clear Remove config for every tool
|
|
83
|
+
|
|
84
|
+
Full uninstall (all tools + credentials):
|
|
85
|
+
$ sudo unbound nuke Wipe everything on the device (MDM + user); requires root
|
|
86
|
+
$ unbound nuke --user Wipe just your tools + credentials (no sudo)
|
|
82
87
|
|
|
83
88
|
MDM SETUP (admin, requires root)
|
|
84
89
|
$ sudo unbound setup mdm --admin-api-key KEY --all
|
|
85
90
|
$ sudo unbound setup mdm --admin-api-key KEY cursor copilot codex-subscription
|
|
86
91
|
$ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription codex-subscription gemini-cli
|
|
87
|
-
$ sudo unbound setup mdm --
|
|
92
|
+
$ sudo unbound setup mdm --clear cursor codex-subscription (no API key needed to clear)
|
|
88
93
|
|
|
89
94
|
MDM AI TOOLS DISCOVERY
|
|
90
95
|
--domain defaults to the configured backend URL (set via "unbound config set-backend-url")
|
package/test/setup-args.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { test } = require('node:test');
|
|
2
2
|
const assert = require('node:assert/strict');
|
|
3
|
-
const { buildScriptArgs, scriptSupportsBackfill } = require('../src/commands/setup');
|
|
3
|
+
const { buildScriptArgs, scriptSupportsBackfill, resolveSetupAllTools } = require('../src/commands/setup');
|
|
4
4
|
|
|
5
5
|
// shellEscape single-quotes every value, so a real key surfaces as
|
|
6
6
|
// --api-key '<key>' at the head of the argv tail.
|
|
@@ -102,3 +102,46 @@ test('buildScriptArgs: result is always trimmed', () => {
|
|
|
102
102
|
assert.equal(args, args.trim(), JSON.stringify([key, opts]));
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
// WEB-4587: clearing never needs an API key — including the MDM path. The
|
|
107
|
+
// help/examples say so, and buildScriptArgs must back that up: clear+mdm with
|
|
108
|
+
// no key emits --clear, no --api-key, no --domain (MDM has no browser auth).
|
|
109
|
+
test('buildScriptArgs: mdm clear without key emits --clear and no --api-key/--domain', () => {
|
|
110
|
+
const args = buildScriptArgs(null, {
|
|
111
|
+
clear: true,
|
|
112
|
+
mdm: true,
|
|
113
|
+
backendUrl: 'https://backend.acme.com',
|
|
114
|
+
gatewayUrl: 'https://gateway.acme.com',
|
|
115
|
+
frontendUrl: 'https://gateway.acme.com',
|
|
116
|
+
});
|
|
117
|
+
assert.ok(args.includes('--clear'), args);
|
|
118
|
+
assert.ok(!args.includes('--api-key'), args);
|
|
119
|
+
assert.ok(!args.includes('--domain'), args); // mdm:true never passes the frontend URL
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// WEB-4587 core fix: `setup --all --clear` must remove EVERY tool Unbound can
|
|
123
|
+
// configure, not just the subscription install bundle. Otherwise a prior
|
|
124
|
+
// gateway-mode or Gemini CLI setup is silently left behind.
|
|
125
|
+
test('resolveSetupAllTools(false): install bundle is subscription-only, no gateway/gemini', () => {
|
|
126
|
+
const tools = resolveSetupAllTools(false);
|
|
127
|
+
// Membership + length (order-independent) so reordering SETUP_ALL_TOOLS doesn't break this.
|
|
128
|
+
assert.equal(tools.length, 4, `expected 4 tools, got ${tools.join(',')}`);
|
|
129
|
+
for (const t of ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot']) {
|
|
130
|
+
assert.ok(tools.includes(t), `install bundle missing ${t}: ${tools.join(',')}`);
|
|
131
|
+
}
|
|
132
|
+
assert.ok(!tools.includes('claude-code-gateway'), tools.join(','));
|
|
133
|
+
assert.ok(!tools.includes('codex-gateway'), tools.join(','));
|
|
134
|
+
assert.ok(!tools.includes('gemini-cli'), tools.join(','));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('resolveSetupAllTools(true): clear-all covers every tool incl. gateway modes + gemini', () => {
|
|
138
|
+
const tools = resolveSetupAllTools(true);
|
|
139
|
+
for (const t of ['cursor', 'copilot', 'claude-code-subscription', 'claude-code-gateway',
|
|
140
|
+
'gemini-cli', 'codex-subscription', 'codex-gateway']) {
|
|
141
|
+
assert.ok(tools.includes(t), `clear-all missing ${t}: ${tools.join(',')}`);
|
|
142
|
+
}
|
|
143
|
+
// The clear set must be a superset of the install bundle.
|
|
144
|
+
for (const t of resolveSetupAllTools(false)) {
|
|
145
|
+
assert.ok(tools.includes(t), `clear-all missing install-bundle tool ${t}`);
|
|
146
|
+
}
|
|
147
|
+
});
|