agentverify 1.4.0 → 2.0.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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Agent Guard — runtime protection for AI agent tools.
3
+ *
4
+ * import { createGuard } from 'agentverify/guard'
5
+ * const guard = createGuard({
6
+ * agent: 'support-bot',
7
+ * policy: {
8
+ * allow: ['lookup_order', 'send_email', 'refund'],
9
+ * requireApproval: ['refund'],
10
+ * args: { send_email: { to: { allowDomains: ['acme.com'] } } },
11
+ * rateLimit: { perMinute: 30 },
12
+ * },
13
+ * approve: async call => askOnSlack(call), // human-in-the-loop for risky tools
14
+ * apiKey: process.env.AGENTVERIFY_API_KEY, // optional: see every decision in the dashboard
15
+ * })
16
+ * const tools = guard.wrapAll({ lookup_order, send_email, refund })
17
+ *
18
+ * Every call to a wrapped tool is checked first. A blocked call throws GuardBlockedError (its message is safe to
19
+ * show the model), so a tricked or confused agent cannot take actions you did not allow.
20
+ */
21
+ type ToolFn = (...args: any[]) => any;
22
+ interface ArgRule {
23
+ /** Only these email domains / URL hosts are allowed in this argument (subdomains included). */
24
+ allowDomains?: string[];
25
+ /** Reject values longer than this (stops bulk data being pushed out through one argument). */
26
+ maxLength?: number;
27
+ /** Reject values matching this pattern. */
28
+ deny?: RegExp;
29
+ }
30
+ interface GuardPolicy {
31
+ /** Tools the agent may call. When set, every other tool is blocked. Supports '*' wildcards. */
32
+ allow?: Array<string | RegExp>;
33
+ /** Tools that are always blocked. Checked before allow. */
34
+ deny?: Array<string | RegExp>;
35
+ /** Tools that need a human yes before they run. */
36
+ requireApproval?: Array<string | RegExp>;
37
+ /** Per-tool argument rules, by argument name (for tools called with one object argument). */
38
+ args?: Record<string, Record<string, ArgRule>>;
39
+ /** Calls allowed across all tools. */
40
+ rateLimit?: {
41
+ perMinute?: number;
42
+ perRun?: number;
43
+ };
44
+ /** Block calls whose arguments contain an API key, token, or private key (the usual data-exfiltration path). Default true. */
45
+ blockSecrets?: boolean;
46
+ /** URL hosts any argument may point at. When set, URLs to other hosts are blocked. */
47
+ allowHosts?: string[];
48
+ }
49
+ interface ToolCall {
50
+ tool: string;
51
+ args: unknown[];
52
+ agent: string;
53
+ }
54
+ type Decision = 'allowed' | 'blocked' | 'approved' | 'rejected';
55
+ interface GuardEvent {
56
+ agent: string;
57
+ tool: string;
58
+ decision: Decision;
59
+ reason: string;
60
+ at: string;
61
+ /** Argument preview with secrets masked and long values cut short. Never the full arguments. */
62
+ argsPreview: string;
63
+ durationMs?: number;
64
+ }
65
+ interface GuardOptions {
66
+ agent?: string;
67
+ policy: GuardPolicy;
68
+ /** Called for tools in requireApproval. Return true to let the call run. If missing, those calls are blocked. */
69
+ approve?: (call: ToolCall) => boolean | Promise<boolean>;
70
+ /** Called for every decision (e.g. to log locally). */
71
+ onEvent?: (event: GuardEvent) => void;
72
+ /** Send decisions to your Agent Verify dashboard. */
73
+ apiKey?: string;
74
+ apiUrl?: string;
75
+ /** Inject for tests. */
76
+ fetchFn?: typeof fetch;
77
+ now?: () => number;
78
+ }
79
+ declare class GuardBlockedError extends Error {
80
+ readonly tool: string;
81
+ readonly reason: string;
82
+ constructor(tool: string, reason: string);
83
+ }
84
+ declare function findSecret(value: unknown): string | null;
85
+ /** A short, safe preview of the arguments for logs: secrets masked, values cut to 120 characters. */
86
+ declare function previewArgs(args: unknown[]): string;
87
+ /** The policy decision for one call, before any human approval. Exported for testing and dry runs. */
88
+ declare function checkCall(policy: GuardPolicy, tool: string, args: unknown[]): {
89
+ ok: true;
90
+ needsApproval: boolean;
91
+ } | {
92
+ ok: false;
93
+ reason: string;
94
+ };
95
+ interface Guard {
96
+ wrap<T extends ToolFn>(name: string, fn: T): T;
97
+ wrapAll<T extends Record<string, ToolFn>>(tools: T): T;
98
+ /** Send any queued events now (call before your process exits). */
99
+ flush(): Promise<void>;
100
+ /** Start a new run: resets the per-run call budget. */
101
+ newRun(): void;
102
+ }
103
+ declare function createGuard(options: GuardOptions): Guard;
104
+
105
+ export { type ArgRule, type Decision, type Guard, GuardBlockedError, type GuardEvent, type GuardOptions, type GuardPolicy, type ToolCall, type ToolFn, checkCall, createGuard, findSecret, previewArgs };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Agent Guard — runtime protection for AI agent tools.
3
+ *
4
+ * import { createGuard } from 'agentverify/guard'
5
+ * const guard = createGuard({
6
+ * agent: 'support-bot',
7
+ * policy: {
8
+ * allow: ['lookup_order', 'send_email', 'refund'],
9
+ * requireApproval: ['refund'],
10
+ * args: { send_email: { to: { allowDomains: ['acme.com'] } } },
11
+ * rateLimit: { perMinute: 30 },
12
+ * },
13
+ * approve: async call => askOnSlack(call), // human-in-the-loop for risky tools
14
+ * apiKey: process.env.AGENTVERIFY_API_KEY, // optional: see every decision in the dashboard
15
+ * })
16
+ * const tools = guard.wrapAll({ lookup_order, send_email, refund })
17
+ *
18
+ * Every call to a wrapped tool is checked first. A blocked call throws GuardBlockedError (its message is safe to
19
+ * show the model), so a tricked or confused agent cannot take actions you did not allow.
20
+ */
21
+ type ToolFn = (...args: any[]) => any;
22
+ interface ArgRule {
23
+ /** Only these email domains / URL hosts are allowed in this argument (subdomains included). */
24
+ allowDomains?: string[];
25
+ /** Reject values longer than this (stops bulk data being pushed out through one argument). */
26
+ maxLength?: number;
27
+ /** Reject values matching this pattern. */
28
+ deny?: RegExp;
29
+ }
30
+ interface GuardPolicy {
31
+ /** Tools the agent may call. When set, every other tool is blocked. Supports '*' wildcards. */
32
+ allow?: Array<string | RegExp>;
33
+ /** Tools that are always blocked. Checked before allow. */
34
+ deny?: Array<string | RegExp>;
35
+ /** Tools that need a human yes before they run. */
36
+ requireApproval?: Array<string | RegExp>;
37
+ /** Per-tool argument rules, by argument name (for tools called with one object argument). */
38
+ args?: Record<string, Record<string, ArgRule>>;
39
+ /** Calls allowed across all tools. */
40
+ rateLimit?: {
41
+ perMinute?: number;
42
+ perRun?: number;
43
+ };
44
+ /** Block calls whose arguments contain an API key, token, or private key (the usual data-exfiltration path). Default true. */
45
+ blockSecrets?: boolean;
46
+ /** URL hosts any argument may point at. When set, URLs to other hosts are blocked. */
47
+ allowHosts?: string[];
48
+ }
49
+ interface ToolCall {
50
+ tool: string;
51
+ args: unknown[];
52
+ agent: string;
53
+ }
54
+ type Decision = 'allowed' | 'blocked' | 'approved' | 'rejected';
55
+ interface GuardEvent {
56
+ agent: string;
57
+ tool: string;
58
+ decision: Decision;
59
+ reason: string;
60
+ at: string;
61
+ /** Argument preview with secrets masked and long values cut short. Never the full arguments. */
62
+ argsPreview: string;
63
+ durationMs?: number;
64
+ }
65
+ interface GuardOptions {
66
+ agent?: string;
67
+ policy: GuardPolicy;
68
+ /** Called for tools in requireApproval. Return true to let the call run. If missing, those calls are blocked. */
69
+ approve?: (call: ToolCall) => boolean | Promise<boolean>;
70
+ /** Called for every decision (e.g. to log locally). */
71
+ onEvent?: (event: GuardEvent) => void;
72
+ /** Send decisions to your Agent Verify dashboard. */
73
+ apiKey?: string;
74
+ apiUrl?: string;
75
+ /** Inject for tests. */
76
+ fetchFn?: typeof fetch;
77
+ now?: () => number;
78
+ }
79
+ declare class GuardBlockedError extends Error {
80
+ readonly tool: string;
81
+ readonly reason: string;
82
+ constructor(tool: string, reason: string);
83
+ }
84
+ declare function findSecret(value: unknown): string | null;
85
+ /** A short, safe preview of the arguments for logs: secrets masked, values cut to 120 characters. */
86
+ declare function previewArgs(args: unknown[]): string;
87
+ /** The policy decision for one call, before any human approval. Exported for testing and dry runs. */
88
+ declare function checkCall(policy: GuardPolicy, tool: string, args: unknown[]): {
89
+ ok: true;
90
+ needsApproval: boolean;
91
+ } | {
92
+ ok: false;
93
+ reason: string;
94
+ };
95
+ interface Guard {
96
+ wrap<T extends ToolFn>(name: string, fn: T): T;
97
+ wrapAll<T extends Record<string, ToolFn>>(tools: T): T;
98
+ /** Send any queued events now (call before your process exits). */
99
+ flush(): Promise<void>;
100
+ /** Start a new run: resets the per-run call budget. */
101
+ newRun(): void;
102
+ }
103
+ declare function createGuard(options: GuardOptions): Guard;
104
+
105
+ export { type ArgRule, type Decision, type Guard, GuardBlockedError, type GuardEvent, type GuardOptions, type GuardPolicy, type ToolCall, type ToolFn, checkCall, createGuard, findSecret, previewArgs };
package/dist/guard.js ADDED
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/guard.ts
21
+ var guard_exports = {};
22
+ __export(guard_exports, {
23
+ GuardBlockedError: () => GuardBlockedError,
24
+ checkCall: () => checkCall,
25
+ createGuard: () => createGuard,
26
+ findSecret: () => findSecret,
27
+ previewArgs: () => previewArgs
28
+ });
29
+ module.exports = __toCommonJS(guard_exports);
30
+ var GuardBlockedError = class extends Error {
31
+ constructor(tool, reason) {
32
+ super(`Blocked by Agent Verify Guard: "${tool}" \u2014 ${reason}. Do not retry this action; tell the user it needs permission.`);
33
+ this.name = "GuardBlockedError";
34
+ this.tool = tool;
35
+ this.reason = reason;
36
+ }
37
+ };
38
+ var SECRET_PATTERNS = [
39
+ /\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}/,
40
+ /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/,
41
+ /\bgh[pousr]_[A-Za-z0-9]{30,}/,
42
+ /\bgithub_pat_[A-Za-z0-9_]{40,}/,
43
+ /\bxox[abpr]-[A-Za-z0-9-]{10,}/,
44
+ /\bAKIA[0-9A-Z]{16}\b/,
45
+ /\bAIza[0-9A-Za-z_-]{35}\b/,
46
+ /\bhf_[A-Za-z0-9]{30,}/,
47
+ /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/,
48
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
49
+ ];
50
+ function matches(name, patterns) {
51
+ return (patterns ?? []).some((p) => {
52
+ if (p instanceof RegExp) return p.test(name);
53
+ if (!p.includes("*")) return p === name;
54
+ const re = new RegExp(`^${p.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
55
+ return re.test(name);
56
+ });
57
+ }
58
+ function strings(value, out = [], depth = 0) {
59
+ if (depth > 6 || out.length > 200) return out;
60
+ if (typeof value === "string") out.push(value);
61
+ else if (Array.isArray(value)) for (const v of value) strings(v, out, depth + 1);
62
+ else if (value && typeof value === "object") for (const v of Object.values(value)) strings(v, out, depth + 1);
63
+ return out;
64
+ }
65
+ function findSecret(value) {
66
+ for (const s of strings(value)) for (const re of SECRET_PATTERNS) if (re.test(s)) return re.source.slice(0, 24);
67
+ return null;
68
+ }
69
+ function hostAllowed(host, allowed) {
70
+ const h = host.toLowerCase();
71
+ return allowed.some((a) => {
72
+ const d = a.toLowerCase().replace(/^\*\./, "");
73
+ return h === d || h.endsWith(`.${d}`);
74
+ });
75
+ }
76
+ function urlsIn(value) {
77
+ const urls = [];
78
+ for (const s of strings(value)) {
79
+ for (const m of s.matchAll(/https?:\/\/[^\s"'<>]+/gi)) {
80
+ try {
81
+ urls.push(new URL(m[0]));
82
+ } catch {
83
+ }
84
+ }
85
+ }
86
+ return urls;
87
+ }
88
+ function previewArgs(args) {
89
+ const mask = (v) => {
90
+ if (typeof v === "string") {
91
+ let s = v;
92
+ for (const re of SECRET_PATTERNS) s = s.replace(new RegExp(re.source, "g"), (m) => `${m.slice(0, 4)}****`);
93
+ return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
94
+ }
95
+ if (Array.isArray(v)) return v.slice(0, 10).map(mask);
96
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).slice(0, 20).map(([k, x]) => [k, mask(x)]));
97
+ return v;
98
+ };
99
+ try {
100
+ const s = JSON.stringify(args.length === 1 ? mask(args[0]) : args.map(mask));
101
+ return (s ?? "").slice(0, 600);
102
+ } catch {
103
+ return "[unserializable]";
104
+ }
105
+ }
106
+ function checkCall(policy, tool, args) {
107
+ if (matches(tool, policy.deny)) return { ok: false, reason: "this tool is on the deny list" };
108
+ if (policy.allow && !matches(tool, policy.allow)) return { ok: false, reason: "this tool is not on the allow list" };
109
+ if (policy.blockSecrets !== false) {
110
+ const secret = findSecret(args);
111
+ if (secret) return { ok: false, reason: "its arguments contain what looks like an API key or private key" };
112
+ }
113
+ if (policy.allowHosts?.length) {
114
+ const bad = urlsIn(args).find((u) => !hostAllowed(u.hostname, policy.allowHosts));
115
+ if (bad) return { ok: false, reason: `${bad.hostname} is not an allowed host` };
116
+ }
117
+ const rules = policy.args?.[tool];
118
+ const obj = args.length === 1 && args[0] && typeof args[0] === "object" ? args[0] : null;
119
+ if (rules && obj) {
120
+ for (const [name, rule] of Object.entries(rules)) {
121
+ const values = strings(obj[name]);
122
+ for (const v of values) {
123
+ if (rule.maxLength !== void 0 && v.length > rule.maxLength) return { ok: false, reason: `"${name}" is longer than ${rule.maxLength} characters` };
124
+ if (rule.deny && rule.deny.test(v)) return { ok: false, reason: `"${name}" matches a blocked pattern` };
125
+ if (rule.allowDomains?.length) {
126
+ const domains = [];
127
+ for (const m of v.matchAll(/@([A-Za-z0-9.-]+\.[A-Za-z]{2,})/g)) domains.push(m[1]);
128
+ for (const u of urlsIn(v)) domains.push(u.hostname);
129
+ if (domains.length === 0 && /@|:\/\//.test(v)) return { ok: false, reason: `"${name}" could not be checked against the allowed domains` };
130
+ const off = domains.find((d) => !hostAllowed(d, rule.allowDomains));
131
+ if (off) return { ok: false, reason: `"${name}" goes to ${off}, which is not an allowed domain` };
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return { ok: true, needsApproval: matches(tool, policy.requireApproval) };
137
+ }
138
+ function createGuard(options) {
139
+ const agent = options.agent ?? "agent";
140
+ const now = options.now ?? Date.now;
141
+ const fetchFn = options.fetchFn ?? (typeof fetch === "function" ? fetch : void 0);
142
+ const apiUrl = (options.apiUrl ?? "https://agentverify-api.agentverify.workers.dev").replace(/\/v1\/scan$/, "");
143
+ const recent = [];
144
+ let runCalls = 0;
145
+ let queue = [];
146
+ let timer = null;
147
+ const send = async () => {
148
+ timer = null;
149
+ if (!queue.length || !options.apiKey || !fetchFn) {
150
+ queue = [];
151
+ return;
152
+ }
153
+ const batch = queue.splice(0, 100);
154
+ try {
155
+ await fetchFn(`${apiUrl}/v1/guard/events`, {
156
+ method: "POST",
157
+ headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-guard/2.0.0" },
158
+ body: JSON.stringify({ events: batch })
159
+ });
160
+ } catch {
161
+ }
162
+ if (queue.length) await send();
163
+ };
164
+ const record = (event) => {
165
+ const full = { agent, at: new Date(now()).toISOString(), ...event };
166
+ try {
167
+ options.onEvent?.(full);
168
+ } catch {
169
+ }
170
+ if (options.apiKey) {
171
+ queue.push(full);
172
+ if (queue.length >= 50) void send();
173
+ else if (!timer) {
174
+ timer = setTimeout(() => void send(), 2e3);
175
+ timer.unref?.();
176
+ }
177
+ }
178
+ };
179
+ const overLimit = () => {
180
+ const t = now();
181
+ while (recent.length && recent[0] <= t - 6e4) recent.shift();
182
+ const perMinute = options.policy.rateLimit?.perMinute;
183
+ if (perMinute !== void 0 && recent.length >= perMinute) return `more than ${perMinute} tool calls in a minute`;
184
+ const perRun = options.policy.rateLimit?.perRun;
185
+ if (perRun !== void 0 && runCalls >= perRun) return `more than ${perRun} tool calls in this run`;
186
+ return null;
187
+ };
188
+ const wrap = (name, fn) => {
189
+ const guarded = async (...args) => {
190
+ const preview = previewArgs(args);
191
+ const limited = overLimit();
192
+ if (limited) {
193
+ record({ tool: name, decision: "blocked", reason: limited, argsPreview: preview });
194
+ throw new GuardBlockedError(name, limited);
195
+ }
196
+ const verdict = checkCall(options.policy, name, args);
197
+ if (!verdict.ok) {
198
+ record({ tool: name, decision: "blocked", reason: verdict.reason, argsPreview: preview });
199
+ throw new GuardBlockedError(name, verdict.reason);
200
+ }
201
+ let decision = "allowed";
202
+ if (verdict.needsApproval) {
203
+ let yes = false;
204
+ try {
205
+ yes = options.approve ? await options.approve({ tool: name, args, agent }) === true : false;
206
+ } catch {
207
+ yes = false;
208
+ }
209
+ if (!yes) {
210
+ const reason = options.approve ? "a person did not approve it" : "it needs human approval and no approver is set up";
211
+ record({ tool: name, decision: "rejected", reason, argsPreview: preview });
212
+ throw new GuardBlockedError(name, reason);
213
+ }
214
+ decision = "approved";
215
+ }
216
+ recent.push(now());
217
+ runCalls++;
218
+ const started = now();
219
+ try {
220
+ return await fn(...args);
221
+ } finally {
222
+ record({ tool: name, decision, reason: decision === "approved" ? "approved by a person" : "allowed by policy", argsPreview: preview, durationMs: now() - started });
223
+ }
224
+ };
225
+ Object.defineProperty(guarded, "name", { value: fn.name || name });
226
+ return guarded;
227
+ };
228
+ return {
229
+ wrap,
230
+ wrapAll(tools) {
231
+ return Object.fromEntries(Object.entries(tools).map(([k, f]) => [k, wrap(k, f)]));
232
+ },
233
+ async flush() {
234
+ if (timer) clearTimeout(timer);
235
+ await send();
236
+ },
237
+ newRun() {
238
+ runCalls = 0;
239
+ }
240
+ };
241
+ }
242
+ // Annotate the CommonJS export names for ESM import in node:
243
+ 0 && (module.exports = {
244
+ GuardBlockedError,
245
+ checkCall,
246
+ createGuard,
247
+ findSecret,
248
+ previewArgs
249
+ });
package/dist/guard.mjs ADDED
@@ -0,0 +1,220 @@
1
+ // src/guard.ts
2
+ var GuardBlockedError = class extends Error {
3
+ constructor(tool, reason) {
4
+ super(`Blocked by Agent Verify Guard: "${tool}" \u2014 ${reason}. Do not retry this action; tell the user it needs permission.`);
5
+ this.name = "GuardBlockedError";
6
+ this.tool = tool;
7
+ this.reason = reason;
8
+ }
9
+ };
10
+ var SECRET_PATTERNS = [
11
+ /\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{20,}/,
12
+ /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/,
13
+ /\bgh[pousr]_[A-Za-z0-9]{30,}/,
14
+ /\bgithub_pat_[A-Za-z0-9_]{40,}/,
15
+ /\bxox[abpr]-[A-Za-z0-9-]{10,}/,
16
+ /\bAKIA[0-9A-Z]{16}\b/,
17
+ /\bAIza[0-9A-Za-z_-]{35}\b/,
18
+ /\bhf_[A-Za-z0-9]{30,}/,
19
+ /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/,
20
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
21
+ ];
22
+ function matches(name, patterns) {
23
+ return (patterns ?? []).some((p) => {
24
+ if (p instanceof RegExp) return p.test(name);
25
+ if (!p.includes("*")) return p === name;
26
+ const re = new RegExp(`^${p.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
27
+ return re.test(name);
28
+ });
29
+ }
30
+ function strings(value, out = [], depth = 0) {
31
+ if (depth > 6 || out.length > 200) return out;
32
+ if (typeof value === "string") out.push(value);
33
+ else if (Array.isArray(value)) for (const v of value) strings(v, out, depth + 1);
34
+ else if (value && typeof value === "object") for (const v of Object.values(value)) strings(v, out, depth + 1);
35
+ return out;
36
+ }
37
+ function findSecret(value) {
38
+ for (const s of strings(value)) for (const re of SECRET_PATTERNS) if (re.test(s)) return re.source.slice(0, 24);
39
+ return null;
40
+ }
41
+ function hostAllowed(host, allowed) {
42
+ const h = host.toLowerCase();
43
+ return allowed.some((a) => {
44
+ const d = a.toLowerCase().replace(/^\*\./, "");
45
+ return h === d || h.endsWith(`.${d}`);
46
+ });
47
+ }
48
+ function urlsIn(value) {
49
+ const urls = [];
50
+ for (const s of strings(value)) {
51
+ for (const m of s.matchAll(/https?:\/\/[^\s"'<>]+/gi)) {
52
+ try {
53
+ urls.push(new URL(m[0]));
54
+ } catch {
55
+ }
56
+ }
57
+ }
58
+ return urls;
59
+ }
60
+ function previewArgs(args) {
61
+ const mask = (v) => {
62
+ if (typeof v === "string") {
63
+ let s = v;
64
+ for (const re of SECRET_PATTERNS) s = s.replace(new RegExp(re.source, "g"), (m) => `${m.slice(0, 4)}****`);
65
+ return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
66
+ }
67
+ if (Array.isArray(v)) return v.slice(0, 10).map(mask);
68
+ if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).slice(0, 20).map(([k, x]) => [k, mask(x)]));
69
+ return v;
70
+ };
71
+ try {
72
+ const s = JSON.stringify(args.length === 1 ? mask(args[0]) : args.map(mask));
73
+ return (s ?? "").slice(0, 600);
74
+ } catch {
75
+ return "[unserializable]";
76
+ }
77
+ }
78
+ function checkCall(policy, tool, args) {
79
+ if (matches(tool, policy.deny)) return { ok: false, reason: "this tool is on the deny list" };
80
+ if (policy.allow && !matches(tool, policy.allow)) return { ok: false, reason: "this tool is not on the allow list" };
81
+ if (policy.blockSecrets !== false) {
82
+ const secret = findSecret(args);
83
+ if (secret) return { ok: false, reason: "its arguments contain what looks like an API key or private key" };
84
+ }
85
+ if (policy.allowHosts?.length) {
86
+ const bad = urlsIn(args).find((u) => !hostAllowed(u.hostname, policy.allowHosts));
87
+ if (bad) return { ok: false, reason: `${bad.hostname} is not an allowed host` };
88
+ }
89
+ const rules = policy.args?.[tool];
90
+ const obj = args.length === 1 && args[0] && typeof args[0] === "object" ? args[0] : null;
91
+ if (rules && obj) {
92
+ for (const [name, rule] of Object.entries(rules)) {
93
+ const values = strings(obj[name]);
94
+ for (const v of values) {
95
+ if (rule.maxLength !== void 0 && v.length > rule.maxLength) return { ok: false, reason: `"${name}" is longer than ${rule.maxLength} characters` };
96
+ if (rule.deny && rule.deny.test(v)) return { ok: false, reason: `"${name}" matches a blocked pattern` };
97
+ if (rule.allowDomains?.length) {
98
+ const domains = [];
99
+ for (const m of v.matchAll(/@([A-Za-z0-9.-]+\.[A-Za-z]{2,})/g)) domains.push(m[1]);
100
+ for (const u of urlsIn(v)) domains.push(u.hostname);
101
+ if (domains.length === 0 && /@|:\/\//.test(v)) return { ok: false, reason: `"${name}" could not be checked against the allowed domains` };
102
+ const off = domains.find((d) => !hostAllowed(d, rule.allowDomains));
103
+ if (off) return { ok: false, reason: `"${name}" goes to ${off}, which is not an allowed domain` };
104
+ }
105
+ }
106
+ }
107
+ }
108
+ return { ok: true, needsApproval: matches(tool, policy.requireApproval) };
109
+ }
110
+ function createGuard(options) {
111
+ const agent = options.agent ?? "agent";
112
+ const now = options.now ?? Date.now;
113
+ const fetchFn = options.fetchFn ?? (typeof fetch === "function" ? fetch : void 0);
114
+ const apiUrl = (options.apiUrl ?? "https://agentverify-api.agentverify.workers.dev").replace(/\/v1\/scan$/, "");
115
+ const recent = [];
116
+ let runCalls = 0;
117
+ let queue = [];
118
+ let timer = null;
119
+ const send = async () => {
120
+ timer = null;
121
+ if (!queue.length || !options.apiKey || !fetchFn) {
122
+ queue = [];
123
+ return;
124
+ }
125
+ const batch = queue.splice(0, 100);
126
+ try {
127
+ await fetchFn(`${apiUrl}/v1/guard/events`, {
128
+ method: "POST",
129
+ headers: { Authorization: `Bearer ${options.apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-guard/2.0.0" },
130
+ body: JSON.stringify({ events: batch })
131
+ });
132
+ } catch {
133
+ }
134
+ if (queue.length) await send();
135
+ };
136
+ const record = (event) => {
137
+ const full = { agent, at: new Date(now()).toISOString(), ...event };
138
+ try {
139
+ options.onEvent?.(full);
140
+ } catch {
141
+ }
142
+ if (options.apiKey) {
143
+ queue.push(full);
144
+ if (queue.length >= 50) void send();
145
+ else if (!timer) {
146
+ timer = setTimeout(() => void send(), 2e3);
147
+ timer.unref?.();
148
+ }
149
+ }
150
+ };
151
+ const overLimit = () => {
152
+ const t = now();
153
+ while (recent.length && recent[0] <= t - 6e4) recent.shift();
154
+ const perMinute = options.policy.rateLimit?.perMinute;
155
+ if (perMinute !== void 0 && recent.length >= perMinute) return `more than ${perMinute} tool calls in a minute`;
156
+ const perRun = options.policy.rateLimit?.perRun;
157
+ if (perRun !== void 0 && runCalls >= perRun) return `more than ${perRun} tool calls in this run`;
158
+ return null;
159
+ };
160
+ const wrap = (name, fn) => {
161
+ const guarded = async (...args) => {
162
+ const preview = previewArgs(args);
163
+ const limited = overLimit();
164
+ if (limited) {
165
+ record({ tool: name, decision: "blocked", reason: limited, argsPreview: preview });
166
+ throw new GuardBlockedError(name, limited);
167
+ }
168
+ const verdict = checkCall(options.policy, name, args);
169
+ if (!verdict.ok) {
170
+ record({ tool: name, decision: "blocked", reason: verdict.reason, argsPreview: preview });
171
+ throw new GuardBlockedError(name, verdict.reason);
172
+ }
173
+ let decision = "allowed";
174
+ if (verdict.needsApproval) {
175
+ let yes = false;
176
+ try {
177
+ yes = options.approve ? await options.approve({ tool: name, args, agent }) === true : false;
178
+ } catch {
179
+ yes = false;
180
+ }
181
+ if (!yes) {
182
+ const reason = options.approve ? "a person did not approve it" : "it needs human approval and no approver is set up";
183
+ record({ tool: name, decision: "rejected", reason, argsPreview: preview });
184
+ throw new GuardBlockedError(name, reason);
185
+ }
186
+ decision = "approved";
187
+ }
188
+ recent.push(now());
189
+ runCalls++;
190
+ const started = now();
191
+ try {
192
+ return await fn(...args);
193
+ } finally {
194
+ record({ tool: name, decision, reason: decision === "approved" ? "approved by a person" : "allowed by policy", argsPreview: preview, durationMs: now() - started });
195
+ }
196
+ };
197
+ Object.defineProperty(guarded, "name", { value: fn.name || name });
198
+ return guarded;
199
+ };
200
+ return {
201
+ wrap,
202
+ wrapAll(tools) {
203
+ return Object.fromEntries(Object.entries(tools).map(([k, f]) => [k, wrap(k, f)]));
204
+ },
205
+ async flush() {
206
+ if (timer) clearTimeout(timer);
207
+ await send();
208
+ },
209
+ newRun() {
210
+ runCalls = 0;
211
+ }
212
+ };
213
+ }
214
+ export {
215
+ GuardBlockedError,
216
+ checkCall,
217
+ createGuard,
218
+ findSecret,
219
+ previewArgs
220
+ };
package/dist/sdk.d.mts CHANGED
@@ -52,6 +52,10 @@ interface Finding {
52
52
  recommendedFix: string;
53
53
  fixCode?: string;
54
54
  quickFix?: string;
55
+ /** Line in the scanned content, when the finding can be located. */
56
+ line?: number;
57
+ /** Set by the API when this finding has a fix your plan does not include (Pro). */
58
+ fixLocked?: boolean;
55
59
  compliance?: {
56
60
  owasp?: string[];
57
61
  nist?: string[];