troxy-cli 1.1.0 → 1.2.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 +87 -0
- package/bin/troxy.js +107 -38
- package/package.json +1 -1
- package/src/activity.js +24 -17
- package/src/api.js +8 -0
- package/src/auth.js +18 -0
- package/src/cards.js +28 -25
- package/src/mcps.js +35 -0
- package/src/policies.js +76 -38
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# troxy-cli
|
|
2
|
+
|
|
3
|
+
The official Troxy CLI — onboard AI agents, manage cards and policies from the terminal.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Node.js 18+ is required. Run `node -v` — if you get a version, skip this.
|
|
8
|
+
|
|
9
|
+
**Amazon Linux / RHEL / CentOS**
|
|
10
|
+
```bash
|
|
11
|
+
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
|
|
12
|
+
sudo yum install -y nodejs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Ubuntu / Debian**
|
|
16
|
+
```bash
|
|
17
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
|
18
|
+
sudo apt-get install -y nodejs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**macOS**
|
|
22
|
+
```bash
|
|
23
|
+
brew install node
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Any system (nvm)**
|
|
27
|
+
```bash
|
|
28
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
|
29
|
+
source ~/.bashrc
|
|
30
|
+
nvm install --lts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Verify: `node --version` and `npx --version` should both return a version.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g troxy-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or run without installing:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx troxy-cli <command>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `troxy init` | Connect an agent to Troxy — validates API key, sets agent name, writes config |
|
|
52
|
+
| `troxy login` | Authenticate via magic-link code (enter the code from your email) |
|
|
53
|
+
| `troxy cards` | List card aliases |
|
|
54
|
+
| `troxy policies` | List policies |
|
|
55
|
+
| `troxy activity` | View recent transaction audit log |
|
|
56
|
+
| `troxy status` | Show current config and connection status |
|
|
57
|
+
|
|
58
|
+
## How it works
|
|
59
|
+
|
|
60
|
+
1. User runs `troxy init --key txy-...` in their agent project
|
|
61
|
+
2. CLI validates the key against `api.troxy.io`, prompts for agent name
|
|
62
|
+
3. Writes a `.troxy/config.json` to the project directory
|
|
63
|
+
4. Agent uses the config to call `/evaluate` before every payment
|
|
64
|
+
|
|
65
|
+
## MCP Server
|
|
66
|
+
|
|
67
|
+
The CLI also ships an MCP server (`src/mcp-server.js`) that exposes Troxy as a tool for Claude and other MCP-compatible agents.
|
|
68
|
+
|
|
69
|
+
## Auth flow
|
|
70
|
+
|
|
71
|
+
`troxy login` triggers the same magic-link flow as the dashboard:
|
|
72
|
+
- Sends a code to your email
|
|
73
|
+
- Prompts you to enter the `XXXX-XXXX` code in the terminal
|
|
74
|
+
- Stores the JWT locally for subsequent CLI calls
|
|
75
|
+
|
|
76
|
+
## Stack
|
|
77
|
+
|
|
78
|
+
- Node.js 18+ (ESM)
|
|
79
|
+
- Zero runtime dependencies except `@modelcontextprotocol/sdk`
|
|
80
|
+
- Published to npm as `troxy-cli`
|
|
81
|
+
|
|
82
|
+
## Related repos
|
|
83
|
+
|
|
84
|
+
| Repo | Description |
|
|
85
|
+
|------|-------------|
|
|
86
|
+
| [troxy-tf-live](https://github.com/troxy-hq/troxy-tf-live) | Backend API the CLI talks to |
|
|
87
|
+
| [troxy-dashboard](https://github.com/troxy-hq/troxy-dashboard) | Web dashboard alternative |
|
package/bin/troxy.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runInit }
|
|
3
|
-
import { runUninstall }
|
|
4
|
-
import { runMcp }
|
|
5
|
-
import { runLogin, clearSession } from '../src/auth.js';
|
|
6
|
-
import { runCards }
|
|
7
|
-
import { runPolicies }
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { table }
|
|
2
|
+
import { runInit } from '../src/init.js';
|
|
3
|
+
import { runUninstall } from '../src/uninstall.js';
|
|
4
|
+
import { runMcp } from '../src/mcp-server.js';
|
|
5
|
+
import { runLogin, clearSession, requireKey } from '../src/auth.js';
|
|
6
|
+
import { runCards } from '../src/cards.js';
|
|
7
|
+
import { runPolicies } from '../src/policies.js';
|
|
8
|
+
import { runMcps } from '../src/mcps.js';
|
|
9
|
+
import { runActivity } from '../src/activity.js';
|
|
10
|
+
import { api } from '../src/api.js';
|
|
11
|
+
import { table } from '../src/print.js';
|
|
12
12
|
|
|
13
13
|
const [,, command, sub, ...rest] = process.argv;
|
|
14
14
|
const allArgs = [sub, ...rest].filter(Boolean);
|
|
@@ -51,7 +51,7 @@ switch (command) {
|
|
|
51
51
|
await runMcp();
|
|
52
52
|
break;
|
|
53
53
|
|
|
54
|
-
// ── Resources
|
|
54
|
+
// ── Resources (read-only: --key or saved config; write: login) ─
|
|
55
55
|
case 'cards':
|
|
56
56
|
await runCards(positional, flags);
|
|
57
57
|
break;
|
|
@@ -60,24 +60,89 @@ switch (command) {
|
|
|
60
60
|
await runPolicies(positional, flags);
|
|
61
61
|
break;
|
|
62
62
|
|
|
63
|
+
case 'mcps':
|
|
64
|
+
await runMcps(positional, flags);
|
|
65
|
+
break;
|
|
66
|
+
|
|
63
67
|
case 'activity':
|
|
64
68
|
await runActivity(flags);
|
|
65
69
|
break;
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
case 'insights': {
|
|
72
|
+
const apiKey = requireKey(flags);
|
|
73
|
+
const period = Number(flags.period || 30);
|
|
74
|
+
const data = await api.agentInsights(apiKey, period);
|
|
75
|
+
const d = data;
|
|
76
|
+
console.log(`
|
|
77
|
+
Insights — last ${d.period_days} days
|
|
78
|
+
──────────────────────────────────
|
|
79
|
+
Total requests: ${d.total_requests}
|
|
80
|
+
Total spent: $${Number(d.total_spent).toFixed(2)}
|
|
81
|
+
Total blocked: $${Number(d.total_blocked).toFixed(2)}
|
|
82
|
+
|
|
83
|
+
Decisions
|
|
84
|
+
ALLOW: ${d.decisions.ALLOW}
|
|
85
|
+
BLOCK: ${d.decisions.BLOCK}
|
|
86
|
+
ESCALATE: ${d.decisions.ESCALATE}
|
|
87
|
+
NOTIFY: ${d.decisions.NOTIFY}
|
|
88
|
+
`);
|
|
89
|
+
if (d.top_merchants?.length) {
|
|
90
|
+
console.log(' Top merchants');
|
|
91
|
+
table(
|
|
92
|
+
['Merchant', 'Requests', 'Total Spend'],
|
|
93
|
+
d.top_merchants.map(m => [m.merchant, m.requests, `$${Number(m.total).toFixed(2)}`]),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Shorthand: troxy list [resource] ──────────────────────────
|
|
68
100
|
case 'list':
|
|
69
|
-
if (!sub || sub === 'cards') { await runCards(['list'], flags);
|
|
101
|
+
if (!sub || sub === 'cards') { await runCards(['list'], flags); break; }
|
|
70
102
|
if (sub === 'policies') { await runPolicies(['list'], flags); break; }
|
|
71
|
-
if (sub === '
|
|
72
|
-
|
|
103
|
+
if (sub === 'mcps') { await runMcps(['list'], flags); break; }
|
|
104
|
+
if (sub === 'activity') { await runActivity(flags); break; }
|
|
105
|
+
console.error(` Unknown resource: ${sub}. Try: cards, policies, mcps, activity\n`);
|
|
73
106
|
process.exit(1);
|
|
74
107
|
|
|
75
108
|
// ── Status ────────────────────────────────────────────────────
|
|
76
109
|
case 'status': {
|
|
77
110
|
const health = await api.health();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
process.stdout.write(`\n API: ${health.status === 'ok' ? '✓ online' : '✗ ' + health.status}\n`);
|
|
112
|
+
process.stdout.write(` DB: ${health.db}\n`);
|
|
113
|
+
process.stdout.write(` Env: ${health.env}\n`);
|
|
114
|
+
|
|
115
|
+
// If we have a key, show enriched status
|
|
116
|
+
try {
|
|
117
|
+
const apiKey = requireKey(flags);
|
|
118
|
+
const data = await api.agentStatus(apiKey);
|
|
119
|
+
const { token, account } = data;
|
|
120
|
+
console.log(`
|
|
121
|
+
Token: ${token.prefix} (${token.name})
|
|
122
|
+
MCP: ${token.connected ? '● connected' : '○ offline'} last seen ${token.last_seen}
|
|
123
|
+
Fallback: ${token.default_action}
|
|
124
|
+
|
|
125
|
+
Account
|
|
126
|
+
Active policies: ${account.active_policies}
|
|
127
|
+
MCPs total: ${account.total_mcps} (${account.connected_mcps} connected)
|
|
128
|
+
Requests 24h: ${account.requests_24h}
|
|
129
|
+
Default action: ${account.default_action}
|
|
130
|
+
`);
|
|
131
|
+
} catch { console.log(); }
|
|
132
|
+
|
|
133
|
+
// Version check
|
|
134
|
+
try {
|
|
135
|
+
const { createRequire } = await import('module');
|
|
136
|
+
const require = createRequire(import.meta.url);
|
|
137
|
+
const { version: current } = require('../package.json');
|
|
138
|
+
const res = await fetch('https://registry.npmjs.org/troxy-cli/latest', { signal: AbortSignal.timeout(3000) });
|
|
139
|
+
const { version: latest } = await res.json();
|
|
140
|
+
if (current !== latest) {
|
|
141
|
+
console.log(` ⚠ New version available: ${latest} (you have ${current})`);
|
|
142
|
+
console.log(` Update with: sudo npm install -g troxy-cli@latest\n`);
|
|
143
|
+
}
|
|
144
|
+
} catch {}
|
|
145
|
+
|
|
81
146
|
break;
|
|
82
147
|
}
|
|
83
148
|
|
|
@@ -87,27 +152,31 @@ switch (command) {
|
|
|
87
152
|
console.log(`
|
|
88
153
|
Troxy — AI payment control
|
|
89
154
|
|
|
155
|
+
Read-only commands work with just an API key (--key txy-... or TROXY_API_KEY env).
|
|
156
|
+
Write commands (create/delete/enable/disable) require: npx troxy login
|
|
157
|
+
|
|
90
158
|
Setup
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
159
|
+
troxy init --key <api-key> Initialize agent on this machine
|
|
160
|
+
troxy uninstall Remove Troxy from this machine
|
|
161
|
+
troxy login Log in for write access
|
|
162
|
+
troxy logout Clear session
|
|
163
|
+
troxy status [--key <key>] API health + account summary
|
|
164
|
+
|
|
165
|
+
Inspect (API key only — works from EC2)
|
|
166
|
+
troxy policies list [--key] All policies with scope + conditions
|
|
167
|
+
troxy policies describe --name "X" [--key]
|
|
168
|
+
troxy mcps list [--key] All MCP connections + status
|
|
169
|
+
troxy cards list [--key] Cards with budget usage
|
|
170
|
+
troxy activity [--key] [--limit 50] [--mine] Recent decisions
|
|
171
|
+
troxy insights [--key] [--period 7] Spend stats
|
|
172
|
+
|
|
173
|
+
Manage (requires login)
|
|
174
|
+
troxy policies create --name "X" --action BLOCK --field amount --operator gte --value 500
|
|
175
|
+
troxy policies enable --name "X"
|
|
176
|
+
troxy policies disable --name "X"
|
|
177
|
+
troxy policies delete --name "X"
|
|
178
|
+
troxy cards create --name "Personal" [--budget 500]
|
|
179
|
+
troxy cards delete --name "Personal"
|
|
111
180
|
`);
|
|
112
181
|
process.exit(command ? 1 : 0);
|
|
113
182
|
}
|
package/package.json
CHANGED
package/src/activity.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import {
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~' };
|
|
6
6
|
|
|
7
7
|
export async function runActivity(flags) {
|
|
8
|
-
const
|
|
9
|
-
const limit
|
|
10
|
-
const
|
|
11
|
-
|
|
8
|
+
const apiKey = requireKey(flags);
|
|
9
|
+
const limit = Number(flags.limit || 20);
|
|
10
|
+
const mine = !!flags.mine;
|
|
11
|
+
|
|
12
|
+
const data = await api.agentActivity(apiKey, limit, mine);
|
|
13
|
+
const rows = data?.activity || [];
|
|
12
14
|
|
|
13
15
|
if (!rows.length) { console.log('\n No activity yet.\n'); return; }
|
|
14
16
|
|
|
15
17
|
console.log();
|
|
16
18
|
table(
|
|
17
|
-
['Decision', '
|
|
18
|
-
rows.map(r =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
['Decision', 'Merchant', 'Category', 'Amount', 'Policy', 'Card', 'Agent', 'When'],
|
|
20
|
+
rows.map(r => {
|
|
21
|
+
const icon = ICON[r.decision?.split('→')[0]] || ' ';
|
|
22
|
+
return [
|
|
23
|
+
`${icon} ${r.decision}`,
|
|
24
|
+
r.merchant,
|
|
25
|
+
r.category,
|
|
26
|
+
r.amount ? `$${Number(r.amount).toFixed(2)}` : '—',
|
|
27
|
+
r.policy,
|
|
28
|
+
r.card,
|
|
29
|
+
r.agent,
|
|
30
|
+
r.when,
|
|
31
|
+
];
|
|
32
|
+
}),
|
|
26
33
|
);
|
|
27
34
|
}
|
package/src/api.js
CHANGED
|
@@ -50,6 +50,14 @@ export const api = {
|
|
|
50
50
|
|
|
51
51
|
// MCP heartbeat (agent API key)
|
|
52
52
|
mcpHeartbeat: (apiKey, agentName) => request('POST', '/mcp/heartbeat', { apiKey, body: agentName ? { agent_name: agentName } : undefined }),
|
|
53
|
+
|
|
54
|
+
// Agent read-only API (API key auth — no JWT required)
|
|
55
|
+
agentStatus: (apiKey) => request('GET', '/agent/status', { apiKey }),
|
|
56
|
+
agentPolicies: (apiKey) => request('GET', '/agent/policies', { apiKey }),
|
|
57
|
+
agentMcps: (apiKey) => request('GET', '/agent/mcps', { apiKey }),
|
|
58
|
+
agentCards: (apiKey) => request('GET', '/agent/cards', { apiKey }),
|
|
59
|
+
agentActivity: (apiKey, limit, mine) => request('GET', `/agent/activity?limit=${limit || 20}${mine ? '&mine=true' : ''}`, { apiKey }),
|
|
60
|
+
agentInsights: (apiKey, period) => request('GET', `/agent/insights?period=${period || 30}`, { apiKey }),
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
// Named export for backwards compat with init.js + mcp-server.js
|
package/src/auth.js
CHANGED
|
@@ -33,6 +33,24 @@ export function requireJwt() {
|
|
|
33
33
|
return session.jwt;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Resolve API key: --key flag → TROXY_API_KEY env → saved config.
|
|
38
|
+
* Exits with a helpful message if nothing is found.
|
|
39
|
+
*/
|
|
40
|
+
export function requireKey(flags = {}) {
|
|
41
|
+
const key = flags.key || process.env.TROXY_API_KEY || loadConfig()?.apiKey;
|
|
42
|
+
if (!key) {
|
|
43
|
+
console.error('\n No API key found. Pass --key txy-... or run: npx troxy init\n');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadConfig() {
|
|
50
|
+
const p = path.join(os.homedir(), '.troxy', 'config.json');
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
/** Interactive magic-link login flow. */
|
|
37
55
|
export async function runLogin({ email } = {}) {
|
|
38
56
|
if (!email) {
|
package/src/cards.js
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import { requireJwt } from './auth.js';
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt, requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
4
|
|
|
5
5
|
export async function runCards([sub, ...args], flags) {
|
|
6
|
+
// list is read-only — works with API key
|
|
7
|
+
if (!sub || sub === 'list') {
|
|
8
|
+
const apiKey = requireKey(flags);
|
|
9
|
+
const data = await api.agentCards(apiKey);
|
|
10
|
+
const cards = data?.cards || [];
|
|
11
|
+
if (!cards.length) { console.log('\n No cards yet.\n'); return; }
|
|
12
|
+
console.log();
|
|
13
|
+
table(
|
|
14
|
+
['Name', 'Last 4', 'Status', 'Budget', 'Used', 'Remaining', 'Default Action'],
|
|
15
|
+
cards.map(c => [
|
|
16
|
+
c.name,
|
|
17
|
+
c.last_four ? `···${c.last_four}` : '—',
|
|
18
|
+
c.status,
|
|
19
|
+
c.monthly_budget ? `$${c.monthly_budget}` : 'no limit',
|
|
20
|
+
`$${Number(c.budget_used || 0).toFixed(2)}`,
|
|
21
|
+
c.budget_remaining != null ? `$${Number(c.budget_remaining).toFixed(2)}` : '—',
|
|
22
|
+
c.default_action || 'ALLOW',
|
|
23
|
+
]),
|
|
24
|
+
);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Write operations need JWT
|
|
6
29
|
const jwt = requireJwt();
|
|
7
30
|
|
|
8
31
|
switch (sub) {
|
|
9
|
-
case 'list':
|
|
10
|
-
case undefined: {
|
|
11
|
-
const data = await api.listCards(jwt);
|
|
12
|
-
const cards = data?.cards || [];
|
|
13
|
-
if (!cards.length) { console.log('\n No cards yet.\n'); return; }
|
|
14
|
-
console.log();
|
|
15
|
-
table(
|
|
16
|
-
['Name', 'Last 4', 'Status', 'Budget', 'Used'],
|
|
17
|
-
cards.map(c => [
|
|
18
|
-
c.name,
|
|
19
|
-
c.last4 ? `···${c.last4}` : '—',
|
|
20
|
-
c.status,
|
|
21
|
-
c.budget ? `$${c.budget}` : 'no limit',
|
|
22
|
-
`$${Number(c.budget_used || 0).toFixed(2)}`,
|
|
23
|
-
]),
|
|
24
|
-
);
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
32
|
case 'create': {
|
|
29
33
|
const name = flags.name;
|
|
30
34
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
@@ -44,8 +48,7 @@ export async function runCards([sub, ...args], flags) {
|
|
|
44
48
|
const name = flags.name;
|
|
45
49
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
46
50
|
const data = await api.listCards(jwt);
|
|
47
|
-
const
|
|
48
|
-
const card = cards.find(c => c.name === name);
|
|
51
|
+
const card = (data?.cards || []).find(c => c.name === name);
|
|
49
52
|
if (!card) { console.error(` Card "${name}" not found\n`); process.exit(1); }
|
|
50
53
|
await api.deleteCard(jwt, card.id);
|
|
51
54
|
console.log(`\n Card "${name}" deleted ✓\n`);
|
|
@@ -54,7 +57,7 @@ export async function runCards([sub, ...args], flags) {
|
|
|
54
57
|
|
|
55
58
|
default:
|
|
56
59
|
console.error(` Unknown subcommand: ${sub}`);
|
|
57
|
-
console.error(' Usage:
|
|
60
|
+
console.error(' Usage: troxy cards [list|create|delete]\n');
|
|
58
61
|
process.exit(1);
|
|
59
62
|
}
|
|
60
63
|
}
|
package/src/mcps.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
import { requireKey } from './auth.js';
|
|
4
|
+
import { table } from './print.js';
|
|
5
|
+
|
|
6
|
+
export async function runMcps([sub], flags) {
|
|
7
|
+
const apiKey = requireKey(flags);
|
|
8
|
+
|
|
9
|
+
switch (sub || 'list') {
|
|
10
|
+
case 'list': {
|
|
11
|
+
const data = await api.agentMcps(apiKey);
|
|
12
|
+
const mcps = data?.mcps || [];
|
|
13
|
+
if (!mcps.length) { console.log('\n No MCP connections yet.\n'); return; }
|
|
14
|
+
console.log();
|
|
15
|
+
table(
|
|
16
|
+
['Name', 'Prefix', 'Status', 'Last Seen', 'Policies', 'Default Action', 'Me'],
|
|
17
|
+
mcps.map(m => [
|
|
18
|
+
m.name,
|
|
19
|
+
m.token_prefix,
|
|
20
|
+
m.connected ? '● connected' : '○ offline',
|
|
21
|
+
m.last_seen,
|
|
22
|
+
m.policies_assigned,
|
|
23
|
+
m.default_action,
|
|
24
|
+
m.is_me ? '← you' : '',
|
|
25
|
+
]),
|
|
26
|
+
);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
console.error(` Unknown subcommand: ${sub}`);
|
|
32
|
+
console.error(' Usage: troxy mcps [list]\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/policies.js
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
|
-
import { api }
|
|
2
|
-
import { requireJwt } from './auth.js';
|
|
3
|
-
import { table }
|
|
1
|
+
import { api } from './api.js';
|
|
2
|
+
import { requireJwt, requireKey } from './auth.js';
|
|
3
|
+
import { table } from './print.js';
|
|
4
|
+
|
|
5
|
+
const DECISION_ICON = { ALLOW: '✓', BLOCK: '✗', ESCALATE: '⏳', NOTIFY: '~', TIERED: '⊕' };
|
|
6
|
+
const SCOPE_COLOR = { 'all MCPs': 'all MCPs', 'this MCP': '→ me', 'other MCPs': 'other' };
|
|
4
7
|
|
|
5
8
|
export async function runPolicies([sub, ...args], flags) {
|
|
6
|
-
|
|
9
|
+
// Read-only subcommands work with just an API key
|
|
10
|
+
const readOnly = !sub || sub === 'list' || sub === 'describe';
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
p
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
if (readOnly) {
|
|
13
|
+
const apiKey = requireKey(flags);
|
|
14
|
+
switch (sub || 'list') {
|
|
15
|
+
case 'list': {
|
|
16
|
+
const data = await api.agentPolicies(apiKey);
|
|
17
|
+
const policies = data?.policies || [];
|
|
18
|
+
if (!policies.length) { console.log('\n No policies yet.\n'); return; }
|
|
19
|
+
console.log();
|
|
20
|
+
table(
|
|
21
|
+
['#', 'Name', 'Action', 'Scope', 'Status', 'Conditions', 'Applies to me'],
|
|
22
|
+
policies.map(p => [
|
|
23
|
+
p.priority,
|
|
24
|
+
p.name,
|
|
25
|
+
p.action,
|
|
26
|
+
SCOPE_COLOR[p.scope] || p.scope,
|
|
27
|
+
p.enabled ? 'enabled' : 'disabled',
|
|
28
|
+
_condSummary(p),
|
|
29
|
+
p.applies_to_me ? '✓' : '—',
|
|
30
|
+
]),
|
|
31
|
+
);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case 'describe': {
|
|
36
|
+
const name = flags.name;
|
|
37
|
+
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
38
|
+
const data = await api.agentPolicies(apiKey);
|
|
39
|
+
const p = (data?.policies || []).find(x => x.name.toLowerCase() === name.toLowerCase());
|
|
40
|
+
if (!p) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
41
|
+
|
|
42
|
+
console.log(`
|
|
43
|
+
Name: ${p.name}
|
|
44
|
+
Action: ${p.action}
|
|
45
|
+
Priority: ${p.priority}
|
|
46
|
+
Status: ${p.enabled ? 'enabled' : 'disabled'}
|
|
47
|
+
Scope: ${p.scope}
|
|
48
|
+
Applies here: ${p.applies_to_me ? 'yes' : 'no'}
|
|
49
|
+
MCPs: ${p.mcps.length ? p.mcps.map(m => m.name).join(', ') : p.global ? 'all MCPs' : 'none'}
|
|
50
|
+
Conditions: ${_condDetail(p)}
|
|
51
|
+
Created: ${new Date(p.created_at).toLocaleDateString()}
|
|
52
|
+
`);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
26
55
|
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Write subcommands need JWT
|
|
60
|
+
const jwt = requireJwt();
|
|
27
61
|
|
|
62
|
+
switch (sub) {
|
|
28
63
|
case 'create': {
|
|
29
64
|
const name = flags.name;
|
|
30
65
|
const action = (flags.action || '').toUpperCase();
|
|
@@ -34,8 +69,6 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
34
69
|
console.error(' --action must be ALLOW, BLOCK, ESCALATE, or NOTIFY\n');
|
|
35
70
|
process.exit(1);
|
|
36
71
|
}
|
|
37
|
-
|
|
38
|
-
// Build a single condition if --field/--operator/--value are provided
|
|
39
72
|
const conditions = [];
|
|
40
73
|
if (flags.field) {
|
|
41
74
|
if (!flags.operator) { console.error(' --operator is required with --field\n'); process.exit(1); }
|
|
@@ -44,16 +77,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
44
77
|
if (flags.value2) cond.value2 = flags.value2;
|
|
45
78
|
conditions.push(cond);
|
|
46
79
|
}
|
|
47
|
-
|
|
48
|
-
const body = {
|
|
49
|
-
name,
|
|
50
|
-
action,
|
|
51
|
-
conditions,
|
|
52
|
-
conditions_logic: (flags.logic || 'AND').toUpperCase(),
|
|
53
|
-
enabled: true,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const policy = await api.createPolicy(jwt, body);
|
|
80
|
+
const policy = await api.createPolicy(jwt, { name, action, conditions, enabled: true });
|
|
57
81
|
console.log(`\n Policy "${policy.name}" created ✓ (priority: ${policy.priority})\n`);
|
|
58
82
|
break;
|
|
59
83
|
}
|
|
@@ -62,7 +86,7 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
62
86
|
const name = flags.name;
|
|
63
87
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
64
88
|
const { policies = [] } = await api.listPolicies(jwt);
|
|
65
|
-
const policy
|
|
89
|
+
const policy = policies.find(p => p.name === name);
|
|
66
90
|
if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
67
91
|
await api.deletePolicy(jwt, policy.id);
|
|
68
92
|
console.log(`\n Policy "${name}" deleted ✓\n`);
|
|
@@ -71,10 +95,10 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
71
95
|
|
|
72
96
|
case 'enable':
|
|
73
97
|
case 'disable': {
|
|
74
|
-
const name
|
|
98
|
+
const name = flags.name;
|
|
75
99
|
if (!name) { console.error(' --name is required\n'); process.exit(1); }
|
|
76
100
|
const { policies = [] } = await api.listPolicies(jwt);
|
|
77
|
-
const policy
|
|
101
|
+
const policy = policies.find(p => p.name === name);
|
|
78
102
|
if (!policy) { console.error(` Policy "${name}" not found\n`); process.exit(1); }
|
|
79
103
|
await api.updatePolicy(jwt, policy.id, { enabled: sub === 'enable' });
|
|
80
104
|
console.log(`\n Policy "${name}" ${sub}d ✓\n`);
|
|
@@ -83,7 +107,21 @@ export async function runPolicies([sub, ...args], flags) {
|
|
|
83
107
|
|
|
84
108
|
default:
|
|
85
109
|
console.error(` Unknown subcommand: ${sub}`);
|
|
86
|
-
console.error(' Usage:
|
|
110
|
+
console.error(' Usage: troxy policies [list|describe|create|delete|enable|disable]\n');
|
|
87
111
|
process.exit(1);
|
|
88
112
|
}
|
|
89
113
|
}
|
|
114
|
+
|
|
115
|
+
function _condSummary(p) {
|
|
116
|
+
const c = p.conditions || [];
|
|
117
|
+
const o = p.or_conditions || [];
|
|
118
|
+
const total = c.length + o.length;
|
|
119
|
+
if (total === 0) return 'always';
|
|
120
|
+
return `${total} condition${total > 1 ? 's' : ''}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _condDetail(p) {
|
|
124
|
+
const c = p.conditions || [];
|
|
125
|
+
if (!c.length) return 'none (always matches)';
|
|
126
|
+
return c.map(x => `${x.field} ${x.operator} ${x.value || ''}${x.value2 ? '–'+x.value2 : ''}`).join(' AND ');
|
|
127
|
+
}
|