@agentguard-run/spend 0.2.2 → 0.3.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.
- package/CHANGELOG.md +9 -1
- package/LICENSE +1 -1
- package/README.es-419.md +28 -102
- package/README.md +50 -124
- package/README.pt-BR.md +28 -102
- package/dist/bindings/anthropic.d.ts +11 -0
- package/dist/bindings/anthropic.d.ts.map +1 -0
- package/dist/bindings/anthropic.js +116 -0
- package/dist/bindings/anthropic.js.map +1 -0
- package/dist/bindings/bedrock.d.ts +11 -0
- package/dist/bindings/bedrock.d.ts.map +1 -0
- package/dist/bindings/bedrock.js +177 -0
- package/dist/bindings/bedrock.js.map +1 -0
- package/dist/cli/auth.d.ts +7 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +189 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/colors.d.ts +8 -3
- package/dist/cli/colors.d.ts.map +1 -1
- package/dist/cli/colors.js +93 -4
- package/dist/cli/colors.js.map +1 -1
- package/dist/cli/demo.d.ts.map +1 -1
- package/dist/cli/demo.js +23 -2
- package/dist/cli/demo.js.map +1 -1
- package/dist/cli/main.d.ts +0 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +36 -16
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/models.d.ts +18 -0
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +277 -0
- package/dist/cli/models.js.map +1 -0
- package/dist/cli/tips.d.ts +21 -0
- package/dist/cli/tips.d.ts.map +1 -0
- package/dist/cli/tips.js +191 -0
- package/dist/cli/tips.js.map +1 -0
- package/dist/cli/wizard.d.ts +27 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +182 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/cost-table.d.ts +11 -36
- package/dist/cost-table.d.ts.map +1 -1
- package/dist/cost-table.js +114 -45
- package/dist/cost-table.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/openrouter-catalog.d.ts +56 -0
- package/dist/openrouter-catalog.d.ts.map +1 -0
- package/dist/openrouter-catalog.js +183 -0
- package/dist/openrouter-catalog.js.map +1 -0
- package/dist/spend-guard.d.ts +38 -55
- package/dist/spend-guard.d.ts.map +1 -1
- package/dist/spend-guard.js +268 -83
- package/dist/spend-guard.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +52 -21
- package/dist/telemetry.js.map +1 -1
- package/dist/templates/index.d.ts +17 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +100 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types.d.ts +18 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +32 -4
- package/src/bindings/anthropic.ts +142 -0
- package/src/bindings/bedrock.ts +200 -0
- package/src/cli/auth.ts +145 -0
- package/src/cli/models.ts +236 -0
- package/src/cli/tips.ts +161 -0
- package/src/cli/wizard.ts +160 -0
- package/src/openrouter-catalog.ts +180 -0
- package/src/templates/agent-support.yaml +30 -0
- package/src/templates/chargeback-evidence.yaml +30 -0
- package/src/templates/code-scan.yaml +30 -0
- package/src/templates/index.ts +109 -0
- package/src/templates/payment-approval.yaml +30 -0
- package/src/templates/risk-review.yaml +30 -0
- package/tests/fixtures/openrouter-catalog.json +1 -0
package/src/cli/tips.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { AGENTGUARD_SPEND_VERSION } from '../index';
|
|
4
|
+
import { agentguardHome, banner, dim, green, statusBar } from './colors';
|
|
5
|
+
|
|
6
|
+
export interface AgentGuardTip {
|
|
7
|
+
id: string;
|
|
8
|
+
surface: string;
|
|
9
|
+
severity: 'info' | 'warning';
|
|
10
|
+
text: string;
|
|
11
|
+
learn_more_url: string;
|
|
12
|
+
dismissible: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TipsState {
|
|
16
|
+
seen?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const TIPS: AgentGuardTip[] = [
|
|
20
|
+
{
|
|
21
|
+
id: 'cheap-read-only',
|
|
22
|
+
surface: 'models',
|
|
23
|
+
severity: 'info',
|
|
24
|
+
text: "Use 'openai/gpt-4o-mini' for read-only tasks. It is far cheaper than gpt-5 with the same audit guarantees.",
|
|
25
|
+
learn_more_url: 'https://agentguard.run/docs/models',
|
|
26
|
+
dismissible: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'openrouter-cfo',
|
|
30
|
+
surface: 'auth',
|
|
31
|
+
severity: 'info',
|
|
32
|
+
text: 'One OpenRouter key gives access to hundreds of models across many providers. Your CFO sees one invoice while AgentGuard enforces who uses what.',
|
|
33
|
+
learn_more_url: 'https://agentguard.run/docs/openrouter',
|
|
34
|
+
dismissible: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'public-verify',
|
|
38
|
+
surface: 'verify',
|
|
39
|
+
severity: 'info',
|
|
40
|
+
text: 'Share https://agentguard.run/verify with your auditor. Paste any receipt and get browser-based cryptographic verification with no install.',
|
|
41
|
+
learn_more_url: 'https://agentguard.run/verify',
|
|
42
|
+
dismissible: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'weekly-pricing',
|
|
46
|
+
surface: 'models',
|
|
47
|
+
severity: 'info',
|
|
48
|
+
text: "Run 'agentguard models --sync-pricing' weekly to keep cost math current with OpenRouter live prices.",
|
|
49
|
+
learn_more_url: 'https://agentguard.run/docs/pricing',
|
|
50
|
+
dismissible: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'payment-attestation',
|
|
54
|
+
surface: 'wizard',
|
|
55
|
+
severity: 'warning',
|
|
56
|
+
text: "Capability tier 'payment_execute' should require a verified attestation. Do not accept caller-supplied claims for real money movement.",
|
|
57
|
+
learn_more_url: 'https://agentguard.run/docs/capabilities',
|
|
58
|
+
dismissible: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'shadow-rollout',
|
|
62
|
+
surface: 'policy',
|
|
63
|
+
severity: 'info',
|
|
64
|
+
text: "Use mode 'shadow' for the first day of a rollout. You get signed decisions before enforcement changes traffic.",
|
|
65
|
+
learn_more_url: 'https://agentguard.run/docs/policies',
|
|
66
|
+
dismissible: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'verify-chain',
|
|
70
|
+
surface: 'verify',
|
|
71
|
+
severity: 'info',
|
|
72
|
+
text: "Run 'agentguard verify <log.jsonl> <pubkey>' before sharing an audit packet. Whole-chain verification catches missing entries.",
|
|
73
|
+
learn_more_url: 'https://agentguard.run/docs/verify',
|
|
74
|
+
dismissible: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'local-overrides',
|
|
78
|
+
surface: 'models',
|
|
79
|
+
severity: 'info',
|
|
80
|
+
text: 'Negotiated rates belong in ~/.agentguard/cost-overrides.json. Keep pricing math local with the rest of the policy state.',
|
|
81
|
+
learn_more_url: 'https://agentguard.run/docs/pricing',
|
|
82
|
+
dismissible: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'bedrock-routing',
|
|
86
|
+
surface: 'bindings',
|
|
87
|
+
severity: 'info',
|
|
88
|
+
text: 'Use the native Bedrock binding when AWS procurement requires direct provider calls. AgentGuard still signs every local decision.',
|
|
89
|
+
learn_more_url: 'https://agentguard.run/docs/bedrock',
|
|
90
|
+
dismissible: true,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export function tipsStatePath(): string {
|
|
95
|
+
return path.join(agentguardHome(), 'tips-seen.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function readTipsState(filePath: string = tipsStatePath()): TipsState {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as TipsState;
|
|
101
|
+
return Array.isArray(parsed.seen) ? parsed : { seen: [] };
|
|
102
|
+
} catch {
|
|
103
|
+
return { seen: [] };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function writeTipsState(state: TipsState, filePath: string = tipsStatePath()): void {
|
|
108
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
109
|
+
fs.writeFileSync(filePath, JSON.stringify({ seen: [...new Set(state.seen ?? [])] }, null, 2) + '\n', { mode: 0o600 });
|
|
110
|
+
try { fs.chmodSync(filePath, 0o600); } catch { return; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function unseenTips(state: TipsState = readTipsState()): AgentGuardTip[] {
|
|
114
|
+
const seen = new Set(state.seen ?? []);
|
|
115
|
+
return TIPS.filter((tip) => !seen.has(tip.id));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function markTipsSeen(ids: string[], filePath: string = tipsStatePath()): void {
|
|
119
|
+
const state = readTipsState(filePath);
|
|
120
|
+
writeTipsState({ seen: [...new Set([...(state.seen ?? []), ...ids])] }, filePath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function maybeShowStartupTips(force = false): void {
|
|
124
|
+
const state = readTipsState();
|
|
125
|
+
const startupIds = ['startup-wizard', 'startup-verify'];
|
|
126
|
+
const seen = new Set(state.seen ?? []);
|
|
127
|
+
if (!force && startupIds.every((id) => seen.has(id))) return;
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(' ' + green("Tip: try 'agentguard wizard' to set up in 90 seconds."));
|
|
130
|
+
console.log(' ' + green('Tip: share https://agentguard.run/verify with your auditor.'));
|
|
131
|
+
markTipsSeen(startupIds);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function runTips(argv: string[]): Promise<number> {
|
|
135
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
136
|
+
console.log('agentguard tips [--reset] [--json]');
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
if (argv.includes('--reset')) {
|
|
140
|
+
writeTipsState({ seen: [] });
|
|
141
|
+
console.log('AgentGuard tips reset.');
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (argv.includes('--json')) {
|
|
145
|
+
console.log(JSON.stringify(TIPS, null, 2));
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(' ' + banner(AGENTGUARD_SPEND_VERSION));
|
|
150
|
+
console.log('');
|
|
151
|
+
for (const tip of TIPS) {
|
|
152
|
+
const label = tip.severity === 'warning' ? 'warning' : 'tip';
|
|
153
|
+
console.log(` ${green(label)} ${tip.text}`);
|
|
154
|
+
console.log(` ${dim(tip.learn_more_url)}`);
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(' ' + statusBar());
|
|
158
|
+
console.log('');
|
|
159
|
+
markTipsSeen(TIPS.filter((tip) => tip.dismissible).map((tip) => tip.id));
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as readline from 'readline';
|
|
4
|
+
import { AGENTGUARD_SPEND_VERSION } from '../index';
|
|
5
|
+
import { TASK_TEMPLATES, getTaskTemplate, listTaskTemplates, type TaskTemplate } from '../templates';
|
|
6
|
+
import { agentguardHome, banner, cyanBold, green, statusBar } from './colors';
|
|
7
|
+
|
|
8
|
+
const BUILD_TYPES = ['chatbot', 'agent', 'batch job', 'API endpoint'];
|
|
9
|
+
const CAPABILITY_TIERS = [
|
|
10
|
+
['read_only', 'Read records and produce findings'],
|
|
11
|
+
['data_write', 'Write allowed fields after policy checks'],
|
|
12
|
+
['payment_initiate', 'Create or recommend payment intents'],
|
|
13
|
+
['payment_execute', 'Execute money movement only with verified attestation'],
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export interface WizardAnswers {
|
|
17
|
+
building: string;
|
|
18
|
+
template: string;
|
|
19
|
+
capability: string;
|
|
20
|
+
allowedModels: string[];
|
|
21
|
+
fallbackModel: string;
|
|
22
|
+
perCallCents: number;
|
|
23
|
+
perDayCents: number;
|
|
24
|
+
perMonthCents: number;
|
|
25
|
+
systemInstructions: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class WizardStateMachine {
|
|
29
|
+
step = 0;
|
|
30
|
+
readonly totalSteps = 7;
|
|
31
|
+
|
|
32
|
+
next(answer: string): boolean {
|
|
33
|
+
if (!this.validate(this.step, answer)) return false;
|
|
34
|
+
this.step = Math.min(this.step + 1, this.totalSteps - 1);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
back(): void {
|
|
39
|
+
this.step = Math.max(0, this.step - 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
validate(step: number, answer: string): boolean {
|
|
43
|
+
if (step === 0) return BUILD_TYPES.includes(answer);
|
|
44
|
+
if (step === 1) return Boolean(TASK_TEMPLATES[answer]);
|
|
45
|
+
if (step === 2) return CAPABILITY_TIERS.some(([tier]) => tier === answer);
|
|
46
|
+
if (step === 3) return answer.split(',').map((item) => item.trim()).filter(Boolean).length > 0;
|
|
47
|
+
if (step === 4) return answer.trim().length > 0;
|
|
48
|
+
if (step === 5) return answer.split(',').every((item) => Number.isSafeInteger(Number(item.trim())) && Number(item.trim()) >= 0);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runWizard(argv: string[]): Promise<number> {
|
|
54
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
55
|
+
console.log('agentguard wizard [--defaults] [--template <name>]');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const templateSlug = valueAfter(argv, '--template') ?? 'risk-review';
|
|
59
|
+
const useDefaults = argv.includes('--defaults') || !process.stdin.isTTY;
|
|
60
|
+
const answers = useDefaults ? defaultAnswers(templateSlug) : await promptAnswers(templateSlug);
|
|
61
|
+
const outputs = writeWizardOutputs(answers);
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(' ' + banner(AGENTGUARD_SPEND_VERSION));
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(` ${green('created')} ${outputs.policyPath}`);
|
|
66
|
+
console.log(` ${green('created')} ${outputs.quickstartTsPath}`);
|
|
67
|
+
console.log(` ${green('created')} ${outputs.quickstartPyPath}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(cyanBold('Quickstart snippet'));
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(outputs.snippet);
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(' ' + statusBar());
|
|
74
|
+
console.log('');
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function defaultAnswers(templateSlug = 'risk-review'): WizardAnswers {
|
|
79
|
+
const template = getTaskTemplate(templateSlug) ?? getTaskTemplate('risk-review') as TaskTemplate;
|
|
80
|
+
const perCall = template.caps.find((cap) => cap.window === 'per_call')?.amountCents ?? 100;
|
|
81
|
+
const perDay = template.caps.find((cap) => cap.window === 'per_day')?.amountCents ?? 2500;
|
|
82
|
+
return {
|
|
83
|
+
building: 'agent',
|
|
84
|
+
template: template.slug,
|
|
85
|
+
capability: template.requiredCapability,
|
|
86
|
+
allowedModels: [...template.allowedModels],
|
|
87
|
+
fallbackModel: template.fallbackModel,
|
|
88
|
+
perCallCents: perCall,
|
|
89
|
+
perDayCents: perDay,
|
|
90
|
+
perMonthCents: perDay * 20,
|
|
91
|
+
systemInstructions: template.systemInstructions,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function writeWizardOutputs(answers: WizardAnswers, home = agentguardHome()): { policyPath: string; quickstartTsPath: string; quickstartPyPath: string; snippet: string } {
|
|
96
|
+
fs.mkdirSync(home, { recursive: true });
|
|
97
|
+
const policyPath = path.join(home, 'policy.yaml');
|
|
98
|
+
const quickstartTsPath = path.join(home, 'quickstart.ts');
|
|
99
|
+
const quickstartPyPath = path.join(home, 'quickstart.py');
|
|
100
|
+
const policy = policyYaml(answers);
|
|
101
|
+
const ts = quickstartTs(answers);
|
|
102
|
+
const py = quickstartPy(answers);
|
|
103
|
+
fs.writeFileSync(policyPath, policy);
|
|
104
|
+
fs.writeFileSync(quickstartTsPath, ts);
|
|
105
|
+
fs.writeFileSync(quickstartPyPath, py);
|
|
106
|
+
return { policyPath, quickstartTsPath, quickstartPyPath, snippet: tsSnippet(answers) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function promptAnswers(templateSlug: string): Promise<WizardAnswers> {
|
|
110
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
111
|
+
const ask = (question: string, fallback: string) => new Promise<string>((resolve) => {
|
|
112
|
+
rl.question(`${question} (${fallback}): `, (answer) => resolve(answer.trim() || fallback));
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const seed = defaultAnswers(templateSlug);
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(' ' + banner(AGENTGUARD_SPEND_VERSION));
|
|
118
|
+
console.log('');
|
|
119
|
+
const building = await ask(`What are you building? ${BUILD_TYPES.join(', ')}`, seed.building);
|
|
120
|
+
const template = await ask(`Pick a task template: ${listTaskTemplates().map((item) => item.slug).join(', ')}`, seed.template);
|
|
121
|
+
const capability = await ask(`Capability tier: ${CAPABILITY_TIERS.map(([tier, label]) => tier + ' (' + label + ')').join(', ')}`, seed.capability);
|
|
122
|
+
const templateDef = getTaskTemplate(template) ?? getTaskTemplate(seed.template) as TaskTemplate;
|
|
123
|
+
const allowedModels = (await ask('Allowed models, comma separated', templateDef.allowedModels.join(','))).split(',').map((item) => item.trim()).filter(Boolean);
|
|
124
|
+
const fallbackModel = await ask('Fallback on downgrade', templateDef.fallbackModel);
|
|
125
|
+
const caps = await ask('Caps in cents: per-call, per-day, per-month', `${seed.perCallCents},${seed.perDayCents},${seed.perMonthCents}`);
|
|
126
|
+
const [perCallCents, perDayCents, perMonthCents] = caps.split(',').map((item) => Number(item.trim()));
|
|
127
|
+
const systemInstructions = await ask('System instructions', templateDef.systemInstructions);
|
|
128
|
+
return { building, template, capability, allowedModels, fallbackModel, perCallCents, perDayCents, perMonthCents, systemInstructions };
|
|
129
|
+
} finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function policyYaml(answers: WizardAnswers): string {
|
|
135
|
+
const id = `agentguard-${answers.template}-v1`;
|
|
136
|
+
const allowed = answers.allowedModels.map((model) => ` - ${model}`).join('\n');
|
|
137
|
+
return `# AgentGuard Spend policy generated by agentguard wizard\nid: ${id}\nname: ${answers.template} policy\nversion: 1\neffectiveFrom: "${new Date().toISOString()}"\nmode: enforce\nrequiredCapability: ${answers.capability}\nscope:\n tenantId: my-tenant\nmodels:\n allowed:\n${allowed}\n fallback: ${answers.fallbackModel}\ncaps:\n # WHY: Per-call cap bounds one agent action.\n - amountCents: ${answers.perCallCents}\n window: per_call\n action: downgrade\n downgradeTo: ${answers.fallbackModel}\n reason: "Per-call budget reached, routing to fallback model"\n # WHY: Daily cap catches runaway loops and unexpected volume.\n - amountCents: ${answers.perDayCents}\n window: per_day\n action: block\n reason: "Daily budget reached"\n # WHY: Monthly cap keeps the finance view predictable.\n - amountCents: ${answers.perMonthCents}\n window: per_month\n action: block\n reason: "Monthly budget reached"\nsystemInstructions: |\n${indent(answers.systemInstructions, 2)}\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function quickstartTs(answers: WizardAnswers): string {
|
|
141
|
+
return `import OpenAI from 'openai';\nimport { withSpendGuard, type SpendPolicy } from '@agentguard-run/spend';\n\nconst policy: SpendPolicy = {\n id: 'agentguard-${answers.template}-v1',\n name: '${answers.template} policy',\n scope: { tenantId: 'my-tenant' },\n caps: [\n { amountCents: ${answers.perCallCents}, window: 'per_call', action: 'downgrade', downgradeTo: '${answers.fallbackModel}' },\n { amountCents: ${answers.perDayCents}, window: 'per_day', action: 'block' },\n { amountCents: ${answers.perMonthCents}, window: 'per_month', action: 'block' },\n ],\n mode: 'enforce',\n requiredCapability: '${answers.capability}',\n version: 1,\n effectiveFrom: new Date().toISOString(),\n};\n\nconst openrouter = new OpenAI({\n baseURL: 'https://openrouter.ai/api/v1',\n apiKey: process.env.OPENROUTER_API_KEY,\n});\n\nexport const guardedClient = withSpendGuard(openrouter, {\n policy,\n scope: { tenantId: 'my-tenant', agentId: '${answers.template}' },\n capabilityClaim: '${answers.capability}',\n});\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function quickstartPy(answers: WizardAnswers): string {
|
|
145
|
+
return `from openai import OpenAI\nfrom agentguard_spend import with_spend_guard\nfrom agentguard_spend.types import SpendPolicy, SpendCap\n\npolicy = SpendPolicy(\n id='agentguard-${answers.template}-v1', name='${answers.template} policy',\n scope={'tenantId': 'my-tenant'},\n caps=[\n SpendCap(amountCents=${answers.perCallCents}, window='per_call', action='downgrade', downgradeTo='${answers.fallbackModel}'),\n SpendCap(amountCents=${answers.perDayCents}, window='per_day', action='block'),\n SpendCap(amountCents=${answers.perMonthCents}, window='per_month', action='block'),\n ],\n mode='enforce', requiredCapability='${answers.capability}',\n version=1, effectiveFrom='2026-05-27T00:00:00Z',\n)\n\nopenrouter = OpenAI(base_url='https://openrouter.ai/api/v1')\nguarded_client = with_spend_guard(openrouter, policy=policy, scope={'tenantId': 'my-tenant', 'agentId': '${answers.template}'}, capability_claim='${answers.capability}')\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function tsSnippet(answers: WizardAnswers): string {
|
|
149
|
+
return `const response = await guardedClient.chat.completions.create({\n model: '${answers.allowedModels[0] ?? 'openai/gpt-4o-mini'}',\n messages: [{ role: 'user', content: 'Run the governed task.' }],\n});`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function indent(value: string, spaces: number): string {
|
|
153
|
+
const prefix = ' '.repeat(spaces);
|
|
154
|
+
return value.split('\n').map((line) => prefix + line).join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function valueAfter(argv: string[], flag: string): string | undefined {
|
|
158
|
+
const index = argv.indexOf(flag);
|
|
159
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
160
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentGuard(TM) Spend: OpenRouter catalog sync.
|
|
3
|
+
*
|
|
4
|
+
* OpenRouter requests go directly from the customer runtime to openrouter.ai.
|
|
5
|
+
* AgentGuard infrastructure does not receive prompts, completions, keys,
|
|
6
|
+
* policy files, or pricing overrides.
|
|
7
|
+
*
|
|
8
|
+
* Patent notice: Protected by U.S. patent-pending technology
|
|
9
|
+
* (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626;
|
|
10
|
+
* 64/071,781; 64/071,789).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as https from 'https';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { DEFAULT_COST_OVERRIDES_PATH, persistCostOverrides, setCostOverride, type ModelCost } from './cost-table';
|
|
18
|
+
|
|
19
|
+
export interface OpenRouterPricing {
|
|
20
|
+
prompt?: string;
|
|
21
|
+
completion?: string;
|
|
22
|
+
image?: string;
|
|
23
|
+
request?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OpenRouterModel {
|
|
27
|
+
id: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
context_length?: number;
|
|
30
|
+
pricing?: OpenRouterPricing;
|
|
31
|
+
architecture?: { modality?: string; tokenizer?: string; instruct_type?: string | null };
|
|
32
|
+
top_provider?: { context_length?: number; max_completion_tokens?: number | null; is_moderated?: boolean };
|
|
33
|
+
per_request_limits?: Record<string, unknown> | null;
|
|
34
|
+
[key: string]: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OpenRouterCatalog {
|
|
38
|
+
data: OpenRouterModel[];
|
|
39
|
+
fetchedAt?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FetchCatalogOptions {
|
|
43
|
+
ttlMs?: number;
|
|
44
|
+
force?: boolean;
|
|
45
|
+
endpoint?: string;
|
|
46
|
+
cachePath?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SyncPricingResult {
|
|
50
|
+
count: number;
|
|
51
|
+
applied: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
55
|
+
const STATE_DIR = process.env.AGENTGUARD_HOME || path.join(os.homedir(), '.agentguard');
|
|
56
|
+
const DEFAULT_CACHE_PATH = path.join(STATE_DIR, 'openrouter-catalog.json');
|
|
57
|
+
const DEFAULT_ENDPOINT = 'https://openrouter.ai/api/v1/models';
|
|
58
|
+
|
|
59
|
+
let memoryCatalog: OpenRouterCatalog | null = null;
|
|
60
|
+
|
|
61
|
+
export async function fetchCatalog(opts: FetchCatalogOptions = {}): Promise<OpenRouterCatalog> {
|
|
62
|
+
const ttlMs = opts.ttlMs ?? DAY_MS;
|
|
63
|
+
const cachePath = opts.cachePath ?? DEFAULT_CACHE_PATH;
|
|
64
|
+
if (!opts.force) {
|
|
65
|
+
const cached = readCachedCatalog(cachePath, ttlMs);
|
|
66
|
+
if (cached) return cached;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fixturePath = process.env.AGENTGUARD_OPENROUTER_CATALOG_FIXTURE;
|
|
70
|
+
if (fixturePath) {
|
|
71
|
+
const fixture = parseCatalog(fs.readFileSync(fixturePath, 'utf8'));
|
|
72
|
+
return saveCatalog(fixture, cachePath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const body = await getJson(opts.endpoint ?? DEFAULT_ENDPOINT);
|
|
77
|
+
const catalog = parseCatalog(body);
|
|
78
|
+
return saveCatalog(catalog, cachePath);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const cached = readCachedCatalog(cachePath, Number.MAX_SAFE_INTEGER);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getCachedCatalog(cachePath: string = DEFAULT_CACHE_PATH): OpenRouterCatalog | null {
|
|
87
|
+
if (memoryCatalog) return memoryCatalog;
|
|
88
|
+
return readCachedCatalog(cachePath, Number.MAX_SAFE_INTEGER);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function syncPricingIntoCostTable(opts: FetchCatalogOptions = {}): Promise<SyncPricingResult> {
|
|
92
|
+
const catalog = await fetchCatalog(opts);
|
|
93
|
+
const applied: string[] = [];
|
|
94
|
+
for (const model of catalog.data) {
|
|
95
|
+
const cost = modelCostFromOpenRouter(model);
|
|
96
|
+
if (!cost) continue;
|
|
97
|
+
setCostOverride(model.id, cost);
|
|
98
|
+
applied.push(model.id);
|
|
99
|
+
}
|
|
100
|
+
persistCostOverrides(DEFAULT_COST_OVERRIDES_PATH);
|
|
101
|
+
return { count: applied.length, applied };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function persistOverrides(filePath?: string): void {
|
|
105
|
+
persistCostOverrides(filePath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function modelCostFromOpenRouter(model: OpenRouterModel): ModelCost | null {
|
|
109
|
+
const prompt = Number(model.pricing?.prompt);
|
|
110
|
+
const completion = Number(model.pricing?.completion);
|
|
111
|
+
if (!Number.isFinite(prompt) || !Number.isFinite(completion) || prompt < 0 || completion < 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
inputCentsPerKtok: prompt * 100 * 1000,
|
|
116
|
+
outputCentsPerKtok: completion * 100 * 1000,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readCachedCatalog(filePath: string, ttlMs: number): OpenRouterCatalog | null {
|
|
121
|
+
try {
|
|
122
|
+
const stat = fs.statSync(filePath);
|
|
123
|
+
if (Date.now() - stat.mtimeMs > ttlMs) return null;
|
|
124
|
+
const catalog = parseCatalog(fs.readFileSync(filePath, 'utf8'));
|
|
125
|
+
memoryCatalog = catalog;
|
|
126
|
+
return catalog;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function saveCatalog(catalog: OpenRouterCatalog, filePath: string): OpenRouterCatalog {
|
|
133
|
+
const stamped: OpenRouterCatalog = { ...catalog, fetchedAt: catalog.fetchedAt ?? new Date().toISOString() };
|
|
134
|
+
memoryCatalog = stamped;
|
|
135
|
+
try {
|
|
136
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
137
|
+
fs.writeFileSync(filePath, JSON.stringify(stamped, null, 2) + '\n', { mode: 0o600 });
|
|
138
|
+
try { fs.chmodSync(filePath, 0o600); } catch { return stamped; }
|
|
139
|
+
} catch {
|
|
140
|
+
return stamped;
|
|
141
|
+
}
|
|
142
|
+
return stamped;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseCatalog(body: string): OpenRouterCatalog {
|
|
146
|
+
const parsed = JSON.parse(body) as OpenRouterCatalog;
|
|
147
|
+
if (!parsed || !Array.isArray(parsed.data)) throw new Error('OpenRouter catalog response must include data[]');
|
|
148
|
+
const data = parsed.data.filter((item): item is OpenRouterModel => Boolean(item && typeof item.id === 'string'));
|
|
149
|
+
return { ...parsed, data };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getJson(endpoint: string): Promise<string> {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const url = new URL(endpoint);
|
|
155
|
+
const req = https.request(
|
|
156
|
+
{
|
|
157
|
+
method: 'GET',
|
|
158
|
+
hostname: url.hostname,
|
|
159
|
+
path: url.pathname + url.search,
|
|
160
|
+
headers: { accept: 'application/json' },
|
|
161
|
+
timeout: 5000,
|
|
162
|
+
},
|
|
163
|
+
(res) => {
|
|
164
|
+
const chunks: Buffer[] = [];
|
|
165
|
+
res.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
166
|
+
res.on('end', () => {
|
|
167
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
168
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
169
|
+
reject(new Error('OpenRouter catalog fetch failed with HTTP ' + String(res.statusCode)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
resolve(body);
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
req.on('error', reject);
|
|
177
|
+
req.on('timeout', () => req.destroy(new Error('OpenRouter catalog fetch timed out')));
|
|
178
|
+
req.end();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# AgentGuard Spend task template: agent-support
|
|
2
|
+
# Local-only policy file. Prompts, completions, API keys, and signing keys stay in the customer runtime.
|
|
3
|
+
id: agent-support-v1
|
|
4
|
+
name: Agent support workflow
|
|
5
|
+
version: 1
|
|
6
|
+
effectiveFrom: "2026-05-27T00:00:00.000Z"
|
|
7
|
+
mode: enforce
|
|
8
|
+
requiredCapability: data_write
|
|
9
|
+
scope:
|
|
10
|
+
tenantId: my-tenant
|
|
11
|
+
models:
|
|
12
|
+
primary: openai/gpt-4o-mini
|
|
13
|
+
fallback: google/gemini-3-flash-preview
|
|
14
|
+
allowed:
|
|
15
|
+
- openai/gpt-4o-mini
|
|
16
|
+
- google/gemini-3-flash-preview
|
|
17
|
+
caps:
|
|
18
|
+
# WHY: 25 cents per call bounds one agent action while keeping normal work flowing.
|
|
19
|
+
- amountCents: 25
|
|
20
|
+
window: per_call
|
|
21
|
+
action: downgrade
|
|
22
|
+
downgradeTo: google/gemini-3-flash-preview
|
|
23
|
+
reason: "Per-call budget reached, routing to fallback model"
|
|
24
|
+
# WHY: Daily cap catches loops and unexpected traffic before monthly budgets drift.
|
|
25
|
+
- amountCents: 10000
|
|
26
|
+
window: per_day
|
|
27
|
+
action: block
|
|
28
|
+
reason: "Daily budget reached"
|
|
29
|
+
systemInstructions: |
|
|
30
|
+
Draft support replies and update allowed fields only after policy checks. Escalate billing, payment, and identity changes.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# AgentGuard Spend task template: chargeback-evidence
|
|
2
|
+
# Local-only policy file. Prompts, completions, API keys, and signing keys stay in the customer runtime.
|
|
3
|
+
id: chargeback-evidence-v1
|
|
4
|
+
name: Chargeback evidence agent
|
|
5
|
+
version: 1
|
|
6
|
+
effectiveFrom: "2026-05-27T00:00:00.000Z"
|
|
7
|
+
mode: enforce
|
|
8
|
+
requiredCapability: read_only
|
|
9
|
+
scope:
|
|
10
|
+
tenantId: my-tenant
|
|
11
|
+
models:
|
|
12
|
+
primary: openai/gpt-5-mini
|
|
13
|
+
fallback: openai/gpt-4o-mini
|
|
14
|
+
allowed:
|
|
15
|
+
- openai/gpt-5-mini
|
|
16
|
+
- openai/gpt-4o-mini
|
|
17
|
+
caps:
|
|
18
|
+
# WHY: 100 cents per call bounds one agent action while keeping normal work flowing.
|
|
19
|
+
- amountCents: 100
|
|
20
|
+
window: per_call
|
|
21
|
+
action: downgrade
|
|
22
|
+
downgradeTo: openai/gpt-4o-mini
|
|
23
|
+
reason: "Per-call budget reached, routing to fallback model"
|
|
24
|
+
# WHY: Daily cap catches loops and unexpected traffic before monthly budgets drift.
|
|
25
|
+
- amountCents: 5000
|
|
26
|
+
window: per_day
|
|
27
|
+
action: block
|
|
28
|
+
reason: "Daily budget reached"
|
|
29
|
+
systemInstructions: |
|
|
30
|
+
Assemble claim evidence from provided records. Cite source IDs and keep disputed facts separate from verified records.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# AgentGuard Spend task template: code-scan
|
|
2
|
+
# Local-only policy file. Prompts, completions, API keys, and signing keys stay in the customer runtime.
|
|
3
|
+
id: code-scan-v1
|
|
4
|
+
name: Code scan agent
|
|
5
|
+
version: 1
|
|
6
|
+
effectiveFrom: "2026-05-27T00:00:00.000Z"
|
|
7
|
+
mode: enforce
|
|
8
|
+
requiredCapability: read_only
|
|
9
|
+
scope:
|
|
10
|
+
tenantId: my-tenant
|
|
11
|
+
models:
|
|
12
|
+
primary: google/gemini-3-flash-preview
|
|
13
|
+
fallback: openai/gpt-4o-mini
|
|
14
|
+
allowed:
|
|
15
|
+
- google/gemini-3-flash-preview
|
|
16
|
+
- openai/gpt-4o-mini
|
|
17
|
+
caps:
|
|
18
|
+
# WHY: 10 cents per call bounds one agent action while keeping normal work flowing.
|
|
19
|
+
- amountCents: 10
|
|
20
|
+
window: per_call
|
|
21
|
+
action: downgrade
|
|
22
|
+
downgradeTo: openai/gpt-4o-mini
|
|
23
|
+
reason: "Per-call budget reached, routing to fallback model"
|
|
24
|
+
# WHY: Daily cap catches loops and unexpected traffic before monthly budgets drift.
|
|
25
|
+
- amountCents: 3000
|
|
26
|
+
window: per_day
|
|
27
|
+
action: block
|
|
28
|
+
reason: "Daily budget reached"
|
|
29
|
+
systemInstructions: |
|
|
30
|
+
Scan code for spend, audit, and integration risks. Return findings with file paths and minimal fix guidance.
|