troxy-cli 1.14.0 → 1.17.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 +1 -1
- package/package.json +1 -1
- package/src/api.js +25 -1
- package/src/mcp-server.js +34 -21
- package/src/tests/model-checkpoint.test.js +68 -0
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ Checkpoints an agent can call:
|
|
|
78
78
|
| `evaluate_login` | logging into or signing up for a site |
|
|
79
79
|
| `evaluate_model` | running a task on a model, and on any model or effort switch |
|
|
80
80
|
| `report_model_usage` | (after) reporting the real token total |
|
|
81
|
-
| `
|
|
81
|
+
| `wait_for_approval` | (after ESCALATE) waiting on the owner's decision |
|
|
82
82
|
|
|
83
83
|
The model checkpoint is the only one that is a required pair. `evaluate_model`
|
|
84
84
|
records what a run is expected to cost; `report_model_usage` records what it
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
1
3
|
export const BASE_URL =
|
|
2
4
|
process.env.TROXY_API_URL ||
|
|
3
5
|
'https://api.troxy.io';
|
|
4
6
|
|
|
7
|
+
// Reported on every heartbeat so the dashboard can explain an empty page.
|
|
8
|
+
//
|
|
9
|
+
// Checkpoints an old CLI does not expose simply never fire, and the affected
|
|
10
|
+
// page just looks blank: before 1.14.0 there was no evaluate_model tool at all,
|
|
11
|
+
// so Token Optimization stayed empty forever with nothing saying why. Knowing
|
|
12
|
+
// the version turns that into a sentence the user can act on.
|
|
13
|
+
export const CLI_VERSION = (() => {
|
|
14
|
+
try {
|
|
15
|
+
return createRequire(import.meta.url)('../package.json').version;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
|
|
5
21
|
async function request(method, path, { apiKey, jwt, body } = {}) {
|
|
6
22
|
const headers = { 'Content-Type': 'application/json' };
|
|
7
23
|
if (apiKey) headers['X-Troxy-Key'] = apiKey;
|
|
@@ -85,7 +101,15 @@ export const api = {
|
|
|
85
101
|
waitApprovalStatus: (token) => request('GET', `/approvals/${encodeURIComponent(token)}/wait`),
|
|
86
102
|
|
|
87
103
|
// MCP heartbeat (agent API key)
|
|
88
|
-
|
|
104
|
+
// cli_version goes on every heartbeat, including the ones that carry no
|
|
105
|
+
// agent name, so a long-running agent that never re-inits still reports it.
|
|
106
|
+
mcpHeartbeat: (apiKey, agentName, force) => request('POST', '/mcp/heartbeat', {
|
|
107
|
+
apiKey,
|
|
108
|
+
body: {
|
|
109
|
+
cli_version: CLI_VERSION,
|
|
110
|
+
...(agentName ? { agent_name: agentName, force_name: !!force } : {}),
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
89
113
|
|
|
90
114
|
// MCP status (agent API key — no login needed)
|
|
91
115
|
mcpStatus: (apiKey) => request('GET', '/mcp/status', { apiKey }),
|
package/src/mcp-server.js
CHANGED
|
@@ -36,22 +36,33 @@ export async function runMcp() {
|
|
|
36
36
|
name: 'troxy',
|
|
37
37
|
version: '0.1.0',
|
|
38
38
|
instructions:
|
|
39
|
-
'You are connected to Troxy, a
|
|
39
|
+
'You are connected to Troxy, a control layer for agent actions: payments, emails, logins, destructive actions, and model spend. ' +
|
|
40
40
|
'You MUST call the evaluate_payment tool before completing any purchase, payment, or financial transaction. ' +
|
|
41
41
|
'Never submit a payment form, confirm a checkout, or transfer funds without first receiving an ALLOW decision from evaluate_payment. ' +
|
|
42
42
|
'If the decision is BLOCK, abort the transaction and inform the user. ' +
|
|
43
|
-
'If the decision is ESCALATE, the response includes an approval_token. Immediately call
|
|
43
|
+
'If the decision is ESCALATE, the response includes an approval_token. Immediately call wait_for_approval with that token; it polls automatically until the request is approved or declined (up to 10 minutes). Once it returns approved, call evaluate_payment again with the same payment details PLUS the approval_token field. The second call will return ALLOW without re-escalating. ' +
|
|
44
44
|
'If the decision is NOTIFY, proceed but the user will be notified. ' +
|
|
45
45
|
'When calling evaluate_payment, use the merchant\'s domain name as merchant_name when available (e.g. "amazon.com", "indigobloom.co.il"); this ensures consistent matching against policies. ' +
|
|
46
46
|
'After every ALLOW or NOTIFY decision, attempt the payment, then call confirm_payment with the audit_id and the outcome (success/failed/cancelled). Always include a reason when the charge fails. ' +
|
|
47
|
-
'You MUST also call the evaluate_email tool before sending any email on the user\'s behalf. Always include the full plaintext "body" argument, exactly as you intend to send it — Troxy policies inspect the body for secrets and confidential content, and cannot do so if it is omitted, summarized, or truncated. When the email goes to more than one person (a reply-all, a group, a bulk send), also pass every To/Cc/Bcc address in "recipients" so mass-email policies can apply. Never send an email without first receiving an ALLOW or NOTIFY decision. If BLOCK, do not send it. If ESCALATE, call
|
|
47
|
+
'You MUST also call the evaluate_email tool before sending any email on the user\'s behalf. Always include the full plaintext "body" argument, exactly as you intend to send it — Troxy policies inspect the body for secrets and confidential content, and cannot do so if it is omitted, summarized, or truncated. When the email goes to more than one person (a reply-all, a group, a bulk send), also pass every To/Cc/Bcc address in "recipients" so mass-email policies can apply. Never send an email without first receiving an ALLOW or NOTIFY decision. If BLOCK, do not send it. If ESCALATE, call wait_for_approval with the returned approval_token, then call evaluate_email again with the approval_token once approved. ' +
|
|
48
48
|
'You MUST also call the evaluate_action tool before any destructive or hard-to-undo action on the user\'s behalf: deleting files or records, dropping or truncating a table, force-pushing or deleting a branch, destroying infrastructure, revoking access, or overwriting data. Give the verb, what it affects, how many items, and whether it can be undone. Never carry the action out without first receiving an ALLOW or NOTIFY decision. ' +
|
|
49
|
-
'You MUST also call the evaluate_login tool before logging into or signing up for any website on the user\'s behalf, giving the site\'s domain and whether it is a login or a signup. Never submit login credentials or a signup form without first receiving an ALLOW or NOTIFY decision. If BLOCK, do not proceed. If ESCALATE, call
|
|
49
|
+
'You MUST also call the evaluate_login tool before logging into or signing up for any website on the user\'s behalf, giving the site\'s domain and whether it is a login or a signup. Never submit login credentials or a signup form without first receiving an ALLOW or NOTIFY decision. If BLOCK, do not proceed. If ESCALATE, call wait_for_approval with the returned approval_token, then call evaluate_login again with the approval_token once approved. ' +
|
|
50
50
|
// The model checkpoint is the only one that is a pair. The estimate on
|
|
51
51
|
// its own is the agent's own prediction, and the dashboard labels every
|
|
52
52
|
// agent that never reports back as unverified, so the instruction has
|
|
53
53
|
// to make the second call as non-optional as the first.
|
|
54
|
-
|
|
54
|
+
//
|
|
55
|
+
// The previous wording gated this on whether the task felt big
|
|
56
|
+
// enough, which was a judgment call, not a rule. Driven live: the
|
|
57
|
+
// same agent, same session, called evaluate_model to review two
|
|
58
|
+
// source files, then minutes later pulled real data from an actual
|
|
59
|
+
// connected business (orders, customer messages, an artifact) and
|
|
60
|
+
// called nothing. Both were real work by any reasonable reading. The
|
|
61
|
+
// old wording meant whatever the model felt like in the moment, and
|
|
62
|
+
// Token Optimization is supposed to be a precise record, not a mood.
|
|
63
|
+
// The trigger below is mechanical instead: did this turn use a tool,
|
|
64
|
+
// yes or no. That is a fact you can check, not a judgment you make.
|
|
65
|
+
'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. A turn that is pure conversation with no tool use at all is the only kind that does not need it. Give the exact model id and how many tokens you expect to use. 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. 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.',
|
|
55
66
|
},
|
|
56
67
|
{ capabilities: { tools: {} } },
|
|
57
68
|
);
|
|
@@ -94,9 +105,10 @@ export async function runMcp() {
|
|
|
94
105
|
},
|
|
95
106
|
},
|
|
96
107
|
{
|
|
97
|
-
name: '
|
|
108
|
+
name: 'wait_for_approval',
|
|
98
109
|
description:
|
|
99
|
-
'Poll Troxy every 10 seconds until a pending
|
|
110
|
+
'Poll Troxy every 10 seconds until a pending approval is resolved. ' +
|
|
111
|
+
'Applies to every ESCALATE decision from any evaluate_* tool (payment, email, login, action, or model), not just payments. ' +
|
|
100
112
|
'Call this immediately after receiving an ESCALATE decision. ' +
|
|
101
113
|
'It will block until the account owner approves or declines (up to 10 minutes), then return the result automatically.',
|
|
102
114
|
inputSchema: {
|
|
@@ -267,8 +279,9 @@ export async function runMcp() {
|
|
|
267
279
|
description:
|
|
268
280
|
'Evaluate whether running a task on a given AI model should be allowed, blocked, or escalated ' +
|
|
269
281
|
'based on your Troxy policies, and record what it is expected to cost. ' +
|
|
270
|
-
'Call this
|
|
271
|
-
'
|
|
282
|
+
'Call this at the start of any turn where you will use a tool of any kind, and whenever you switch model or effort level mid-turn. ' +
|
|
283
|
+
'This is mechanical, not a judgment call: tool use in this turn means call it, no tool use means you can skip it. ' +
|
|
284
|
+
'Returns an audit_id. Keep it: you must pass it to report_model_usage at the end of the turn.',
|
|
272
285
|
inputSchema: {
|
|
273
286
|
type: 'object',
|
|
274
287
|
required: ['model'],
|
|
@@ -308,8 +321,8 @@ export async function runMcp() {
|
|
|
308
321
|
{
|
|
309
322
|
name: 'report_model_usage',
|
|
310
323
|
description:
|
|
311
|
-
'Report how many tokens
|
|
312
|
-
'Call this after every ALLOW or NOTIFY from evaluate_model, using the audit_id it returned. ' +
|
|
324
|
+
'Report how many tokens the turn ACTUALLY used, once it has finished. ' +
|
|
325
|
+
'Call this after every ALLOW or NOTIFY from evaluate_model, using the audit_id it returned — no judgment call, every approved evaluate_model gets a matching report. ' +
|
|
313
326
|
'Without it Troxy only has your prediction, and the account owner is told, per agent, that its figures are unverified. ' +
|
|
314
327
|
'Report the real number even when it is far above what you estimated: the gap is the point.',
|
|
315
328
|
inputSchema: {
|
|
@@ -339,7 +352,7 @@ export async function runMcp() {
|
|
|
339
352
|
const toolName = request.params.name;
|
|
340
353
|
const args = request.params.arguments ?? {};
|
|
341
354
|
|
|
342
|
-
if (toolName === '
|
|
355
|
+
if (toolName === 'wait_for_approval') {
|
|
343
356
|
const { approval_token } = args;
|
|
344
357
|
const MAX_MS = 60 * 60_000; // overall cap: 1 hour
|
|
345
358
|
const deadline = Date.now() + MAX_MS;
|
|
@@ -359,19 +372,19 @@ export async function runMcp() {
|
|
|
359
372
|
|
|
360
373
|
if (status.status === 'APPROVED') {
|
|
361
374
|
return {
|
|
362
|
-
content: [{ type: 'text', text: `✅
|
|
375
|
+
content: [{ type: 'text', text: `✅ Approved! Call the same evaluate_* tool again with the original arguments and approval_token: "${approval_token}" to proceed.` }],
|
|
363
376
|
isError: false,
|
|
364
377
|
};
|
|
365
378
|
}
|
|
366
379
|
if (status.status === 'DECLINED') {
|
|
367
380
|
return {
|
|
368
|
-
content: [{ type: 'text', text: `❌
|
|
381
|
+
content: [{ type: 'text', text: `❌ Declined by the account owner. Do not proceed with this action.` }],
|
|
369
382
|
isError: true,
|
|
370
383
|
};
|
|
371
384
|
}
|
|
372
385
|
if (status.status === 'EXPIRED') {
|
|
373
386
|
return {
|
|
374
|
-
content: [{ type: 'text', text: `⏱ Approval request has expired.
|
|
387
|
+
content: [{ type: 'text', text: `⏱ Approval request has expired. It cannot be approved; re-run the evaluate_* call to start over if still needed.` }],
|
|
375
388
|
isError: true,
|
|
376
389
|
};
|
|
377
390
|
}
|
|
@@ -430,7 +443,7 @@ export async function runMcp() {
|
|
|
430
443
|
emailText = `✗ Email blocked.${reason ? ` ${reason}` : ''} Do not send it. (audit: ${audit_id})`;
|
|
431
444
|
break;
|
|
432
445
|
case 'ESCALATE':
|
|
433
|
-
emailText = `⏳ Email requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
446
|
+
emailText = `⏳ Email requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_email again with the same recipient/recipients/subject/body PLUS this approval_token. Do not send until it returns approved.`;
|
|
434
447
|
break;
|
|
435
448
|
default:
|
|
436
449
|
emailText = JSON.stringify(result);
|
|
@@ -464,7 +477,7 @@ export async function runMcp() {
|
|
|
464
477
|
actionText = `✗ Blocked: ${what}.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
|
|
465
478
|
break;
|
|
466
479
|
case 'ESCALATE':
|
|
467
|
-
actionText = `⏳ ${what} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
480
|
+
actionText = `⏳ ${what} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_action again with the same arguments PLUS this approval_token. Do not proceed until it returns approved.`;
|
|
468
481
|
break;
|
|
469
482
|
default:
|
|
470
483
|
actionText = JSON.stringify(result);
|
|
@@ -488,7 +501,7 @@ export async function runMcp() {
|
|
|
488
501
|
// Every non-blocked branch repeats the audit_id and the instruction to
|
|
489
502
|
// report back. Half the value of this checkpoint is the second call, and
|
|
490
503
|
// an agent that is only told "approved" has no reason to make it.
|
|
491
|
-
const followUp = ` When
|
|
504
|
+
const followUp = ` When this turn finishes, call report_model_usage(audit_id="${audit_id}", actual_tokens=<real total>).`;
|
|
492
505
|
let modelText;
|
|
493
506
|
switch (decision) {
|
|
494
507
|
case 'ALLOW':
|
|
@@ -499,7 +512,7 @@ export async function runMcp() {
|
|
|
499
512
|
modelText = `✗ Blocked: ${what}.${reason ? ` ${reason}` : ''} Do not use this model for this task. Choose a cheaper or smaller model and call evaluate_model again with the new model id. (audit: ${audit_id})`;
|
|
500
513
|
break;
|
|
501
514
|
case 'ESCALATE':
|
|
502
|
-
modelText = `⏳ ${what} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
515
|
+
modelText = `⏳ ${what} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_model again with the same arguments PLUS this approval_token. Do not run the task until it returns approved.`;
|
|
503
516
|
break;
|
|
504
517
|
default:
|
|
505
518
|
modelText = JSON.stringify(result);
|
|
@@ -555,7 +568,7 @@ export async function runMcp() {
|
|
|
555
568
|
loginText = `✗ ${verb} blocked.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
|
|
556
569
|
break;
|
|
557
570
|
case 'ESCALATE':
|
|
558
|
-
loginText = `⏳ ${verb} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
571
|
+
loginText = `⏳ ${verb} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval, then call evaluate_login again with the same site/login_action PLUS this approval_token. Do not proceed until it returns approved.`;
|
|
559
572
|
break;
|
|
560
573
|
default:
|
|
561
574
|
loginText = JSON.stringify(result);
|
|
@@ -592,7 +605,7 @@ export async function runMcp() {
|
|
|
592
605
|
text = `✗ Payment blocked by policy "${policy}". Do not proceed with this payment. (audit: ${audit_id})`;
|
|
593
606
|
break;
|
|
594
607
|
case 'ESCALATE':
|
|
595
|
-
text = `⏳ Payment requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
608
|
+
text = `⏳ Payment requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call wait_for_approval(approval_token="${approval_token}") to automatically detect approval. Do not proceed until it returns approved. (audit: ${audit_id})`;
|
|
596
609
|
break;
|
|
597
610
|
case 'NOTIFY':
|
|
598
611
|
text = `✓ Payment approved with notification. Policy matched: "${policy}". (audit: ${audit_id})\n\nAfter the charge attempt completes, call confirm_payment with audit_id "${audit_id}" and status "success", "failed", or "cancelled".`;
|
|
@@ -98,3 +98,71 @@ test('the model id is asked for exactly, not as a friendly name', () => {
|
|
|
98
98
|
'send family names and be priced as the wrong generation',
|
|
99
99
|
);
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
test('the heartbeat reports which CLI version is running', () => {
|
|
103
|
+
// An old CLI does not fail, it simply never calls a checkpoint it has never
|
|
104
|
+
// heard of, so the affected page looks broken rather than out of date.
|
|
105
|
+
// Before 1.14.0 Token Optimization was empty for every MCP agent and nothing
|
|
106
|
+
// anywhere said why. The dashboard can only explain that if it knows what is
|
|
107
|
+
// running.
|
|
108
|
+
assert.ok(
|
|
109
|
+
/CLI_VERSION/.test(apiSrc),
|
|
110
|
+
'api.js does not read its own version',
|
|
111
|
+
);
|
|
112
|
+
const hb = apiSrc.slice(apiSrc.indexOf('mcpHeartbeat:'));
|
|
113
|
+
assert.ok(
|
|
114
|
+
/cli_version: CLI_VERSION/.test(hb.slice(0, 400)),
|
|
115
|
+
'the heartbeat does not send the CLI version',
|
|
116
|
+
);
|
|
117
|
+
// Every heartbeat, not only the ones carrying a name: a long-running agent
|
|
118
|
+
// that never re-inits would otherwise never report its version at all.
|
|
119
|
+
assert.ok(
|
|
120
|
+
!/agentName \? \{[^}]*cli_version/.test(hb.slice(0, 400)),
|
|
121
|
+
'cli_version is only sent when an agent name is present, so a steady-state '
|
|
122
|
+
+ 'agent never reports its version',
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('the version is read from package.json, not hardcoded', () => {
|
|
127
|
+
// A hardcoded string drifts from the published version the moment someone
|
|
128
|
+
// bumps one and not the other, and then the dashboard advises upgrading a
|
|
129
|
+
// CLI that is already current.
|
|
130
|
+
assert.ok(
|
|
131
|
+
/createRequire/.test(apiSrc) && /package\.json/.test(apiSrc),
|
|
132
|
+
'CLI_VERSION is not derived from package.json',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('the checkpoint trigger is mechanical, not a judgment call', () => {
|
|
137
|
+
// "When you begin a substantial task" was tried first and driven live: the
|
|
138
|
+
// same agent, same session, called evaluate_model to review two source
|
|
139
|
+
// files, then minutes later pulled real data from an actual connected
|
|
140
|
+
// business (orders, customer messages, an artifact) and called nothing.
|
|
141
|
+
// Both were real work. "Substantial" meant whatever the model felt like in
|
|
142
|
+
// the moment, which is not what a precise cost record can be built on.
|
|
143
|
+
// Sliced from the opening quote of the instructions STRING, not the
|
|
144
|
+
// instructions: field or file start, so the explanatory code comment
|
|
145
|
+
// above it (which deliberately quotes the retired word for context) does
|
|
146
|
+
// not trip this check.
|
|
147
|
+
const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
|
|
148
|
+
assert.ok(
|
|
149
|
+
!/substantial/i.test(instructions),
|
|
150
|
+
'the instructions still gate the checkpoint on "substantial", a judgment '
|
|
151
|
+
+ 'call proven inconsistent on the same agent in the same session',
|
|
152
|
+
);
|
|
153
|
+
assert.ok(
|
|
154
|
+
/tool/i.test(instructions) && /every time/i.test(instructions),
|
|
155
|
+
'the instructions do not state a mechanical, checkable trigger (tool use) '
|
|
156
|
+
+ 'to replace the judgment call that was removed',
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('evaluate_model\'s own tool description matches the mechanical trigger', () => {
|
|
161
|
+
// The top-level instructions are what an agent reads once; the tool
|
|
162
|
+
// description is what it sees every time it considers calling this
|
|
163
|
+
// specific tool. Both have to agree, or whichever one it reads last wins.
|
|
164
|
+
const schema = src.slice(src.indexOf("name: 'evaluate_model'"));
|
|
165
|
+
const desc = schema.slice(0, schema.indexOf('inputSchema'));
|
|
166
|
+
assert.ok(!/substantial/i.test(desc), 'the tool description still says "substantial"');
|
|
167
|
+
assert.ok(/tool/i.test(desc), 'the tool description does not mention the tool-use trigger');
|
|
168
|
+
});
|