troxy-cli 1.21.1 → 1.23.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 +2 -1
- package/bin/troxy.js +97 -22
- package/package.json +7 -4
- package/src/api.js +18 -1
- package/src/data/policy-spec.json +496 -0
- package/src/data/templates.json +1429 -0
- package/src/init.js +13 -2
- package/src/mcp-server.js +86 -1
- package/src/mcps.js +1 -0
- package/src/policies.js +351 -114
- package/src/policy-fields.js +107 -0
- package/src/secrets.js +1 -1
- package/src/settings.js +0 -3
- package/src/simulate.js +251 -0
- package/src/tests/mcp-checkpoints.test.js +79 -0
- package/src/tests/model-checkpoint.test.js +5 -1
- package/src/tests/policies-create.test.js +218 -0
- package/src/tests/policies-org.test.js +60 -0
- package/src/tests/simulate.test.js +127 -0
- package/src/tests/templates.test.js +46 -0
- package/src/tests/tool_detect.test.js +389 -0
- package/src/tool_detect.js +181 -0
- package/src/chat-budget.js +0 -73
package/src/init.js
CHANGED
|
@@ -185,7 +185,7 @@ export function writeAgentsFile(dir, text) {
|
|
|
185
185
|
return file;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
function prompt(question) {
|
|
188
|
+
export function prompt(question) {
|
|
189
189
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
190
190
|
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
191
191
|
}
|
|
@@ -302,6 +302,17 @@ export async function runInit({ key, name } = {}) {
|
|
|
302
302
|
|
|
303
303
|
await reprovisionKeyConsumers(key, agentName);
|
|
304
304
|
|
|
305
|
+
// Scan for local AI coding tools and (interactively) confirm the plan for
|
|
306
|
+
// each, so the dashboard shows real subscription cost, not token math.
|
|
307
|
+
// Non-fatal + TTY-gated inside runToolDetection - a scripted init never
|
|
308
|
+
// hangs and a scan failure never breaks setup.
|
|
309
|
+
try {
|
|
310
|
+
const { runToolDetection } = await import('./tool_detect.js');
|
|
311
|
+
await runToolDetection(key, { interactive: true });
|
|
312
|
+
} catch {
|
|
313
|
+
// Tool detection is a bonus, never a blocker for a successful init.
|
|
314
|
+
}
|
|
315
|
+
|
|
305
316
|
console.log('\n Your payments are now protected.');
|
|
306
317
|
console.log(' Dashboard → https://dash.troxy.io');
|
|
307
318
|
console.log('\n For more information, visit https://docs.troxy.io\n');
|
|
@@ -553,7 +564,7 @@ export function patchClaudeCodeHooks(
|
|
|
553
564
|
// single marker *file* whose mere existence means "Claude Code is
|
|
554
565
|
// installed" - `~/.claude.json` and `~/.claude/settings.json` are both
|
|
555
566
|
// created lazily and may not exist yet on a fresh install.
|
|
556
|
-
function hasClaudeCode() {
|
|
567
|
+
export function hasClaudeCode() {
|
|
557
568
|
try {
|
|
558
569
|
execSync('claude --version', { stdio: 'ignore' });
|
|
559
570
|
return true;
|
package/src/mcp-server.js
CHANGED
|
@@ -12,6 +12,13 @@ const evaluateLogin = (body, apiKey) => api.evaluateLogin(body, apiKey);
|
|
|
12
12
|
const evaluateAction = (body, apiKey) => api.evaluateAction(body, apiKey);
|
|
13
13
|
const evaluateModel = (body, apiKey) => api.evaluateModel(body, apiKey);
|
|
14
14
|
const reportModelUsage = (body, apiKey) => api.reportModelUsage(body, apiKey);
|
|
15
|
+
// Was missing entirely until 2026-08-15: every other checkpoint had an MCP
|
|
16
|
+
// tool, this one did not, so no MCP-connected agent could ever call
|
|
17
|
+
// /evaluate/secrets - not "the agent chose not to," there was nothing to
|
|
18
|
+
// call. evaluate.py's other_checkpoints discovery (every response advertises
|
|
19
|
+
// the other five) does mention it, but as a raw HTTP URL; useless to an
|
|
20
|
+
// agent whose only way to reach Troxy is through the tools listed here.
|
|
21
|
+
const evaluateSecrets = (body, apiKey) => api.evaluateSecrets(body, apiKey);
|
|
15
22
|
|
|
16
23
|
export async function runMcp() {
|
|
17
24
|
const config = loadConfig();
|
|
@@ -74,7 +81,7 @@ export async function runMcp() {
|
|
|
74
81
|
// Token Optimization is supposed to be a precise record, not a mood.
|
|
75
82
|
// The trigger below is mechanical instead: did this turn use a tool,
|
|
76
83
|
// yes or no. That is a fact you can check, not a judgment you make.
|
|
77
|
-
'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems, and WAIT for the decision before proceeding. Give the exact model id and how many
|
|
84
|
+
'You MUST also call the evaluate_model tool at the start of ANY turn where you will use one or more tools of any kind — reading or writing a file, running a command, browsing, calling an external API, anything beyond a plain text reply — and whenever you switch model or effort level partway through the turn. This is not a judgment call about how the turn feels: if you are about to use a tool, call evaluate_model first, every time, regardless of how small the turn seems, and WAIT for the decision before proceeding. Give the exact model id, how many tokens you expect to use, and how many tool calls you expect to make this turn (estimated_tool_count). Do not use any tool without first receiving an ALLOW or NOTIFY decision; if BLOCK, pick a cheaper or smaller model and call evaluate_model again with the new id; if ESCALATE, call wait_for_approval and do not proceed until it resolves. Then, at the end of that same turn, you MUST call report_model_usage with the audit_id from that decision and the real token total from your model API. Both halves are required: without the second call Troxy has only your prediction, and the account owner is shown that this agent\'s costs are unverified. Report the true number even when it is far above your estimate. ' +
|
|
78
85
|
(hookCaptureActive
|
|
79
86
|
// Claude Code's own Stop hook already reports real, host-captured
|
|
80
87
|
// usage for every turn in this session, chat-only included - see
|
|
@@ -323,6 +330,10 @@ export async function runMcp() {
|
|
|
323
330
|
type: 'number',
|
|
324
331
|
description: 'How many tokens you expect the task to use in total, input plus output. Troxy prices this itself, so an estimate in tokens is enough; do not convert it to dollars.',
|
|
325
332
|
},
|
|
333
|
+
estimated_tool_count: {
|
|
334
|
+
type: 'number',
|
|
335
|
+
description: 'How many tool calls you expect to make this turn (reading/writing files, running commands, browsing, calling external APIs, etc). Report your honest expectation, not a running total across the session - Troxy uses this alongside estimated_tokens to judge how complex the task actually is.',
|
|
336
|
+
},
|
|
326
337
|
effort: {
|
|
327
338
|
type: 'string',
|
|
328
339
|
enum: ['low', 'medium', 'high'],
|
|
@@ -374,6 +385,48 @@ export async function runMcp() {
|
|
|
374
385
|
},
|
|
375
386
|
},
|
|
376
387
|
},
|
|
388
|
+
{
|
|
389
|
+
name: 'evaluate_secrets',
|
|
390
|
+
description:
|
|
391
|
+
'Evaluate whether content you are about to send, write, or publish should be allowed, blocked, or ' +
|
|
392
|
+
'escalated based on your Troxy policies. Troxy scans the content server-side for API keys, credit ' +
|
|
393
|
+
'card numbers, passwords, private keys, tokens, and personally identifiable information - you do ' +
|
|
394
|
+
'not pre-classify it yourself. Call this BEFORE you share a file, send a message, push code, upload ' +
|
|
395
|
+
'data, or write content to any external destination - anything that leaves the current context. ' +
|
|
396
|
+
'If you are unsure whether something counts as sensitive, call this anyway.',
|
|
397
|
+
inputSchema: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
required: ['content'],
|
|
400
|
+
properties: {
|
|
401
|
+
content: {
|
|
402
|
+
type: 'string',
|
|
403
|
+
description: 'The text, code, or data you are about to send or write.',
|
|
404
|
+
},
|
|
405
|
+
destination: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: 'Where it is going: a person, a repo, a channel, a service, or "public" if anyone can see it.',
|
|
408
|
+
},
|
|
409
|
+
action: {
|
|
410
|
+
type: 'string',
|
|
411
|
+
enum: ['send', 'push', 'upload', 'write', 'share'],
|
|
412
|
+
description: 'What you are doing with it.',
|
|
413
|
+
},
|
|
414
|
+
content_type: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
enum: ['code', 'message', 'file', 'config', 'log', 'other'],
|
|
417
|
+
description: 'What it is.',
|
|
418
|
+
},
|
|
419
|
+
agent: {
|
|
420
|
+
type: 'string',
|
|
421
|
+
description: 'Name of the agent (optional).',
|
|
422
|
+
},
|
|
423
|
+
approval_token: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'Approval token from a previous ESCALATE response. Include this to proceed after the user has approved.',
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
377
430
|
],
|
|
378
431
|
}));
|
|
379
432
|
|
|
@@ -608,6 +661,38 @@ export async function runMcp() {
|
|
|
608
661
|
};
|
|
609
662
|
}
|
|
610
663
|
|
|
664
|
+
if (toolName === 'evaluate_secrets') {
|
|
665
|
+
if (agentName && !args.agent) args.agent = agentName;
|
|
666
|
+
let result;
|
|
667
|
+
try {
|
|
668
|
+
result = await evaluateSecrets(args, apiKey);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
return { content: [{ type: 'text', text: `Troxy error: ${err.message}` }], isError: true };
|
|
671
|
+
}
|
|
672
|
+
if (result.error) {
|
|
673
|
+
return { content: [{ type: 'text', text: `Troxy error: ${result.error}` }], isError: true };
|
|
674
|
+
}
|
|
675
|
+
const { decision, reason, audit_id, approval_token, detections } = result;
|
|
676
|
+
const found = (detections || []).map(d => d.type).join(', ');
|
|
677
|
+
const detail = found ? ` Found: ${found}.` : '';
|
|
678
|
+
let secretsText;
|
|
679
|
+
switch (decision) {
|
|
680
|
+
case 'ALLOW':
|
|
681
|
+
case 'NOTIFY':
|
|
682
|
+
secretsText = `✓ Approved.${reason ? ` ${reason}` : ''}${detail} You may proceed. (audit: ${audit_id})`;
|
|
683
|
+
break;
|
|
684
|
+
case 'BLOCK':
|
|
685
|
+
secretsText = `✗ Blocked.${reason ? ` ${reason}` : ''}${detail} Do not send, write, or publish this content as-is. (audit: ${audit_id})`;
|
|
686
|
+
break;
|
|
687
|
+
case 'ESCALATE':
|
|
688
|
+
secretsText = `⏳ This requires human approval; a request has been sent to the account owner.${detail}\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_secrets again with the same arguments PLUS this approval_token. Do not proceed until it returns approved.`;
|
|
689
|
+
break;
|
|
690
|
+
default:
|
|
691
|
+
secretsText = JSON.stringify(result);
|
|
692
|
+
}
|
|
693
|
+
return { content: [{ type: 'text', text: secretsText }], isError: decision === 'BLOCK' };
|
|
694
|
+
}
|
|
695
|
+
|
|
611
696
|
if (toolName !== 'evaluate_payment') {
|
|
612
697
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
613
698
|
}
|