ambitry 0.1.0 → 0.1.2

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/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve } from 'node:path';
3
+ import { rmSync } from 'node:fs';
3
4
  import { Store } from "./db.js";
4
5
  import { Controls, loadPolicy } from "./policy.js";
5
6
  import { createServer } from "./server.js";
@@ -67,6 +68,13 @@ function main(argv) {
67
68
  const port = Number(flag(argv, 'port') ?? 8787);
68
69
  // Demo traffic goes to its own file so it never mixes with real traces.
69
70
  const dbPath = resolve(flag(argv, 'db') ?? (demo ? 'ambitry-demo.db' : 'ambitry.db'));
71
+ // Each demo run starts clean. Otherwise a second run doubles every number
72
+ // on the dashboard, which is confusing on its own and useless to record.
73
+ if (demo) {
74
+ for (const suffix of ['', '-journal', '-wal', '-shm']) {
75
+ rmSync(`${dbPath}${suffix}`, { force: true });
76
+ }
77
+ }
70
78
  const policyPath = resolve(flag(argv, 'policy') ?? 'ambitry.json');
71
79
  let controls;
72
80
  try {
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
@@ -112,9 +125,14 @@ async function refresh() {
112
125
  // The headline. What people came for is "am I leaking anything", and the
113
126
  // answer belongs above the fold, not inside a trace nobody opens.
114
127
  const f = status.findings || [];
128
+ const total = f.reduce((n, x) => n + x.occurrences, 0);
129
+ // "Credentials" only when they all are. An email address is personal data,
130
+ // not a credential, and this line is the first thing anyone reads — the
131
+ // whole positioning rests on not overstating.
132
+ const allCreds = f.every(x => x.severity === 'critical');
133
+ const noun = allCreds ? 'credential' : 'sensitive value';
115
134
  $('alert').innerHTML = !f.length ? '' :
116
- '<div class="alert"><h3>' + f.reduce((n, x) => n + x.occurrences, 0) +
117
- ' credential' + (f.reduce((n, x) => n + x.occurrences, 0) === 1 ? '' : 's') +
135
+ '<div class="alert"><h3>' + total + ' ' + noun + (total === 1 ? '' : 's') +
118
136
  ' sent to your model provider</h3>' +
119
137
  '<p>Found in outgoing prompts over the last 7 days. The finding keeps only a masked preview — ' +
120
138
  'but the prompt itself is in your local trace file, so treat that file as sensitive.</p>' +
@@ -122,7 +140,11 @@ async function refresh() {
122
140
  '<span class="mono">' + x.occurrences + '× in ' + x.traces + ' request' + (x.traces === 1 ? '' : 's') +
123
141
  '</span></div>').join('') + '</div>';
124
142
 
125
- const blocked = traces.filter(t => t.status === 403).length;
143
+ // Counts denied tool calls as well as rejected requests. Tool denial
144
+ // returns 200 with the call stripped, so counting status codes alone
145
+ // reports zero blocks while enforcement is visibly working.
146
+ const blocked = traces.reduce((n, t) =>
147
+ n + (t.denied_count || 0) + (t.status === 403 ? 1 : 0), 0);
126
148
  $('cards').innerHTML =
127
149
  meter('Spend · last hour', status.spend.lastHour, status.limits.perHourUsd) +
128
150
  meter('Spend · last day', status.spend.lastDay, status.limits.perDayUsd) +
@@ -143,7 +165,7 @@ async function refresh() {
143
165
  '<td>' + new Date(t.started_at).toLocaleTimeString() +
144
166
  (t.status === 403 ? ' <span class="tag no">blocked</span>' : '') + '</td>' +
145
167
  '<td>' + esc(t.model || '—') + '</td>' +
146
- '<td class="muted">' + (t.streamed ? 'stream' : '') + '</td>' +
168
+ '<td>' + toolCells(t) + '</td>' +
147
169
  '<td class="num">' + ((t.input_tokens ?? 0) + (t.output_tokens ?? 0) || '—') + '</td>' +
148
170
  '<td class="num">' + money(t.cost_usd) + '</td></tr>').join('') +
149
171
  '</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.2",
4
4
  "description": "Audit log and permission layer for AI agents. One-line integration, zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {