troxy-cli 1.13.0 → 1.16.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 +18 -0
- package/package.json +1 -1
- package/src/api.js +30 -1
- package/src/mcp-server.js +146 -14
- package/src/tests/model-checkpoint.test.js +134 -0
package/README.md
CHANGED
|
@@ -67,6 +67,24 @@ npx troxy-cli <command>
|
|
|
67
67
|
|
|
68
68
|
The CLI also ships an MCP server (`src/mcp-server.js`) that exposes Troxy as a tool for Claude and other MCP-compatible agents.
|
|
69
69
|
|
|
70
|
+
Checkpoints an agent can call:
|
|
71
|
+
|
|
72
|
+
| Tool | Call it before |
|
|
73
|
+
|------|----------------|
|
|
74
|
+
| `evaluate_payment` | paying, buying, transferring funds |
|
|
75
|
+
| `confirm_payment` | (after) reporting whether the charge succeeded |
|
|
76
|
+
| `evaluate_email` | sending an email or message |
|
|
77
|
+
| `evaluate_action` | deleting, dropping, force-pushing, revoking |
|
|
78
|
+
| `evaluate_login` | logging into or signing up for a site |
|
|
79
|
+
| `evaluate_model` | running a task on a model, and on any model or effort switch |
|
|
80
|
+
| `report_model_usage` | (after) reporting the real token total |
|
|
81
|
+
| `wait_for_approval` | (after ESCALATE) waiting on the owner's decision |
|
|
82
|
+
|
|
83
|
+
The model checkpoint is the only one that is a required pair. `evaluate_model`
|
|
84
|
+
records what a run is expected to cost; `report_model_usage` records what it
|
|
85
|
+
actually cost. Without the second call Troxy holds only the agent's own
|
|
86
|
+
prediction, and the dashboard marks that agent's figures as unverified.
|
|
87
|
+
|
|
70
88
|
## Auth flow
|
|
71
89
|
|
|
72
90
|
`troxy login` uses a device-code flow:
|
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;
|
|
@@ -74,13 +90,26 @@ export const api = {
|
|
|
74
90
|
evaluateEmail: (body, apiKey) => request('POST', '/evaluate/email', { apiKey, body }),
|
|
75
91
|
evaluateLogin: (body, apiKey) => request('POST', '/evaluate/login', { apiKey, body }),
|
|
76
92
|
evaluateAction: (body, apiKey) => request('POST', '/evaluate/action', { apiKey, body }),
|
|
93
|
+
// The model checkpoint is two calls: ask before the run, report after it.
|
|
94
|
+
// Only the pair produces usable data. The estimate alone is the agent's own
|
|
95
|
+
// guess, and Token Optimization says so on every agent that never reports.
|
|
96
|
+
evaluateModel: (body, apiKey) => request('POST', '/evaluate/model', { apiKey, body }),
|
|
97
|
+
reportModelUsage: (body, apiKey) => request('POST', '/evaluate/model/complete', { apiKey, body }),
|
|
77
98
|
selfRevoke: (apiKey) => request('POST', '/mcp/self-revoke', { apiKey }),
|
|
78
99
|
revokeAllOthers: (apiKey) => request('POST', '/mcp/revoke-all-others', { apiKey }),
|
|
79
100
|
confirmPayment: (auditId, body, apiKey) => request('POST', `/payments/${auditId}/confirm`, { apiKey, body }),
|
|
80
101
|
waitApprovalStatus: (token) => request('GET', `/approvals/${encodeURIComponent(token)}/wait`),
|
|
81
102
|
|
|
82
103
|
// MCP heartbeat (agent API key)
|
|
83
|
-
|
|
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
|
+
}),
|
|
84
113
|
|
|
85
114
|
// MCP status (agent API key — no login needed)
|
|
86
115
|
mcpStatus: (apiKey) => request('GET', '/mcp/status', { apiKey }),
|
package/src/mcp-server.js
CHANGED
|
@@ -10,6 +10,8 @@ const confirmPayment = (auditId, body, apiKey) => api.confirmPayment(auditId, bo
|
|
|
10
10
|
const evaluateEmail = (body, apiKey) => api.evaluateEmail(body, apiKey);
|
|
11
11
|
const evaluateLogin = (body, apiKey) => api.evaluateLogin(body, apiKey);
|
|
12
12
|
const evaluateAction = (body, apiKey) => api.evaluateAction(body, apiKey);
|
|
13
|
+
const evaluateModel = (body, apiKey) => api.evaluateModel(body, apiKey);
|
|
14
|
+
const reportModelUsage = (body, apiKey) => api.reportModelUsage(body, apiKey);
|
|
13
15
|
|
|
14
16
|
export async function runMcp() {
|
|
15
17
|
const config = loadConfig();
|
|
@@ -34,17 +36,22 @@ export async function runMcp() {
|
|
|
34
36
|
name: 'troxy',
|
|
35
37
|
version: '0.1.0',
|
|
36
38
|
instructions:
|
|
37
|
-
'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. ' +
|
|
38
40
|
'You MUST call the evaluate_payment tool before completing any purchase, payment, or financial transaction. ' +
|
|
39
41
|
'Never submit a payment form, confirm a checkout, or transfer funds without first receiving an ALLOW decision from evaluate_payment. ' +
|
|
40
42
|
'If the decision is BLOCK, abort the transaction and inform the user. ' +
|
|
41
|
-
'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. ' +
|
|
42
44
|
'If the decision is NOTIFY, proceed but the user will be notified. ' +
|
|
43
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. ' +
|
|
44
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. ' +
|
|
45
|
-
'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. ' +
|
|
46
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. ' +
|
|
47
|
-
'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
|
+
// The model checkpoint is the only one that is a pair. The estimate on
|
|
51
|
+
// its own is the agent's own prediction, and the dashboard labels every
|
|
52
|
+
// agent that never reports back as unverified, so the instruction has
|
|
53
|
+
// to make the second call as non-optional as the first.
|
|
54
|
+
'You MUST also call the evaluate_model tool when you begin a substantial task and whenever you switch model or effort level partway through it, giving the exact model id and how many tokens you expect to use. Do not run the task 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, when the task finishes, 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.',
|
|
48
55
|
},
|
|
49
56
|
{ capabilities: { tools: {} } },
|
|
50
57
|
);
|
|
@@ -87,9 +94,10 @@ export async function runMcp() {
|
|
|
87
94
|
},
|
|
88
95
|
},
|
|
89
96
|
{
|
|
90
|
-
name: '
|
|
97
|
+
name: 'wait_for_approval',
|
|
91
98
|
description:
|
|
92
|
-
'Poll Troxy every 10 seconds until a pending
|
|
99
|
+
'Poll Troxy every 10 seconds until a pending approval is resolved. ' +
|
|
100
|
+
'Applies to every ESCALATE decision from any evaluate_* tool (payment, email, login, action, or model), not just payments. ' +
|
|
93
101
|
'Call this immediately after receiving an ESCALATE decision. ' +
|
|
94
102
|
'It will block until the account owner approves or declines (up to 10 minutes), then return the result automatically.',
|
|
95
103
|
inputSchema: {
|
|
@@ -255,6 +263,76 @@ export async function runMcp() {
|
|
|
255
263
|
},
|
|
256
264
|
},
|
|
257
265
|
},
|
|
266
|
+
{
|
|
267
|
+
name: 'evaluate_model',
|
|
268
|
+
description:
|
|
269
|
+
'Evaluate whether running a task on a given AI model should be allowed, blocked, or escalated ' +
|
|
270
|
+
'based on your Troxy policies, and record what it is expected to cost. ' +
|
|
271
|
+
'Call this when you start a task and whenever you switch model or effort level mid-task. ' +
|
|
272
|
+
'Returns an audit_id. Keep it: you must pass it to report_model_usage when the task finishes.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
required: ['model'],
|
|
276
|
+
properties: {
|
|
277
|
+
model: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
description: 'The full model id you are about to use, e.g. "claude-sonnet-4.5", "gpt-4o-mini". Send the exact id, not a friendly name: Troxy prices per model, and generations of the same family differ (Claude 3 Opus costs three times Opus 4.x).',
|
|
280
|
+
},
|
|
281
|
+
estimated_tokens: {
|
|
282
|
+
type: 'number',
|
|
283
|
+
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.',
|
|
284
|
+
},
|
|
285
|
+
effort: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
enum: ['low', 'medium', 'high'],
|
|
288
|
+
description: 'How much work this task warrants. Report it honestly: "high effort on a simple task" is one of the things Troxy flags as waste.',
|
|
289
|
+
},
|
|
290
|
+
task: {
|
|
291
|
+
type: 'string',
|
|
292
|
+
description: 'One short line on what the model is being asked to do (optional). Shown to the account owner in the audit log.',
|
|
293
|
+
},
|
|
294
|
+
provider: {
|
|
295
|
+
type: 'string',
|
|
296
|
+
description: 'Model provider, e.g. "anthropic", "openai" (optional; inferred from the model id when omitted).',
|
|
297
|
+
},
|
|
298
|
+
agent: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'Name of the agent running the task (optional).',
|
|
301
|
+
},
|
|
302
|
+
approval_token: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
description: 'Approval token from a previous ESCALATE response. Include this to proceed after the user has approved.',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: 'report_model_usage',
|
|
311
|
+
description:
|
|
312
|
+
'Report how many tokens a task ACTUALLY used, once it has finished. ' +
|
|
313
|
+
'Call this after every ALLOW or NOTIFY from evaluate_model, using the audit_id it returned. ' +
|
|
314
|
+
'Without it Troxy only has your prediction, and the account owner is told, per agent, that its figures are unverified. ' +
|
|
315
|
+
'Report the real number even when it is far above what you estimated: the gap is the point.',
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: 'object',
|
|
318
|
+
required: ['audit_id', 'actual_tokens'],
|
|
319
|
+
properties: {
|
|
320
|
+
audit_id: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description: 'The audit_id returned by evaluate_model for this task',
|
|
323
|
+
},
|
|
324
|
+
actual_tokens: {
|
|
325
|
+
type: 'number',
|
|
326
|
+
description: 'Total tokens the task really consumed, input plus output. Use the usage figures your model API returned, not an estimate.',
|
|
327
|
+
},
|
|
328
|
+
effort: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
enum: ['low', 'medium', 'high'],
|
|
331
|
+
description: 'The effort level the task actually took, if it differed from what you declared up front (optional).',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
258
336
|
],
|
|
259
337
|
}));
|
|
260
338
|
|
|
@@ -262,7 +340,7 @@ export async function runMcp() {
|
|
|
262
340
|
const toolName = request.params.name;
|
|
263
341
|
const args = request.params.arguments ?? {};
|
|
264
342
|
|
|
265
|
-
if (toolName === '
|
|
343
|
+
if (toolName === 'wait_for_approval') {
|
|
266
344
|
const { approval_token } = args;
|
|
267
345
|
const MAX_MS = 60 * 60_000; // overall cap: 1 hour
|
|
268
346
|
const deadline = Date.now() + MAX_MS;
|
|
@@ -282,19 +360,19 @@ export async function runMcp() {
|
|
|
282
360
|
|
|
283
361
|
if (status.status === 'APPROVED') {
|
|
284
362
|
return {
|
|
285
|
-
content: [{ type: 'text', text: `✅
|
|
363
|
+
content: [{ type: 'text', text: `✅ Approved! Call the same evaluate_* tool again with the original arguments and approval_token: "${approval_token}" to proceed.` }],
|
|
286
364
|
isError: false,
|
|
287
365
|
};
|
|
288
366
|
}
|
|
289
367
|
if (status.status === 'DECLINED') {
|
|
290
368
|
return {
|
|
291
|
-
content: [{ type: 'text', text: `❌
|
|
369
|
+
content: [{ type: 'text', text: `❌ Declined by the account owner. Do not proceed with this action.` }],
|
|
292
370
|
isError: true,
|
|
293
371
|
};
|
|
294
372
|
}
|
|
295
373
|
if (status.status === 'EXPIRED') {
|
|
296
374
|
return {
|
|
297
|
-
content: [{ type: 'text', text: `⏱ Approval request has expired.
|
|
375
|
+
content: [{ type: 'text', text: `⏱ Approval request has expired. It cannot be approved; re-run the evaluate_* call to start over if still needed.` }],
|
|
298
376
|
isError: true,
|
|
299
377
|
};
|
|
300
378
|
}
|
|
@@ -353,7 +431,7 @@ export async function runMcp() {
|
|
|
353
431
|
emailText = `✗ Email blocked.${reason ? ` ${reason}` : ''} Do not send it. (audit: ${audit_id})`;
|
|
354
432
|
break;
|
|
355
433
|
case 'ESCALATE':
|
|
356
|
-
emailText = `⏳ Email requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
434
|
+
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.`;
|
|
357
435
|
break;
|
|
358
436
|
default:
|
|
359
437
|
emailText = JSON.stringify(result);
|
|
@@ -387,7 +465,7 @@ export async function runMcp() {
|
|
|
387
465
|
actionText = `✗ Blocked: ${what}.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
|
|
388
466
|
break;
|
|
389
467
|
case 'ESCALATE':
|
|
390
|
-
actionText = `⏳ ${what} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
468
|
+
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.`;
|
|
391
469
|
break;
|
|
392
470
|
default:
|
|
393
471
|
actionText = JSON.stringify(result);
|
|
@@ -395,6 +473,60 @@ export async function runMcp() {
|
|
|
395
473
|
return { content: [{ type: 'text', text: actionText }], isError: decision === 'BLOCK' };
|
|
396
474
|
}
|
|
397
475
|
|
|
476
|
+
if (toolName === 'evaluate_model') {
|
|
477
|
+
if (agentName && !args.agent) args.agent = agentName;
|
|
478
|
+
let result;
|
|
479
|
+
try {
|
|
480
|
+
result = await evaluateModel(args, apiKey);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
return { content: [{ type: 'text', text: `Troxy error: ${err.message}` }], isError: true };
|
|
483
|
+
}
|
|
484
|
+
if (result.error) {
|
|
485
|
+
return { content: [{ type: 'text', text: `Troxy error: ${result.error}` }], isError: true };
|
|
486
|
+
}
|
|
487
|
+
const { decision, reason, audit_id, approval_token } = result;
|
|
488
|
+
const what = `${args.model}${args.effort ? ` at ${args.effort} effort` : ''}`;
|
|
489
|
+
// Every non-blocked branch repeats the audit_id and the instruction to
|
|
490
|
+
// report back. Half the value of this checkpoint is the second call, and
|
|
491
|
+
// an agent that is only told "approved" has no reason to make it.
|
|
492
|
+
const followUp = ` When the task finishes, call report_model_usage(audit_id="${audit_id}", actual_tokens=<real total>).`;
|
|
493
|
+
let modelText;
|
|
494
|
+
switch (decision) {
|
|
495
|
+
case 'ALLOW':
|
|
496
|
+
case 'NOTIFY':
|
|
497
|
+
modelText = `✓ Approved: ${what}.${reason ? ` ${reason}` : ''} You may run it.${followUp} (audit: ${audit_id})`;
|
|
498
|
+
break;
|
|
499
|
+
case 'BLOCK':
|
|
500
|
+
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})`;
|
|
501
|
+
break;
|
|
502
|
+
case 'ESCALATE':
|
|
503
|
+
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.`;
|
|
504
|
+
break;
|
|
505
|
+
default:
|
|
506
|
+
modelText = JSON.stringify(result);
|
|
507
|
+
}
|
|
508
|
+
return { content: [{ type: 'text', text: modelText }], isError: decision === 'BLOCK' };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (toolName === 'report_model_usage') {
|
|
512
|
+
let result;
|
|
513
|
+
try {
|
|
514
|
+
result = await reportModelUsage(args, apiKey);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
return { content: [{ type: 'text', text: `Troxy error: ${err.message}` }], isError: true };
|
|
517
|
+
}
|
|
518
|
+
if (result.error) {
|
|
519
|
+
return { content: [{ type: 'text', text: `Troxy error: ${result.error}` }], isError: true };
|
|
520
|
+
}
|
|
521
|
+
const cost = typeof result.actual_cost === 'number'
|
|
522
|
+
? ` Cost: $${result.actual_cost.toFixed(4)}.`
|
|
523
|
+
: '';
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: 'text', text: `✓ Usage recorded: ${result.actual_tokens} tokens.${cost} (audit: ${args.audit_id})` }],
|
|
526
|
+
isError: false,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
398
530
|
if (toolName === 'evaluate_login') {
|
|
399
531
|
if (agentName && !args.agent) args.agent = agentName;
|
|
400
532
|
let result;
|
|
@@ -424,7 +556,7 @@ export async function runMcp() {
|
|
|
424
556
|
loginText = `✗ ${verb} blocked.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
|
|
425
557
|
break;
|
|
426
558
|
case 'ESCALATE':
|
|
427
|
-
loginText = `⏳ ${verb} requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
559
|
+
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.`;
|
|
428
560
|
break;
|
|
429
561
|
default:
|
|
430
562
|
loginText = JSON.stringify(result);
|
|
@@ -461,7 +593,7 @@ export async function runMcp() {
|
|
|
461
593
|
text = `✗ Payment blocked by policy "${policy}". Do not proceed with this payment. (audit: ${audit_id})`;
|
|
462
594
|
break;
|
|
463
595
|
case 'ESCALATE':
|
|
464
|
-
text = `⏳ Payment requires human approval; a request has been sent to the account owner.\n\nApproval token: ${approval_token}\n\nNow call
|
|
596
|
+
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})`;
|
|
465
597
|
break;
|
|
466
598
|
case 'NOTIFY':
|
|
467
599
|
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".`;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The model checkpoint has to be reachable, and it has to be a pair.
|
|
2
|
+
//
|
|
3
|
+
// Until 2026-08-06 the API had /evaluate/model and /evaluate/model/complete and
|
|
4
|
+
// the MCP server exposed neither. Any agent connected over MCP, which is how
|
|
5
|
+
// most users connect, could not report model usage at all, so Token
|
|
6
|
+
// Optimization was permanently empty for them and nothing on the page said why.
|
|
7
|
+
// Verified on the live database that day: every model row on a real account was
|
|
8
|
+
// hand-made test data. Nothing had ever reported on its own.
|
|
9
|
+
//
|
|
10
|
+
// The subtler half is the report. An estimate on its own is the agent's own
|
|
11
|
+
// prediction of what a run would cost, which is exactly the number the
|
|
12
|
+
// dashboard refuses to present as fact. A tool list that offers the check
|
|
13
|
+
// without the report reintroduces that gap while looking complete.
|
|
14
|
+
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import assert from 'node:assert';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const src = readFileSync(
|
|
22
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', 'mcp-server.js'),
|
|
23
|
+
'utf8',
|
|
24
|
+
);
|
|
25
|
+
const apiSrc = readFileSync(
|
|
26
|
+
join(dirname(fileURLToPath(import.meta.url)), '..', 'api.js'),
|
|
27
|
+
'utf8',
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
test('both halves of the model checkpoint are exposed as tools', () => {
|
|
31
|
+
for (const name of ['evaluate_model', 'report_model_usage']) {
|
|
32
|
+
assert.ok(
|
|
33
|
+
src.includes(`name: '${name}'`),
|
|
34
|
+
`${name} is not in the tool list, so an MCP agent cannot call it and ` +
|
|
35
|
+
`Token Optimization stays empty for every agent connected this way`,
|
|
36
|
+
);
|
|
37
|
+
assert.ok(
|
|
38
|
+
src.includes(`toolName === '${name}'`),
|
|
39
|
+
`${name} is advertised but has no handler, so calling it does nothing`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('both endpoints are wired in the api client', () => {
|
|
45
|
+
assert.ok(apiSrc.includes("'/evaluate/model'"), 'evaluate/model not wired');
|
|
46
|
+
assert.ok(
|
|
47
|
+
apiSrc.includes("'/evaluate/model/complete'"),
|
|
48
|
+
'evaluate/model/complete not wired, so actuals can never be reported',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the server instructions tell the agent to do both', () => {
|
|
53
|
+
// Checkpoints are cooperative: an agent only calls in because it was told
|
|
54
|
+
// to. A tool nobody is instructed to use is a tool nobody uses.
|
|
55
|
+
const instructions = src.slice(0, src.indexOf('capabilities'));
|
|
56
|
+
assert.ok(
|
|
57
|
+
instructions.includes('evaluate_model'),
|
|
58
|
+
'nothing in the server instructions tells the agent to check its model, ' +
|
|
59
|
+
'so the tool exists and never gets called',
|
|
60
|
+
);
|
|
61
|
+
assert.ok(
|
|
62
|
+
instructions.includes('report_model_usage'),
|
|
63
|
+
'the instructions ask for the estimate but never for the actual, which ' +
|
|
64
|
+
'is the half that makes the number trustworthy',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('an approved decision repeats the audit_id and asks for the report', () => {
|
|
69
|
+
// The agent has to carry the audit_id from one call to the other. If the
|
|
70
|
+
// ALLOW response does not hand it back along with the instruction, the
|
|
71
|
+
// second call depends on the agent having remembered a tool description.
|
|
72
|
+
const handler = src.slice(src.indexOf("toolName === 'evaluate_model'"));
|
|
73
|
+
const allowBranch = handler.slice(0, handler.indexOf("toolName === 'report_model_usage'"));
|
|
74
|
+
assert.ok(
|
|
75
|
+
allowBranch.includes('report_model_usage(audit_id='),
|
|
76
|
+
'the ALLOW/NOTIFY response does not tell the agent how to report back ' +
|
|
77
|
+
'with the audit_id it was just given',
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a blocked model tells the agent what to do instead', () => {
|
|
82
|
+
const handler = src.slice(src.indexOf("toolName === 'evaluate_model'"));
|
|
83
|
+
const blocked = handler.slice(handler.indexOf("case 'BLOCK'"));
|
|
84
|
+
assert.ok(
|
|
85
|
+
/cheaper or smaller model/.test(blocked.slice(0, 400)),
|
|
86
|
+
'BLOCK says no without saying what to do next, so the agent either ' +
|
|
87
|
+
'stops or retries the same model',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the model id is asked for exactly, not as a friendly name', () => {
|
|
92
|
+
// Generations of one family do not share a price: Claude 3 Opus is $15/$75
|
|
93
|
+
// against $5/$25 for Opus 4.x. "claude opus" prices as the wrong one.
|
|
94
|
+
const schema = src.slice(src.indexOf("name: 'evaluate_model'"));
|
|
95
|
+
assert.ok(
|
|
96
|
+
/exact id, not a friendly name/.test(schema.slice(0, 2000)),
|
|
97
|
+
'the model description does not insist on the exact id, so agents will ' +
|
|
98
|
+
'send family names and be priced as the wrong generation',
|
|
99
|
+
);
|
|
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
|
+
});
|