ambitry 0.1.0 → 0.1.1

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/dist/db.d.ts CHANGED
@@ -14,6 +14,9 @@ export interface TraceRow {
14
14
  request_json: string;
15
15
  response_json: string | null;
16
16
  error: string | null;
17
+ /** Present on recentTraces only: comma-separated tool names, and denials. */
18
+ tool_names?: string | null;
19
+ denied_count?: number;
17
20
  }
18
21
  export interface ToolCallRow {
19
22
  id: string;
@@ -85,6 +88,14 @@ export declare class Store {
85
88
  decision: Decision;
86
89
  started_at: number;
87
90
  }[];
91
+ /**
92
+ * Recent traces, each carrying a summary of its tool calls.
93
+ *
94
+ * The counts are joined here rather than derived client-side because a
95
+ * denied tool call returns HTTP 200 with the call stripped — so anything
96
+ * counting blocks by status code reports zero while enforcement is
97
+ * actively working.
98
+ */
88
99
  recentTraces(limit?: number): TraceRow[];
89
100
  trace(id: string): {
90
101
  trace: TraceRow;
package/dist/db.js CHANGED
@@ -117,9 +117,25 @@ export class Store {
117
117
  order by t.started_at asc`)
118
118
  .all(since);
119
119
  }
120
+ /**
121
+ * Recent traces, each carrying a summary of its tool calls.
122
+ *
123
+ * The counts are joined here rather than derived client-side because a
124
+ * denied tool call returns HTTP 200 with the call stripped — so anything
125
+ * counting blocks by status code reports zero while enforcement is
126
+ * actively working.
127
+ */
120
128
  recentTraces(limit = 100) {
121
129
  return this.db
122
- .prepare(`select * from traces order by started_at desc limit ?`)
130
+ .prepare(`select t.*,
131
+ -- decision:name pairs, because group_concat order is not defined
132
+ -- and the client must know which specific call was denied.
133
+ (select group_concat(c.decision || ':' || c.name, ',')
134
+ from tool_calls c where c.trace_id = t.id) as tool_names,
135
+ (select count(*) from tool_calls c where c.trace_id = t.id and c.decision = 'deny') as denied_count
136
+ from traces t
137
+ order by t.started_at desc
138
+ limit ?`)
123
139
  .all(limit);
124
140
  }
125
141
  trace(id) {
package/dist/viewer.js CHANGED
@@ -90,6 +90,19 @@ const $ = (id) => document.getElementById(id);
90
90
  const money = (n) => n == null ? '—' : '$' + Number(n).toFixed(n < 1 ? 4 : 2);
91
91
  const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
92
92
 
93
+ // Tool names for a row, with denied ones marked. The whole point of the
94
+ // product is visible here, so an empty cell is a wasted line.
95
+ function toolCells(t) {
96
+ const pairs = (t.tool_names || '').split(',').filter(Boolean);
97
+ if (!pairs.length) return '<span class="muted">' + (t.streamed ? 'stream' : '—') + '</span>';
98
+ return pairs.map(p => {
99
+ const cut = p.indexOf(':');
100
+ const decision = p.slice(0, cut);
101
+ const name = p.slice(cut + 1);
102
+ return '<span class="tag ' + (decision === 'deny' ? 'no' : 'ok') + '">' + esc(name) + '</span>';
103
+ }).join('');
104
+ }
105
+
93
106
  function meter(label, spent, cap) {
94
107
  const pct = cap ? Math.min(100, (spent / cap) * 100) : 0;
95
108
  const bar = cap
@@ -122,7 +135,11 @@ async function refresh() {
122
135
  '<span class="mono">' + x.occurrences + '× in ' + x.traces + ' request' + (x.traces === 1 ? '' : 's') +
123
136
  '</span></div>').join('') + '</div>';
124
137
 
125
- const blocked = traces.filter(t => t.status === 403).length;
138
+ // Counts denied tool calls as well as rejected requests. Tool denial
139
+ // returns 200 with the call stripped, so counting status codes alone
140
+ // reports zero blocks while enforcement is visibly working.
141
+ const blocked = traces.reduce((n, t) =>
142
+ n + (t.denied_count || 0) + (t.status === 403 ? 1 : 0), 0);
126
143
  $('cards').innerHTML =
127
144
  meter('Spend · last hour', status.spend.lastHour, status.limits.perHourUsd) +
128
145
  meter('Spend · last day', status.spend.lastDay, status.limits.perDayUsd) +
@@ -143,7 +160,7 @@ async function refresh() {
143
160
  '<td>' + new Date(t.started_at).toLocaleTimeString() +
144
161
  (t.status === 403 ? ' <span class="tag no">blocked</span>' : '') + '</td>' +
145
162
  '<td>' + esc(t.model || '—') + '</td>' +
146
- '<td class="muted">' + (t.streamed ? 'stream' : '') + '</td>' +
163
+ '<td>' + toolCells(t) + '</td>' +
147
164
  '<td class="num">' + ((t.input_tokens ?? 0) + (t.output_tokens ?? 0) || '—') + '</td>' +
148
165
  '<td class="num">' + money(t.cost_usd) + '</td></tr>').join('') +
149
166
  '</tbody></table>';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ambitry",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Audit log and permission layer for AI agents. One-line integration, zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {