@agentguard-run/spend 0.9.0 → 0.10.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 +6 -0
- package/dist/bindings/anthropic.js +18 -0
- package/dist/bindings/anthropic.js.map +1 -1
- package/dist/bindings/bedrock.js +9 -0
- package/dist/bindings/bedrock.js.map +1 -1
- package/dist/byo/index.d.ts +33 -0
- package/dist/byo/index.d.ts.map +1 -0
- package/dist/byo/index.js +344 -0
- package/dist/byo/index.js.map +1 -0
- package/dist/cli/byo.d.ts +4 -0
- package/dist/cli/byo.d.ts.map +1 -0
- package/dist/cli/byo.js +118 -0
- package/dist/cli/byo.js.map +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +5 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +14 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/governance.d.ts +77 -0
- package/dist/governance.d.ts.map +1 -0
- package/dist/governance.js +283 -0
- package/dist/governance.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/spend-guard.d.ts +19 -0
- package/dist/spend-guard.d.ts.map +1 -1
- package/dist/spend-guard.js +75 -1
- package/dist/spend-guard.js.map +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -2
- package/src/bindings/anthropic.ts +21 -0
- package/src/bindings/bedrock.ts +10 -0
- package/src/byo/index.ts +322 -0
- package/src/cli/byo.ts +87 -0
- package/src/governance.ts +376 -0
package/src/byo/index.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ByoOutcomeConfig {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
vertical: string;
|
|
8
|
+
outcome: string;
|
|
9
|
+
primaryModel: string;
|
|
10
|
+
fallbackModel: string;
|
|
11
|
+
maxUsdPerRun: number;
|
|
12
|
+
maxIterations: number;
|
|
13
|
+
maxTokens: number;
|
|
14
|
+
repeatThreshold: number;
|
|
15
|
+
windowSeconds: number;
|
|
16
|
+
stepCap: number;
|
|
17
|
+
reviewerMode: 'human' | 'model';
|
|
18
|
+
destructivePatterns: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const STARTER_TEMPLATE_NAMES = [
|
|
22
|
+
'scraper-under-cap',
|
|
23
|
+
'research-agent',
|
|
24
|
+
'coding-agent-overnight',
|
|
25
|
+
'content-pipeline',
|
|
26
|
+
'batch-classifier',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export type StarterTemplateName = typeof STARTER_TEMPLATE_NAMES[number];
|
|
30
|
+
|
|
31
|
+
const STARTERS: Record<StarterTemplateName, ByoOutcomeConfig> = {
|
|
32
|
+
'scraper-under-cap': {
|
|
33
|
+
slug: 'scraper-under-cap',
|
|
34
|
+
name: 'Scraper under cap',
|
|
35
|
+
vertical: 'generic',
|
|
36
|
+
outcome: 'page_batch_extracted',
|
|
37
|
+
primaryModel: 'openai/gpt-4o-mini',
|
|
38
|
+
fallbackModel: 'baseten/llama-4-13b',
|
|
39
|
+
maxUsdPerRun: 1,
|
|
40
|
+
maxIterations: 50,
|
|
41
|
+
maxTokens: 200000,
|
|
42
|
+
repeatThreshold: 3,
|
|
43
|
+
windowSeconds: 300,
|
|
44
|
+
stepCap: 50,
|
|
45
|
+
reviewerMode: 'model',
|
|
46
|
+
destructivePatterns: ['email.send_bulk', 'db.write'],
|
|
47
|
+
},
|
|
48
|
+
'research-agent': {
|
|
49
|
+
slug: 'research-agent',
|
|
50
|
+
name: 'Research agent',
|
|
51
|
+
vertical: 'generic',
|
|
52
|
+
outcome: 'research_brief_completed',
|
|
53
|
+
primaryModel: 'anthropic/claude-sonnet-4-6',
|
|
54
|
+
fallbackModel: 'openai/gpt-4o-mini',
|
|
55
|
+
maxUsdPerRun: 5,
|
|
56
|
+
maxIterations: 80,
|
|
57
|
+
maxTokens: 800000,
|
|
58
|
+
repeatThreshold: 3,
|
|
59
|
+
windowSeconds: 300,
|
|
60
|
+
stepCap: 60,
|
|
61
|
+
reviewerMode: 'model',
|
|
62
|
+
destructivePatterns: ['db.write', 'deploy.*', 'email.send_bulk'],
|
|
63
|
+
},
|
|
64
|
+
'coding-agent-overnight': {
|
|
65
|
+
slug: 'coding-agent-overnight',
|
|
66
|
+
name: 'Coding agent overnight',
|
|
67
|
+
vertical: 'software',
|
|
68
|
+
outcome: 'pull_request_prepared',
|
|
69
|
+
primaryModel: 'anthropic/claude-sonnet-4-6',
|
|
70
|
+
fallbackModel: 'baseten/llama-4',
|
|
71
|
+
maxUsdPerRun: 8,
|
|
72
|
+
maxIterations: 100,
|
|
73
|
+
maxTokens: 2000000,
|
|
74
|
+
repeatThreshold: 3,
|
|
75
|
+
windowSeconds: 300,
|
|
76
|
+
stepCap: 50,
|
|
77
|
+
reviewerMode: 'human',
|
|
78
|
+
destructivePatterns: ['shell.rm', 'rm -rf', 'git push --force', 'git push -f', 'deploy.*'],
|
|
79
|
+
},
|
|
80
|
+
'content-pipeline': {
|
|
81
|
+
slug: 'content-pipeline',
|
|
82
|
+
name: 'Content pipeline',
|
|
83
|
+
vertical: 'marketing',
|
|
84
|
+
outcome: 'content_batch_ready',
|
|
85
|
+
primaryModel: 'openai/gpt-4o-mini',
|
|
86
|
+
fallbackModel: 'baseten/llama-4-13b',
|
|
87
|
+
maxUsdPerRun: 3,
|
|
88
|
+
maxIterations: 60,
|
|
89
|
+
maxTokens: 500000,
|
|
90
|
+
repeatThreshold: 3,
|
|
91
|
+
windowSeconds: 300,
|
|
92
|
+
stepCap: 50,
|
|
93
|
+
reviewerMode: 'model',
|
|
94
|
+
destructivePatterns: ['email.send_bulk', 'deploy.*'],
|
|
95
|
+
},
|
|
96
|
+
'batch-classifier': {
|
|
97
|
+
slug: 'batch-classifier',
|
|
98
|
+
name: 'Batch classifier',
|
|
99
|
+
vertical: 'operations',
|
|
100
|
+
outcome: 'batch_classified',
|
|
101
|
+
primaryModel: 'baseten/llama-4-13b',
|
|
102
|
+
fallbackModel: 'openai/gpt-4o-mini',
|
|
103
|
+
maxUsdPerRun: 2,
|
|
104
|
+
maxIterations: 100,
|
|
105
|
+
maxTokens: 1000000,
|
|
106
|
+
repeatThreshold: 3,
|
|
107
|
+
windowSeconds: 300,
|
|
108
|
+
stepCap: 50,
|
|
109
|
+
reviewerMode: 'model',
|
|
110
|
+
destructivePatterns: ['db.write', 'db.delete'],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function listStarterTemplates(): StarterTemplateName[] {
|
|
115
|
+
return [...STARTER_TEMPLATE_NAMES];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isStarterTemplateName(value: string | undefined): value is StarterTemplateName {
|
|
119
|
+
return !!value && (STARTER_TEMPLATE_NAMES as readonly string[]).includes(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function starterConfig(name: StarterTemplateName): ByoOutcomeConfig {
|
|
123
|
+
return { ...STARTERS[name], destructivePatterns: [...STARTERS[name].destructivePatterns] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function starterYaml(name: StarterTemplateName): string {
|
|
127
|
+
return renderOutcomeYaml(starterConfig(name));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function renderOutcomeYaml(config: ByoOutcomeConfig): string {
|
|
131
|
+
return `# AgentGuard BYO outcome. Metadata-only governance, no provider keys stored here.
|
|
132
|
+
slug: ${config.slug}
|
|
133
|
+
name: ${config.name}
|
|
134
|
+
vertical: ${config.vertical}
|
|
135
|
+
outcome: ${config.outcome}
|
|
136
|
+
|
|
137
|
+
models:
|
|
138
|
+
primary: ${config.primaryModel}
|
|
139
|
+
fallback: ${config.fallbackModel}
|
|
140
|
+
|
|
141
|
+
workflow_envelope:
|
|
142
|
+
max_usd_per_run: ${config.maxUsdPerRun.toFixed(2)}
|
|
143
|
+
max_iterations: ${config.maxIterations}
|
|
144
|
+
max_tokens: ${config.maxTokens}
|
|
145
|
+
max_wall_clock_seconds: 3600
|
|
146
|
+
daily_usd_cap: ${(config.maxUsdPerRun * 10).toFixed(2)}
|
|
147
|
+
|
|
148
|
+
circuit_breaker:
|
|
149
|
+
enabled: true
|
|
150
|
+
repeat_threshold: ${config.repeatThreshold}
|
|
151
|
+
window_seconds: ${config.windowSeconds}
|
|
152
|
+
plan_churn_max: 2
|
|
153
|
+
step_cap: ${config.stepCap}
|
|
154
|
+
|
|
155
|
+
reviewer_cascade:
|
|
156
|
+
enabled: true
|
|
157
|
+
mode: ${config.reviewerMode}
|
|
158
|
+
destructive_patterns:
|
|
159
|
+
${config.destructivePatterns.map((pattern) => ` - ${JSON.stringify(pattern)}`).join('\n')}
|
|
160
|
+
|
|
161
|
+
inputs:
|
|
162
|
+
- name: task_id
|
|
163
|
+
type: string
|
|
164
|
+
required: true
|
|
165
|
+
- name: payload_ref
|
|
166
|
+
type: string
|
|
167
|
+
required: true
|
|
168
|
+
|
|
169
|
+
outputs:
|
|
170
|
+
format: signed_receipt
|
|
171
|
+
content_policy: metadata_only
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseOutcomeYaml(source: string): ByoOutcomeConfig {
|
|
176
|
+
const read = (key: string, fallback = '') => findScalar(source, key) || fallback;
|
|
177
|
+
const number = (key: string, fallback: number) => {
|
|
178
|
+
const value = Number(read(key));
|
|
179
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
180
|
+
};
|
|
181
|
+
const slug = read('slug');
|
|
182
|
+
const primaryModel = findNestedScalar(source, 'models', 'primary') || read('primary_model');
|
|
183
|
+
const fallbackModel = findNestedScalar(source, 'models', 'fallback') || read('fallback_model');
|
|
184
|
+
const destructivePatterns = findList(source, 'destructive_patterns');
|
|
185
|
+
return {
|
|
186
|
+
slug,
|
|
187
|
+
name: read('name', slug),
|
|
188
|
+
vertical: read('vertical', 'generic'),
|
|
189
|
+
outcome: read('outcome', slug),
|
|
190
|
+
primaryModel,
|
|
191
|
+
fallbackModel,
|
|
192
|
+
maxUsdPerRun: number('max_usd_per_run', 5),
|
|
193
|
+
maxIterations: Math.round(number('max_iterations', 100)),
|
|
194
|
+
maxTokens: Math.round(number('max_tokens', 2000000)),
|
|
195
|
+
repeatThreshold: Math.round(number('repeat_threshold', 3)),
|
|
196
|
+
windowSeconds: Math.round(number('window_seconds', 300)),
|
|
197
|
+
stepCap: Math.round(number('step_cap', 50)),
|
|
198
|
+
reviewerMode: read('mode', 'model') === 'human' ? 'human' : 'model',
|
|
199
|
+
destructivePatterns: destructivePatterns.length ? destructivePatterns : ['deploy.*', 'shell.rm', 'payment.*'],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function validateOutcomeYaml(source: string): { ok: boolean; errors: string[]; config?: ByoOutcomeConfig } {
|
|
204
|
+
const errors: string[] = [];
|
|
205
|
+
let config: ByoOutcomeConfig;
|
|
206
|
+
try {
|
|
207
|
+
config = parseOutcomeYaml(source);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { ok: false, errors: [err instanceof Error ? err.message : 'Could not parse outcome YAML'] };
|
|
210
|
+
}
|
|
211
|
+
if (!config.slug) errors.push('slug is required');
|
|
212
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(config.slug)) errors.push('slug must use lowercase letters, numbers, and hyphens');
|
|
213
|
+
if (!config.primaryModel) errors.push('models.primary is required');
|
|
214
|
+
if (!config.fallbackModel) errors.push('models.fallback is required');
|
|
215
|
+
if (config.maxUsdPerRun <= 0) errors.push('workflow_envelope.max_usd_per_run must be positive');
|
|
216
|
+
if (config.repeatThreshold < 2) errors.push('circuit_breaker.repeat_threshold must be at least 2');
|
|
217
|
+
return errors.length ? { ok: false, errors } : { ok: true, errors: [], config };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function writeStarterTemplate(name: StarterTemplateName, outputPath = path.join(process.cwd(), 'outcome.yaml'), force = false): string {
|
|
221
|
+
if (fs.existsSync(outputPath) && !force) {
|
|
222
|
+
throw new Error(`${outputPath} already exists. Pass --force to overwrite.`);
|
|
223
|
+
}
|
|
224
|
+
fs.writeFileSync(outputPath, starterYaml(name), { mode: 0o600 });
|
|
225
|
+
return outputPath;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function simulateOutcomeRun(config: ByoOutcomeConfig, inputs: Record<string, unknown>): Record<string, unknown> {
|
|
229
|
+
return {
|
|
230
|
+
guarded: true,
|
|
231
|
+
status: 'planned',
|
|
232
|
+
slug: config.slug,
|
|
233
|
+
outcome: config.outcome,
|
|
234
|
+
workflowEnvelope: {
|
|
235
|
+
maxUsdPerRun: config.maxUsdPerRun,
|
|
236
|
+
maxIterations: config.maxIterations,
|
|
237
|
+
maxTokens: config.maxTokens,
|
|
238
|
+
},
|
|
239
|
+
circuitBreaker: {
|
|
240
|
+
repeatThreshold: config.repeatThreshold,
|
|
241
|
+
windowSeconds: config.windowSeconds,
|
|
242
|
+
stepCap: config.stepCap,
|
|
243
|
+
},
|
|
244
|
+
reviewerCascade: {
|
|
245
|
+
mode: config.reviewerMode,
|
|
246
|
+
destructivePatterns: config.destructivePatterns,
|
|
247
|
+
},
|
|
248
|
+
inputKeys: Object.keys(inputs).sort(),
|
|
249
|
+
keySource: 'env_or_keychain',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function persistDeployment(config: ByoOutcomeConfig, dir = path.join(process.cwd(), '.agentguard', 'deployments')): string {
|
|
254
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
255
|
+
const file = path.join(dir, `${config.slug}.json`);
|
|
256
|
+
fs.writeFileSync(file, JSON.stringify({ ...config, deployedAt: new Date().toISOString(), keySource: 'env_or_keychain' }, null, 2) + '\n', { mode: 0o600 });
|
|
257
|
+
return file;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findScalar(source: string, key: string): string | null {
|
|
261
|
+
const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:\\s*(.+?)\\s*$`, 'm');
|
|
262
|
+
const match = source.match(re);
|
|
263
|
+
return match ? cleanScalar(match[1]!) : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function findNestedScalar(source: string, parent: string, key: string): string | null {
|
|
267
|
+
const section = sectionLines(source, parent);
|
|
268
|
+
for (const line of section) {
|
|
269
|
+
const trimmed = line.trim();
|
|
270
|
+
const prefix = key + ':';
|
|
271
|
+
if (trimmed.startsWith(prefix)) return cleanScalar(trimmed.slice(prefix.length));
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findList(source: string, key: string): string[] {
|
|
277
|
+
const lines = source.split('\n');
|
|
278
|
+
const out: string[] = [];
|
|
279
|
+
let collecting = false;
|
|
280
|
+
for (const line of lines) {
|
|
281
|
+
const trimmed = line.trim();
|
|
282
|
+
if (!collecting && trimmed === key + ':') {
|
|
283
|
+
collecting = true;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!collecting) continue;
|
|
287
|
+
if (trimmed.startsWith('- ')) {
|
|
288
|
+
out.push(cleanScalar(trimmed.slice(2)));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (trimmed && !line.startsWith(' ')) break;
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sectionLines(source: string, parent: string): string[] {
|
|
297
|
+
const lines = source.split('\n');
|
|
298
|
+
const out: string[] = [];
|
|
299
|
+
let collecting = false;
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
if (!collecting && line.trim() === parent + ':') {
|
|
302
|
+
collecting = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!collecting) continue;
|
|
306
|
+
if (line.trim() && !line.startsWith(' ')) break;
|
|
307
|
+
out.push(line);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function cleanScalar(value: string): string {
|
|
313
|
+
const trimmed = value.trim();
|
|
314
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
315
|
+
return trimmed.slice(1, -1);
|
|
316
|
+
}
|
|
317
|
+
return trimmed;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function escapeRegExp(value: string): string {
|
|
321
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
322
|
+
}
|
package/src/cli/byo.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
isStarterTemplateName,
|
|
5
|
+
listStarterTemplates,
|
|
6
|
+
parseOutcomeYaml,
|
|
7
|
+
persistDeployment,
|
|
8
|
+
simulateOutcomeRun,
|
|
9
|
+
validateOutcomeYaml,
|
|
10
|
+
writeStarterTemplate,
|
|
11
|
+
} from '../byo';
|
|
12
|
+
import { green, yellow, dim } from './colors';
|
|
13
|
+
|
|
14
|
+
export async function runByoInit(argv: string[]): Promise<number> {
|
|
15
|
+
const template = argv[0];
|
|
16
|
+
const force = argv.includes('--force');
|
|
17
|
+
const output = valueAfter(argv, '--output') ?? 'outcome.yaml';
|
|
18
|
+
if (!isStarterTemplateName(template)) {
|
|
19
|
+
process.stderr.write(`agentguard init: unknown template '${template ?? ''}'\n`);
|
|
20
|
+
process.stderr.write('available templates: ' + listStarterTemplates().join(', ') + '\n');
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const file = writeStarterTemplate(template, path.resolve(output), force);
|
|
25
|
+
process.stdout.write(`${green('+ created')} ${path.relative(process.cwd(), file) || file}\n`);
|
|
26
|
+
process.stdout.write(`${dim('next:')} agentguard run ${template} --inputs '{"task_id":"demo","payload_ref":"local-file"}'\n`);
|
|
27
|
+
return 0;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
process.stderr.write(`agentguard init: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runOutcome(argv: string[]): Promise<number> {
|
|
35
|
+
const slug = argv[0];
|
|
36
|
+
if (!slug || argv.includes('--help') || argv.includes('-h')) {
|
|
37
|
+
process.stdout.write("agentguard run <slug> --inputs '{...}' [--config ./outcome.yaml]\n");
|
|
38
|
+
return slug ? 0 : 2;
|
|
39
|
+
}
|
|
40
|
+
const configPath = path.resolve(valueAfter(argv, '--config') ?? 'outcome.yaml');
|
|
41
|
+
const inputsRaw = valueAfter(argv, '--inputs') ?? '{}';
|
|
42
|
+
let inputs: Record<string, unknown>;
|
|
43
|
+
try {
|
|
44
|
+
inputs = JSON.parse(inputsRaw) as Record<string, unknown>;
|
|
45
|
+
} catch {
|
|
46
|
+
process.stderr.write('agentguard run: --inputs must be valid JSON\n');
|
|
47
|
+
return 2;
|
|
48
|
+
}
|
|
49
|
+
const loaded = loadConfig(configPath);
|
|
50
|
+
if (!loaded.ok) return loaded.code;
|
|
51
|
+
if (loaded.config.slug !== slug) {
|
|
52
|
+
process.stderr.write(`agentguard run: config slug is ${loaded.config.slug}, not ${slug}\n`);
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
process.stdout.write(JSON.stringify(simulateOutcomeRun(loaded.config, inputs), null, 2) + '\n');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runDeploy(argv: string[]): Promise<number> {
|
|
60
|
+
const configPath = path.resolve(valueAfter(argv, '--config') ?? 'outcome.yaml');
|
|
61
|
+
const loaded = loadConfig(configPath);
|
|
62
|
+
if (!loaded.ok) return loaded.code;
|
|
63
|
+
const file = persistDeployment(loaded.config);
|
|
64
|
+
process.stdout.write(`${green('+ deployed')} ${loaded.config.slug} ${dim('local config only')}\n`);
|
|
65
|
+
process.stdout.write(`${dim('receipt config:')} ${path.relative(process.cwd(), file) || file}\n`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadConfig(configPath: string): { ok: true; config: ReturnType<typeof parseOutcomeYaml> } | { ok: false; code: number } {
|
|
70
|
+
if (!fs.existsSync(configPath)) {
|
|
71
|
+
process.stderr.write(`agentguard: missing ${configPath}. Run agentguard init <template> first.\n`);
|
|
72
|
+
return { ok: false, code: 2 };
|
|
73
|
+
}
|
|
74
|
+
const source = fs.readFileSync(configPath, 'utf8');
|
|
75
|
+
const validated = validateOutcomeYaml(source);
|
|
76
|
+
if (!validated.ok || !validated.config) {
|
|
77
|
+
process.stderr.write(`${yellow('invalid outcome.yaml')}\n`);
|
|
78
|
+
for (const error of validated.errors) process.stderr.write(` ${error}\n`);
|
|
79
|
+
return { ok: false, code: 2 };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, config: validated.config };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function valueAfter(argv: string[], flag: string): string | undefined {
|
|
85
|
+
const index = argv.indexOf(flag);
|
|
86
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
87
|
+
}
|