import * as fs from 'fs'; import * as path from 'path'; export interface ByoOutcomeConfig { slug: string; name: string; vertical: string; outcome: string; primaryModel: string; fallbackModel: string; maxUsdPerRun: number; maxIterations: number; maxTokens: number; repeatThreshold: number; windowSeconds: number; stepCap: number; reviewerMode: 'human' | 'model'; destructivePatterns: string[]; } export const STARTER_TEMPLATE_NAMES = [ 'scraper-under-cap', 'research-agent', 'coding-agent-overnight', 'content-pipeline', 'batch-classifier', ] as const; export type StarterTemplateName = typeof STARTER_TEMPLATE_NAMES[number]; const STARTERS: Record = { 'scraper-under-cap': { slug: 'scraper-under-cap', name: 'Scraper under cap', vertical: 'generic', outcome: 'page_batch_extracted', primaryModel: 'openai/gpt-4o-mini', fallbackModel: 'baseten/llama-4-13b', maxUsdPerRun: 1, maxIterations: 50, maxTokens: 200000, repeatThreshold: 3, windowSeconds: 300, stepCap: 50, reviewerMode: 'model', destructivePatterns: ['email.send_bulk', 'db.write'], }, 'research-agent': { slug: 'research-agent', name: 'Research agent', vertical: 'generic', outcome: 'research_brief_completed', primaryModel: 'anthropic/claude-sonnet-4-6', fallbackModel: 'openai/gpt-4o-mini', maxUsdPerRun: 5, maxIterations: 80, maxTokens: 800000, repeatThreshold: 3, windowSeconds: 300, stepCap: 60, reviewerMode: 'model', destructivePatterns: ['db.write', 'deploy.*', 'email.send_bulk'], }, 'coding-agent-overnight': { slug: 'coding-agent-overnight', name: 'Coding agent overnight', vertical: 'software', outcome: 'pull_request_prepared', primaryModel: 'anthropic/claude-sonnet-4-6', fallbackModel: 'baseten/llama-4', maxUsdPerRun: 8, maxIterations: 100, maxTokens: 2000000, repeatThreshold: 3, windowSeconds: 300, stepCap: 50, reviewerMode: 'human', destructivePatterns: ['shell.rm', 'rm -rf', 'git push --force', 'git push -f', 'deploy.*'], }, 'content-pipeline': { slug: 'content-pipeline', name: 'Content pipeline', vertical: 'marketing', outcome: 'content_batch_ready', primaryModel: 'openai/gpt-4o-mini', fallbackModel: 'baseten/llama-4-13b', maxUsdPerRun: 3, maxIterations: 60, maxTokens: 500000, repeatThreshold: 3, windowSeconds: 300, stepCap: 50, reviewerMode: 'model', destructivePatterns: ['email.send_bulk', 'deploy.*'], }, 'batch-classifier': { slug: 'batch-classifier', name: 'Batch classifier', vertical: 'operations', outcome: 'batch_classified', primaryModel: 'baseten/llama-4-13b', fallbackModel: 'openai/gpt-4o-mini', maxUsdPerRun: 2, maxIterations: 100, maxTokens: 1000000, repeatThreshold: 3, windowSeconds: 300, stepCap: 50, reviewerMode: 'model', destructivePatterns: ['db.write', 'db.delete'], }, }; export function listStarterTemplates(): StarterTemplateName[] { return [...STARTER_TEMPLATE_NAMES]; } export function isStarterTemplateName(value: string | undefined): value is StarterTemplateName { return !!value && (STARTER_TEMPLATE_NAMES as readonly string[]).includes(value); } export function starterConfig(name: StarterTemplateName): ByoOutcomeConfig { return { ...STARTERS[name], destructivePatterns: [...STARTERS[name].destructivePatterns] }; } export function starterYaml(name: StarterTemplateName): string { return renderOutcomeYaml(starterConfig(name)); } export function renderOutcomeYaml(config: ByoOutcomeConfig): string { return `# AgentGuard BYO outcome. Metadata-only governance, no provider keys stored here. slug: ${config.slug} name: ${config.name} vertical: ${config.vertical} outcome: ${config.outcome} models: primary: ${config.primaryModel} fallback: ${config.fallbackModel} workflow_envelope: max_usd_per_run: ${config.maxUsdPerRun.toFixed(2)} max_iterations: ${config.maxIterations} max_tokens: ${config.maxTokens} max_wall_clock_seconds: 3600 daily_usd_cap: ${(config.maxUsdPerRun * 10).toFixed(2)} circuit_breaker: enabled: true repeat_threshold: ${config.repeatThreshold} window_seconds: ${config.windowSeconds} plan_churn_max: 2 step_cap: ${config.stepCap} reviewer_cascade: enabled: true mode: ${config.reviewerMode} destructive_patterns: ${config.destructivePatterns.map((pattern) => ` - ${JSON.stringify(pattern)}`).join('\n')} inputs: - name: task_id type: string required: true - name: payload_ref type: string required: true outputs: format: signed_receipt content_policy: metadata_only `; } export function parseOutcomeYaml(source: string): ByoOutcomeConfig { const read = (key: string, fallback = '') => findScalar(source, key) || fallback; const number = (key: string, fallback: number) => { const value = Number(read(key)); return Number.isFinite(value) && value > 0 ? value : fallback; }; const slug = read('slug'); const primaryModel = findNestedScalar(source, 'models', 'primary') || read('primary_model'); const fallbackModel = findNestedScalar(source, 'models', 'fallback') || read('fallback_model'); const destructivePatterns = findList(source, 'destructive_patterns'); return { slug, name: read('name', slug), vertical: read('vertical', 'generic'), outcome: read('outcome', slug), primaryModel, fallbackModel, maxUsdPerRun: number('max_usd_per_run', 5), maxIterations: Math.round(number('max_iterations', 100)), maxTokens: Math.round(number('max_tokens', 2000000)), repeatThreshold: Math.round(number('repeat_threshold', 3)), windowSeconds: Math.round(number('window_seconds', 300)), stepCap: Math.round(number('step_cap', 50)), reviewerMode: read('mode', 'model') === 'human' ? 'human' : 'model', destructivePatterns: destructivePatterns.length ? destructivePatterns : ['deploy.*', 'shell.rm', 'payment.*'], }; } export function validateOutcomeYaml(source: string): { ok: boolean; errors: string[]; config?: ByoOutcomeConfig } { const errors: string[] = []; let config: ByoOutcomeConfig; try { config = parseOutcomeYaml(source); } catch (err) { return { ok: false, errors: [err instanceof Error ? err.message : 'Could not parse outcome YAML'] }; } if (!config.slug) errors.push('slug is required'); if (!/^[a-z0-9][a-z0-9-]*$/.test(config.slug)) errors.push('slug must use lowercase letters, numbers, and hyphens'); if (!config.primaryModel) errors.push('models.primary is required'); if (!config.fallbackModel) errors.push('models.fallback is required'); if (config.maxUsdPerRun <= 0) errors.push('workflow_envelope.max_usd_per_run must be positive'); if (config.repeatThreshold < 2) errors.push('circuit_breaker.repeat_threshold must be at least 2'); return errors.length ? { ok: false, errors } : { ok: true, errors: [], config }; } export function writeStarterTemplate(name: StarterTemplateName, outputPath = path.join(process.cwd(), 'outcome.yaml'), force = false): string { if (fs.existsSync(outputPath) && !force) { throw new Error(`${outputPath} already exists. Pass --force to overwrite.`); } fs.writeFileSync(outputPath, starterYaml(name), { mode: 0o600 }); return outputPath; } export function simulateOutcomeRun(config: ByoOutcomeConfig, inputs: Record): Record { return { guarded: true, status: 'planned', slug: config.slug, outcome: config.outcome, workflowEnvelope: { maxUsdPerRun: config.maxUsdPerRun, maxIterations: config.maxIterations, maxTokens: config.maxTokens, }, circuitBreaker: { repeatThreshold: config.repeatThreshold, windowSeconds: config.windowSeconds, stepCap: config.stepCap, }, reviewerCascade: { mode: config.reviewerMode, destructivePatterns: config.destructivePatterns, }, inputKeys: Object.keys(inputs).sort(), keySource: 'env_or_keychain', }; } export function persistDeployment(config: ByoOutcomeConfig, dir = path.join(process.cwd(), '.agentguard', 'deployments')): string { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); const file = path.join(dir, `${config.slug}.json`); fs.writeFileSync(file, JSON.stringify({ ...config, deployedAt: new Date().toISOString(), keySource: 'env_or_keychain' }, null, 2) + '\n', { mode: 0o600 }); return file; } function findScalar(source: string, key: string): string | null { const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*(.+?)\\s*$`, 'm'); const match = source.match(re); return match ? cleanScalar(match[1]!) : null; } function findNestedScalar(source: string, parent: string, key: string): string | null { const section = sectionLines(source, parent); for (const line of section) { const trimmed = line.trim(); const prefix = key + ':'; if (trimmed.startsWith(prefix)) return cleanScalar(trimmed.slice(prefix.length)); } return null; } function findList(source: string, key: string): string[] { const lines = source.split('\n'); const out: string[] = []; let collecting = false; for (const line of lines) { const trimmed = line.trim(); if (!collecting && trimmed === key + ':') { collecting = true; continue; } if (!collecting) continue; if (trimmed.startsWith('- ')) { out.push(cleanScalar(trimmed.slice(2))); continue; } if (trimmed && !line.startsWith(' ')) break; } return out; } function sectionLines(source: string, parent: string): string[] { const lines = source.split('\n'); const out: string[] = []; let collecting = false; for (const line of lines) { if (!collecting && line.trim() === parent + ':') { collecting = true; continue; } if (!collecting) continue; if (line.trim() && !line.startsWith(' ')) break; out.push(line); } return out; } function cleanScalar(value: string): string { const trimmed = value.trim(); if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) { return trimmed.slice(1, -1); } return trimmed; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }