troxy-cli 1.17.0 → 1.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.17.0",
3
+ "version": "1.18.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/mcp-server.js CHANGED
@@ -62,7 +62,16 @@ export async function runMcp() {
62
62
  // Token Optimization is supposed to be a precise record, not a mood.
63
63
  // The trigger below is mechanical instead: did this turn use a tool,
64
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.',
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, and WAIT for the decision before proceeding. 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; 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. ' +
66
+ // A turn that is PURE conversation, no tool use at all, still gets
67
+ // logged, just not gated. Blocking a chat reply on a network round
68
+ // trip protects against a risk that is vanishingly rare in practice
69
+ // (almost nobody writes a policy to block message content), and the
70
+ // cost of blocking anyway is a slower product on every single message.
71
+ // So chat calls the same two tools, in the same order, but treats the
72
+ // decision as a record rather than a gate: reply to the user first,
73
+ // report afterward, never hold the reply for Troxy's answer.
74
+ 'A turn that is pure conversation, with no tool use at all, is different: you still call evaluate_model and report_model_usage for it, but you do NOT wait for or act on the decision before replying. Reply to the user first. Then, as the last step of that same turn (or the first step of your next turn if your environment does not let you act again after producing a reply), call evaluate_model with the model and your best token estimate, and call report_model_usage with the real total right after — same two tools as above, just not gating. If the decision comes back BLOCK or ESCALATE for a chat turn, do not retry with a different model and do not poll wait_for_approval: the reply has already been sent, there is nothing left to gate, and the decision is recorded for the account owner\'s visibility rather than enforced in the moment. Chat usage still counts toward every rolling spend and token limit, so it still shapes what the next tool-use call is allowed to do.',
66
75
  },
67
76
  { capabilities: { tools: {} } },
68
77
  );
@@ -279,9 +288,10 @@ export async function runMcp() {
279
288
  description:
280
289
  'Evaluate whether running a task on a given AI model should be allowed, blocked, or escalated ' +
281
290
  'based on your Troxy policies, and record what it is expected to cost. ' +
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.',
291
+ 'Call this on every turn: at the start, before proceeding, if you will use a tool of any kind, or whenever you switch model or effort level mid-turn; ' +
292
+ 'or after replying, without waiting for the decision, if the turn was pure conversation with no tool use. ' +
293
+ 'Tool use gates on the decision; chat only records it. ' +
294
+ 'Returns an audit_id. Keep it: you must pass it to report_model_usage when the turn finishes.',
285
295
  inputSchema: {
286
296
  type: 'object',
287
297
  required: ['model'],
@@ -166,3 +166,82 @@ test('evaluate_model\'s own tool description matches the mechanical trigger', ()
166
166
  assert.ok(!/substantial/i.test(desc), 'the tool description still says "substantial"');
167
167
  assert.ok(/tool/i.test(desc), 'the tool description does not mention the tool-use trigger');
168
168
  });
169
+
170
+ test('pure chat still reports usage, it just does not gate on the decision', () => {
171
+ // The first version of this fix (tool-use = mechanical trigger) left chat
172
+ // unreported entirely, on the reasoning that chat isn't "work." That
173
+ // reasoning doesn't survive contact with the goal: chat still costs real
174
+ // tokens, and a page that silently misses all chat-only usage is not a
175
+ // precise record, it just moved the same gap somewhere less visible.
176
+ const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
177
+ const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
178
+ assert.ok(chatSeg, 'no instruction exists for the pure-conversation case at all');
179
+ assert.ok(
180
+ /still call evaluate_model and report_model_usage/.test(chatSeg),
181
+ 'chat turns are not instructed to call the checkpoint tools at all, so '
182
+ + 'chat-only usage is invisible on Token Optimization',
183
+ );
184
+ });
185
+
186
+ test('chat is told explicitly not to block the reply on the decision', () => {
187
+ // The whole point of treating chat differently from tool use is that a
188
+ // human is waiting on the reply. If the instructions still say "wait for
189
+ // the decision" for chat, this fix adds latency to every message for no
190
+ // behavioural gain over the tool-use path it was meant to avoid copying.
191
+ const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
192
+ const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
193
+ assert.ok(
194
+ /do NOT wait for or act on the decision before replying/.test(chatSeg),
195
+ 'chat instructions do not say to reply before waiting on the checkpoint',
196
+ );
197
+ assert.ok(
198
+ /Reply to the user first/.test(chatSeg),
199
+ 'chat instructions do not establish reply-then-report ordering',
200
+ );
201
+ });
202
+
203
+ test('a BLOCK or ESCALATE on a chat turn does not trigger retry or polling', () => {
204
+ // Retrying with a cheaper model, or polling wait_for_approval, are the
205
+ // tool-use responses to BLOCK/ESCALATE. For chat the reply is already sent
206
+ // by the time the decision comes back, so both of those actions are
207
+ // meaningless and would just make the agent stall on a message that
208
+ // already went out.
209
+ const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
210
+ const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
211
+ assert.ok(
212
+ /do not retry with a different model and do not poll wait_for_approval/.test(chatSeg),
213
+ 'chat instructions do not rule out the tool-use recovery actions, which '
214
+ + 'would stall a reply that has already been sent',
215
+ );
216
+ });
217
+
218
+ test('chat usage still feeds the rolling limits that gate tool use', () => {
219
+ // Not gating chat in real time is only an acceptable trade if chat spend
220
+ // still counts toward spend_per_hour / tokens_per_day, so a chat-heavy
221
+ // agent still gets caught the moment it tries to use a tool. Otherwise
222
+ // "we don't block chat" quietly becomes "chat spend is uncapped."
223
+ const instructions = src.slice(src.indexOf("'You are connected to Troxy"), src.indexOf('capabilities'));
224
+ const chatSeg = instructions.slice(instructions.indexOf('A turn that is pure conversation'));
225
+ assert.ok(
226
+ /still counts toward every rolling spend and token limit/.test(chatSeg),
227
+ 'nothing tells the agent that unblocked chat spend still accumulates '
228
+ + 'toward the limits a later tool-use call is checked against',
229
+ );
230
+ });
231
+
232
+ test('no new tool was introduced for the chat path', () => {
233
+ // The backend already supports this: report_model_usage looks up its row
234
+ // by id/user/agent, never by decision, so it works identically whether the
235
+ // row says ALLOW, BLOCK, or ESCALATE. Reusing evaluate_model +
236
+ // report_model_usage for chat means zero backend changes and zero new
237
+ // surface area to get wrong; a bespoke chat-logging tool would duplicate a
238
+ // path that is already tested end to end.
239
+ // Scoped to the tools array, not the whole file: the Server constructor
240
+ // also has a top-level `name: 'troxy'` for the server itself, which the
241
+ // unscoped regex was catching as a ninth "tool".
242
+ const toolsSeg = src.slice(src.indexOf('tools: ['), src.indexOf('server.setRequestHandler(CallToolRequestSchema'));
243
+ const toolNames = [...toolsSeg.matchAll(/name: '([a-z_]+)'/g)].map(m => m[1]);
244
+ const unique = new Set(toolNames);
245
+ assert.strictEqual(unique.size, 8, 'a tool was added or removed for this change');
246
+ assert.ok(unique.has('evaluate_model') && unique.has('report_model_usage'));
247
+ });