@agentguard-run/spend 0.15.5 → 0.15.7

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/src/cli/wizard.ts CHANGED
@@ -1,10 +1,39 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as readline from 'readline';
4
+ import { randomBytes } from 'crypto';
5
+ import * as ed from '@noble/ed25519';
4
6
  import { AGENTGUARD_SPEND_VERSION } from '../index';
5
7
  import { TASK_TEMPLATES, getTaskTemplate, listTaskTemplates, type TaskTemplate } from '../templates';
6
8
  import { agentguardHome, banner, cyanBold, green, statusBar } from './colors';
7
9
 
10
+ const SIGNING_PRIVATE_KEY_FILE = 'signing-private.key';
11
+ const SIGNING_PUBLIC_KEY_FILE = 'signing-public.key';
12
+
13
+ /**
14
+ * Read the existing local signing keypair or generate one. The private seed is
15
+ * stored mode 0600 under ~/.agentguard/ and never leaves the machine. Returns
16
+ * both keys as lowercase hex.
17
+ */
18
+ export async function ensureSigningKeys(home = agentguardHome()): Promise<{ privateKeyHex: string; publicKeyHex: string }> {
19
+ fs.mkdirSync(home, { recursive: true });
20
+ const privatePath = path.join(home, SIGNING_PRIVATE_KEY_FILE);
21
+ const publicPath = path.join(home, SIGNING_PUBLIC_KEY_FILE);
22
+ if (fs.existsSync(privatePath) && fs.existsSync(publicPath)) {
23
+ return {
24
+ privateKeyHex: fs.readFileSync(privatePath, 'utf-8').trim(),
25
+ publicKeyHex: fs.readFileSync(publicPath, 'utf-8').trim(),
26
+ };
27
+ }
28
+ const privateSeed = new Uint8Array(randomBytes(32));
29
+ const publicKey = await ed.getPublicKeyAsync(privateSeed);
30
+ const privateKeyHex = Buffer.from(privateSeed).toString('hex');
31
+ const publicKeyHex = Buffer.from(publicKey).toString('hex');
32
+ fs.writeFileSync(privatePath, privateKeyHex, { mode: 0o600 });
33
+ fs.writeFileSync(publicPath, publicKeyHex);
34
+ return { privateKeyHex, publicKeyHex };
35
+ }
36
+
8
37
  const BUILD_TYPES = ['chatbot', 'agent', 'batch job', 'API endpoint'];
9
38
  const CAPABILITY_TIERS = [
10
39
  ['read_only', 'Read records and produce findings'],
@@ -58,7 +87,8 @@ export async function runWizard(argv: string[]): Promise<number> {
58
87
  const templateSlug = valueAfter(argv, '--template') ?? 'risk-review';
59
88
  const useDefaults = argv.includes('--defaults') || !process.stdin.isTTY;
60
89
  const answers = useDefaults ? defaultAnswers(templateSlug) : await promptAnswers(templateSlug);
61
- const outputs = writeWizardOutputs(answers);
90
+ const signingKeys = await ensureSigningKeys();
91
+ const outputs = writeWizardOutputs(answers, agentguardHome(), signingKeys);
62
92
  console.log('');
63
93
  console.log(' ' + banner(AGENTGUARD_SPEND_VERSION));
64
94
  console.log('');
@@ -92,13 +122,17 @@ export function defaultAnswers(templateSlug = 'risk-review'): WizardAnswers {
92
122
  };
93
123
  }
94
124
 
95
- export function writeWizardOutputs(answers: WizardAnswers, home = agentguardHome()): { policyPath: string; quickstartTsPath: string; quickstartPyPath: string; snippet: string } {
125
+ export function writeWizardOutputs(
126
+ answers: WizardAnswers,
127
+ home = agentguardHome(),
128
+ signingKeys?: { publicKeyHex: string },
129
+ ): { policyPath: string; quickstartTsPath: string; quickstartPyPath: string; snippet: string } {
96
130
  fs.mkdirSync(home, { recursive: true });
97
131
  const policyPath = path.join(home, 'policy.yaml');
98
132
  const quickstartTsPath = path.join(home, 'quickstart.ts');
99
133
  const quickstartPyPath = path.join(home, 'quickstart.py');
100
134
  const policy = policyYaml(answers);
101
- const ts = quickstartTs(answers);
135
+ const ts = quickstartTs(answers, signingKeys?.publicKeyHex);
102
136
  const py = quickstartPy(answers);
103
137
  fs.writeFileSync(policyPath, policy);
104
138
  fs.writeFileSync(quickstartTsPath, ts);
@@ -137,8 +171,9 @@ function policyYaml(answers: WizardAnswers): string {
137
171
  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
172
  }
139
173
 
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`;
174
+ function quickstartTs(answers: WizardAnswers, publicKeyHex?: string): string {
175
+ const pubHex = publicKeyHex ?? '';
176
+ return `import OpenAI from 'openai';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { withSpendGuard, NdjsonDecisionLogStore, 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\n// Local Ed25519 signing key generated by \`agentguard wizard\` (never leaves this machine).\nconst home = path.join(os.homedir(), '.agentguard');\nconst signingKeys = {\n privateKey: Uint8Array.from(Buffer.from(fs.readFileSync(path.join(home, 'signing-private.key'), 'utf-8').trim(), 'hex')),\n publicKey: Uint8Array.from(Buffer.from(fs.readFileSync(path.join(home, 'signing-public.key'), 'utf-8').trim(), 'hex')),\n};\nconst publicKeyHex = '${pubHex}' || Buffer.from(signingKeys.publicKey).toString('hex');\n\nexport const guardedClient = withSpendGuard(openrouter, {\n policy,\n scope: { tenantId: 'my-tenant', agentId: '${answers.template}' },\n capabilityClaim: '${answers.capability}',\n config: {\n // Persists signed receipts to ~/.agentguard/my-tenant/decisions.ndjson so\n // \`agentguard serve\` shows governed spend, $ saved, and a verifiable trail.\n logStore: new NdjsonDecisionLogStore('my-tenant', { publicKeyHex }),\n signingKeys,\n },\n});\n`;
142
177
  }
143
178
 
144
179
  function quickstartPy(answers: WizardAnswers): string {