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 +8 -0
- package/dist/db.d.ts +11 -0
- package/dist/db.js +17 -1
- package/dist/viewer.js +26 -4
- package/package.json +1 -1
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
|
|
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) => ({'&':'&','<':'<','>':'>'}[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>' +
|
|
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
|
-
|
|
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
|
|
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>';
|