troxy-cli 1.14.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 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
- | `wait_for_payment_approval` | (after ESCALATE) waiting on the owner's decision |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "Control layer for AI agents: check payments, emails, logins and destructive actions against your policies",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
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
- mcpHeartbeat: (apiKey, agentName, force) => request('POST', '/mcp/heartbeat', { apiKey, body: agentName ? { agent_name: agentName, force_name: !!force } : undefined }),
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,17 +36,17 @@ export async function runMcp() {
36
36
  name: 'troxy',
37
37
  version: '0.1.0',
38
38
  instructions:
39
- 'You are connected to Troxy, a payment control system. ' +
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 wait_for_payment_approval with that token; it polls automatically until the payment 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. ' +
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 wait_for_payment_approval with the returned approval_token (same wait mechanism payments use), then call evaluate_email again with the approval_token once approved. ' +
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 wait_for_payment_approval with the returned approval_token, then call evaluate_login again with the approval_token once approved. ' +
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
@@ -94,9 +94,10 @@ export async function runMcp() {
94
94
  },
95
95
  },
96
96
  {
97
- name: 'wait_for_payment_approval',
97
+ name: 'wait_for_approval',
98
98
  description:
99
- 'Poll Troxy every 10 seconds until a pending payment approval is resolved. ' +
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. ' +
100
101
  'Call this immediately after receiving an ESCALATE decision. ' +
101
102
  'It will block until the account owner approves or declines (up to 10 minutes), then return the result automatically.',
102
103
  inputSchema: {
@@ -339,7 +340,7 @@ export async function runMcp() {
339
340
  const toolName = request.params.name;
340
341
  const args = request.params.arguments ?? {};
341
342
 
342
- if (toolName === 'wait_for_payment_approval') {
343
+ if (toolName === 'wait_for_approval') {
343
344
  const { approval_token } = args;
344
345
  const MAX_MS = 60 * 60_000; // overall cap: 1 hour
345
346
  const deadline = Date.now() + MAX_MS;
@@ -359,19 +360,19 @@ export async function runMcp() {
359
360
 
360
361
  if (status.status === 'APPROVED') {
361
362
  return {
362
- content: [{ type: 'text', text: `✅ Payment approved! Call evaluate_payment again with the same payment details and approval_token: "${approval_token}" to proceed.` }],
363
+ content: [{ type: 'text', text: `✅ Approved! Call the same evaluate_* tool again with the original arguments and approval_token: "${approval_token}" to proceed.` }],
363
364
  isError: false,
364
365
  };
365
366
  }
366
367
  if (status.status === 'DECLINED') {
367
368
  return {
368
- content: [{ type: 'text', text: `❌ Payment declined by the account owner. Do not proceed with this payment.` }],
369
+ content: [{ type: 'text', text: `❌ Declined by the account owner. Do not proceed with this action.` }],
369
370
  isError: true,
370
371
  };
371
372
  }
372
373
  if (status.status === 'EXPIRED') {
373
374
  return {
374
- content: [{ type: 'text', text: `⏱ Approval request has expired. The payment cannot be approved.` }],
375
+ content: [{ type: 'text', text: `⏱ Approval request has expired. It cannot be approved; re-run the evaluate_* call to start over if still needed.` }],
375
376
  isError: true,
376
377
  };
377
378
  }
@@ -430,7 +431,7 @@ export async function runMcp() {
430
431
  emailText = `✗ Email blocked.${reason ? ` ${reason}` : ''} Do not send it. (audit: ${audit_id})`;
431
432
  break;
432
433
  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 wait_for_payment_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
+ 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
435
  break;
435
436
  default:
436
437
  emailText = JSON.stringify(result);
@@ -464,7 +465,7 @@ export async function runMcp() {
464
465
  actionText = `✗ Blocked: ${what}.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
465
466
  break;
466
467
  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 wait_for_payment_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
+ 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
469
  break;
469
470
  default:
470
471
  actionText = JSON.stringify(result);
@@ -499,7 +500,7 @@ export async function runMcp() {
499
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})`;
500
501
  break;
501
502
  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 wait_for_payment_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
+ 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
504
  break;
504
505
  default:
505
506
  modelText = JSON.stringify(result);
@@ -555,7 +556,7 @@ export async function runMcp() {
555
556
  loginText = `✗ ${verb} blocked.${reason ? ` ${reason}` : ''} Do not proceed. (audit: ${audit_id})`;
556
557
  break;
557
558
  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 wait_for_payment_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
+ 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
560
  break;
560
561
  default:
561
562
  loginText = JSON.stringify(result);
@@ -592,7 +593,7 @@ export async function runMcp() {
592
593
  text = `✗ Payment blocked by policy "${policy}". Do not proceed with this payment. (audit: ${audit_id})`;
593
594
  break;
594
595
  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 wait_for_payment_approval(approval_token="${approval_token}") to automatically detect approval. Do not proceed until it returns approved. (audit: ${audit_id})`;
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})`;
596
597
  break;
597
598
  case 'NOTIFY':
598
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".`;
@@ -98,3 +98,37 @@ 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
+ });