@agentguard-run/spend 0.13.1 → 0.13.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/frameworks/claude-code.d.ts +32 -0
  3. package/dist/frameworks/claude-code.d.ts.map +1 -0
  4. package/dist/frameworks/claude-code.js +75 -0
  5. package/dist/frameworks/claude-code.js.map +1 -0
  6. package/dist/frameworks/common.d.ts +51 -0
  7. package/dist/frameworks/common.d.ts.map +1 -0
  8. package/dist/frameworks/common.js +168 -0
  9. package/dist/frameworks/common.js.map +1 -0
  10. package/dist/frameworks/hermes.d.ts +34 -0
  11. package/dist/frameworks/hermes.d.ts.map +1 -0
  12. package/dist/frameworks/hermes.js +50 -0
  13. package/dist/frameworks/hermes.js.map +1 -0
  14. package/dist/frameworks/index.d.ts +6 -0
  15. package/dist/frameworks/index.d.ts.map +1 -0
  16. package/dist/frameworks/index.js +22 -0
  17. package/dist/frameworks/index.js.map +1 -0
  18. package/dist/frameworks/openrouter.d.ts +19 -0
  19. package/dist/frameworks/openrouter.d.ts.map +1 -0
  20. package/dist/frameworks/openrouter.js +85 -0
  21. package/dist/frameworks/openrouter.js.map +1 -0
  22. package/dist/frameworks/vercel-ai.d.ts +27 -0
  23. package/dist/frameworks/vercel-ai.d.ts.map +1 -0
  24. package/dist/frameworks/vercel-ai.js +96 -0
  25. package/dist/frameworks/vercel-ai.js.map +1 -0
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -1
  29. package/dist/index.js.map +1 -1
  30. package/package.json +33 -3
  31. package/src/frameworks/README.md +44 -0
  32. package/src/frameworks/claude-code.ts +108 -0
  33. package/src/frameworks/common.ts +208 -0
  34. package/src/frameworks/hermes.ts +83 -0
  35. package/src/frameworks/index.ts +5 -0
  36. package/src/frameworks/openrouter.ts +90 -0
  37. package/src/frameworks/vercel-ai.ts +118 -0
@@ -0,0 +1,83 @@
1
+ import type { SpendGuard } from '../spend-guard';
2
+ import { createFrameworkGuard, recordFrameworkReceipt, redactDataPlane, safeRequestShape, stringValue, type FrameworkAdapterOptions } from './common';
3
+
4
+ export interface HermesToolEvent {
5
+ toolName?: string;
6
+ name?: string;
7
+ args?: Record<string, unknown>;
8
+ toolArgs?: Record<string, unknown>;
9
+ workflowId?: string;
10
+ sessionId?: string;
11
+ metadata?: Record<string, unknown>;
12
+ result?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface HermesHookResult {
16
+ allow: boolean;
17
+ approved?: boolean;
18
+ reason?: string;
19
+ receiptId?: string;
20
+ editedArgs?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface HermesPlugin {
24
+ name: 'agentguard-spend';
25
+ guard: SpendGuard;
26
+ hooks: {
27
+ 'pre-tool-call': (event: HermesToolEvent) => Promise<HermesHookResult>;
28
+ approval: (event: HermesToolEvent) => Promise<HermesHookResult>;
29
+ 'post-tool-call': (event: HermesToolEvent) => Promise<HermesHookResult>;
30
+ };
31
+ preToolCall(event: HermesToolEvent): Promise<HermesHookResult>;
32
+ approval(event: HermesToolEvent): Promise<HermesHookResult>;
33
+ postToolCall(event: HermesToolEvent): Promise<HermesHookResult>;
34
+ }
35
+
36
+ export function createHermesPlugin(opts: FrameworkAdapterOptions): HermesPlugin {
37
+ const guard = createFrameworkGuard({ ...opts, framework: 'hermes' });
38
+ const plugin: HermesPlugin = {
39
+ name: 'agentguard-spend',
40
+ guard,
41
+ hooks: {
42
+ 'pre-tool-call': (event) => preToolCall(guard, opts, event),
43
+ approval: (event) => approval(guard, opts, event),
44
+ 'post-tool-call': (event) => postToolCall(guard, event),
45
+ },
46
+ preToolCall: (event) => preToolCall(guard, opts, event),
47
+ approval: (event) => approval(guard, opts, event),
48
+ postToolCall: (event) => postToolCall(guard, event),
49
+ };
50
+ return plugin;
51
+ }
52
+
53
+ export const agentguardHermesPlugin = createHermesPlugin;
54
+
55
+ async function preToolCall(guard: SpendGuard, opts: FrameworkAdapterOptions, event: HermesToolEvent): Promise<HermesHookResult> {
56
+ const gate = await guard.guardToolCall({
57
+ toolName: toolName(event),
58
+ toolArgs: safeRequestShape(redactDataPlane(event.toolArgs ?? event.args ?? {})),
59
+ workflowId: event.workflowId ?? event.sessionId,
60
+ scope: opts.scope,
61
+ });
62
+ if (!gate.approved) return { allow: false, approved: false, reason: gate.decision.reasons.join('; ') };
63
+ return { allow: true, approved: true, editedArgs: gate.editedArgs };
64
+ }
65
+
66
+ async function approval(guard: SpendGuard, opts: FrameworkAdapterOptions, event: HermesToolEvent): Promise<HermesHookResult> {
67
+ return preToolCall(guard, opts, event);
68
+ }
69
+
70
+ async function postToolCall(guard: SpendGuard, event: HermesToolEvent): Promise<HermesHookResult> {
71
+ const { decision } = await recordFrameworkReceipt(guard, 'hermes', {
72
+ event: 'post-tool-call',
73
+ toolName: toolName(event),
74
+ workflowId: event.workflowId ?? event.sessionId ?? null,
75
+ metadata: redactDataPlane(event.metadata ?? {}),
76
+ result: redactDataPlane(event.result ?? {}),
77
+ });
78
+ return { allow: true, approved: true, receiptId: decision.decisionId };
79
+ }
80
+
81
+ function toolName(event: HermesToolEvent): string {
82
+ return stringValue(event.toolName) ?? stringValue(event.name) ?? 'unknown_tool';
83
+ }
@@ -0,0 +1,5 @@
1
+ export * from './common';
2
+ export * from './openrouter';
3
+ export * from './vercel-ai';
4
+ export * from './claude-code';
5
+ export * from './hermes';
@@ -0,0 +1,90 @@
1
+ import { withSpendGuard, type OpenAIBindingOptions, type SpendGuardConfig } from '../spend-guard';
2
+ import type { CapabilityTier, SpendPolicy, SpendScope } from '../types';
3
+ import { createFrameworkGuard, preflightFrameworkCall, settleFrameworkCall, type FrameworkAdapterOptions } from './common';
4
+
5
+ export interface OpenRouterBindingOptions {
6
+ policy: SpendPolicy;
7
+ scope: SpendScope;
8
+ capabilityClaim?: CapabilityTier;
9
+ config?: Omit<SpendGuardConfig, 'policy'>;
10
+ licenseKey?: string;
11
+ openRouterBaseUrl?: string;
12
+ }
13
+
14
+ export function withSpendGuardOpenRouter<TClient>(client: TClient, opts: OpenRouterBindingOptions): TClient {
15
+ const config = {
16
+ ...(opts.config ?? {}),
17
+ openRouterBaseUrl: opts.openRouterBaseUrl ?? opts.config?.openRouterBaseUrl ?? 'https://openrouter.ai/api/v1',
18
+ providerRoute: opts.config?.providerRoute ?? 'openrouter',
19
+ } as Omit<SpendGuardConfig, 'policy'>;
20
+ return withSpendGuard(client, {
21
+ policy: opts.policy,
22
+ scope: opts.scope,
23
+ capabilityClaim: opts.capabilityClaim,
24
+ licenseKey: opts.licenseKey,
25
+ config,
26
+ } satisfies OpenAIBindingOptions) as TClient;
27
+ }
28
+
29
+ export function withOpenRouterSpendGuard<TClient>(client: TClient, opts: OpenRouterBindingOptions): TClient {
30
+ return withSpendGuardOpenRouter(client, opts);
31
+ }
32
+
33
+ export interface OpenRouterFetchOptions extends FrameworkAdapterOptions {
34
+ fetchImpl?: (url: string, init?: Record<string, unknown>) => Promise<unknown>;
35
+ endpoint?: string;
36
+ }
37
+
38
+ export function createOpenRouterFetch(opts: OpenRouterFetchOptions): (url: string, init?: Record<string, unknown>) => Promise<unknown> {
39
+ const guard = createFrameworkGuard({ ...opts, defaultModel: opts.defaultModel ?? 'openrouter/auto', framework: 'openrouter' });
40
+ const fetchImpl = opts.fetchImpl ?? (globalThis as unknown as { fetch?: (url: string, init?: Record<string, unknown>) => Promise<unknown> }).fetch;
41
+ if (!fetchImpl) throw new Error('createOpenRouterFetch: fetch is not available');
42
+ const endpoint = opts.endpoint ?? 'https://openrouter.ai/api/v1/chat/completions';
43
+
44
+ return async (url: string, init: Record<string, unknown> = {}) => {
45
+ const target = String(url);
46
+ if (!target.startsWith(endpoint)) return fetchImpl(target, init);
47
+ const body = parseJsonBody(init.body);
48
+ const preflight = await preflightFrameworkCall(guard, opts, {
49
+ framework: 'openrouter',
50
+ model: typeof body.model === 'string' ? body.model : opts.defaultModel ?? 'openrouter/auto',
51
+ params: body,
52
+ metadata: metadataRecord(body.metadata),
53
+ requestShape: {
54
+ endpoint: '/api/v1/chat/completions',
55
+ stream: body.stream === true,
56
+ maxTokens: typeof body.max_tokens === 'number' ? body.max_tokens : undefined,
57
+ toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
58
+ },
59
+ });
60
+ const nextBody = preflight.decision.action === 'downgrade' && preflight.decision.modelResolved !== preflight.decision.modelRequested
61
+ ? { ...body, model: preflight.decision.modelResolved }
62
+ : body;
63
+ const response = await fetchImpl(target, { ...init, body: JSON.stringify(nextBody) });
64
+ await settleFrameworkCall(preflight, await responseJson(response));
65
+ return response;
66
+ };
67
+ }
68
+
69
+ function parseJsonBody(body: unknown): Record<string, unknown> {
70
+ if (typeof body === 'string') {
71
+ try { return JSON.parse(body) as Record<string, unknown>; } catch { return {}; }
72
+ }
73
+ if (body && typeof body === 'object' && !Array.isArray(body)) return body as Record<string, unknown>;
74
+ return {};
75
+ }
76
+
77
+ function metadataRecord(value: unknown): Record<string, unknown> {
78
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
79
+ }
80
+
81
+ async function responseJson(response: unknown): Promise<unknown> {
82
+ const maybe = response as { clone?: () => { json?: () => Promise<unknown> }; json?: () => Promise<unknown> };
83
+ try {
84
+ if (typeof maybe.clone === 'function') return await maybe.clone().json?.();
85
+ if (typeof maybe.json === 'function') return await maybe.json();
86
+ } catch {
87
+ return null;
88
+ }
89
+ return null;
90
+ }
@@ -0,0 +1,118 @@
1
+ import type { SpendGuard } from '../spend-guard';
2
+ import { createFrameworkGuard, modelFromUnknown, objectRecord, preflightFrameworkCall, recordFrameworkReceipt, redactDataPlane, settleFrameworkCall, type FrameworkAdapterOptions, type FrameworkPreflight } from './common';
3
+
4
+ export type AgentGuardAiSdkMiddleware = {
5
+ specificationVersion: 'v3';
6
+ transformParams?: (args: { type: 'generate' | 'stream'; params: unknown; model: unknown }) => Promise<unknown> | unknown;
7
+ wrapGenerate?: (args: { doGenerate: () => Promise<unknown>; doStream?: () => Promise<unknown>; params: unknown; model: unknown }) => Promise<unknown>;
8
+ wrapStream?: (args: { doGenerate?: () => Promise<unknown>; doStream: () => Promise<unknown>; params: unknown; model: unknown }) => Promise<unknown>;
9
+ };
10
+
11
+ export interface AgentGuardAiSdkOptions extends FrameworkAdapterOptions {
12
+ framework?: 'vercel-ai-sdk';
13
+ }
14
+
15
+ export function agentguardAiSdkMiddleware(opts: AgentGuardAiSdkOptions): AgentGuardAiSdkMiddleware {
16
+ const guard = createFrameworkGuard({ ...opts, framework: 'vercel-ai-sdk' });
17
+ return {
18
+ specificationVersion: 'v3',
19
+ transformParams: ({ params }) => params,
20
+ wrapGenerate: async ({ doGenerate, params, model }) => {
21
+ const preflight = await aiSdkPreflight(guard, opts, params, model, 'generate');
22
+ const result = await doGenerate();
23
+ await settleFrameworkCall(preflight, result);
24
+ await recordFrameworkReceipt(guard, 'vercel-ai-sdk', {
25
+ event: 'post_generate',
26
+ decisionId: preflight.decision.decisionId,
27
+ model: preflight.decision.modelResolved,
28
+ requestShape: requestShape(params, 'generate'),
29
+ hasUsage: Boolean(objectRecord(result)?.usage),
30
+ });
31
+ return result;
32
+ },
33
+ wrapStream: async ({ doStream, params, model }) => {
34
+ const preflight = await aiSdkPreflight(guard, opts, params, model, 'stream');
35
+ const result = await doStream();
36
+ return wrapAiSdkStreamResult(guard, preflight, result, params);
37
+ },
38
+ };
39
+ }
40
+
41
+ export const agentguard = agentguardAiSdkMiddleware;
42
+
43
+ async function aiSdkPreflight(
44
+ guard: SpendGuard,
45
+ opts: AgentGuardAiSdkOptions,
46
+ params: unknown,
47
+ model: unknown,
48
+ type: 'generate' | 'stream',
49
+ ): Promise<FrameworkPreflight> {
50
+ return preflightFrameworkCall(guard, opts, {
51
+ framework: 'vercel-ai-sdk',
52
+ model: modelFromUnknown(model, opts.defaultModel ?? 'unknown'),
53
+ params,
54
+ metadata: agentguardMetadata(params),
55
+ requestShape: requestShape(params, type),
56
+ });
57
+ }
58
+
59
+ function requestShape(params: unknown, type: 'generate' | 'stream'): Record<string, unknown> {
60
+ const record = objectRecord(params) ?? {};
61
+ const tools = objectRecord(record.tools) ?? {};
62
+ return {
63
+ type,
64
+ maxOutputTokens: record.maxOutputTokens ?? record.maxTokens,
65
+ toolNames: Object.keys(tools).sort(),
66
+ providerOptionsPresent: Boolean(record.providerOptions),
67
+ redacted: redactDataPlane({ prompt: record.prompt, messages: record.messages }),
68
+ };
69
+ }
70
+
71
+ function agentguardMetadata(params: unknown): Record<string, unknown> {
72
+ const record = objectRecord(params) ?? {};
73
+ const providerOptions = objectRecord(record.providerOptions) ?? objectRecord(record.providerMetadata) ?? {};
74
+ const metadata = objectRecord(providerOptions.agentguard) ?? {};
75
+ return metadata;
76
+ }
77
+
78
+ async function wrapAiSdkStreamResult(
79
+ guard: SpendGuard,
80
+ preflight: FrameworkPreflight,
81
+ result: unknown,
82
+ params: unknown,
83
+ ): Promise<unknown> {
84
+ const record = objectRecord(result);
85
+ const stream = record?.stream;
86
+ if (!stream || typeof (stream as { pipeThrough?: unknown }).pipeThrough !== 'function') {
87
+ await settleFrameworkCall(preflight, result, true);
88
+ await recordFrameworkReceipt(guard, 'vercel-ai-sdk', {
89
+ event: 'post_stream',
90
+ decisionId: preflight.decision.decisionId,
91
+ model: preflight.decision.modelResolved,
92
+ requestShape: requestShape(params, 'stream'),
93
+ partial: true,
94
+ });
95
+ return result;
96
+ }
97
+ const TransformStreamCtor = (globalThis as unknown as { TransformStream?: new (handlers: Record<string, unknown>) => unknown }).TransformStream;
98
+ if (!TransformStreamCtor) return result;
99
+ let finalUsage: unknown = null;
100
+ const transform = new TransformStreamCtor({
101
+ transform(chunk: unknown, controller: { enqueue: (chunk: unknown) => void }) {
102
+ const usage = objectRecord(chunk)?.usage;
103
+ if (usage) finalUsage = { usage };
104
+ controller.enqueue(chunk);
105
+ },
106
+ async flush() {
107
+ await settleFrameworkCall(preflight, finalUsage ?? result);
108
+ await recordFrameworkReceipt(guard, 'vercel-ai-sdk', {
109
+ event: 'post_stream',
110
+ decisionId: preflight.decision.decisionId,
111
+ model: preflight.decision.modelResolved,
112
+ requestShape: requestShape(params, 'stream'),
113
+ partial: false,
114
+ });
115
+ },
116
+ });
117
+ return { ...record, stream: (stream as { pipeThrough: (transform: unknown) => unknown }).pipeThrough(transform) };
118
+ }