@orchagent/cli 0.3.28 → 0.3.29
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/dist/commands/agents.js +87 -10
- package/dist/commands/billing.js +122 -0
- package/dist/commands/call.js +85 -1
- package/dist/commands/index.js +8 -0
- package/dist/commands/info.js +56 -3
- package/dist/commands/install.js +64 -5
- package/dist/commands/pricing.js +72 -0
- package/dist/commands/publish.js +71 -0
- package/dist/commands/run.js +50 -0
- package/dist/commands/search.js +15 -0
- package/dist/commands/security.js +344 -0
- package/dist/commands/seller.js +142 -0
- package/dist/commands/skill.js +83 -8
- package/dist/commands/whoami.js +8 -0
- package/dist/lib/api.js +41 -0
- package/dist/lib/errors.js +2 -0
- package/dist/lib/output.js +5 -1
- package/dist/lib/pricing.js +22 -0
- package/package.json +1 -1
package/dist/commands/publish.js
CHANGED
|
@@ -162,6 +162,8 @@ function registerPublishCommand(program) {
|
|
|
162
162
|
.option('--skills <skills>', 'Default skills (comma-separated, e.g., org/skill@v1,org/other@v1)')
|
|
163
163
|
.option('--skills-locked', 'Lock default skills (callers cannot override via headers)')
|
|
164
164
|
.option('--docker', 'Include Dockerfile for custom environment (builds E2B template)')
|
|
165
|
+
.option('--price <amount>', 'Set price per call in USD (e.g., 0.50 for $0.50/call)')
|
|
166
|
+
.option('--pricing-mode <mode>', 'Pricing mode: free or per_call (default: free)')
|
|
165
167
|
.action(async (options) => {
|
|
166
168
|
if (options.private) {
|
|
167
169
|
process.stderr.write('Warning: --private is deprecated (private is now the default). You can safely remove it.\n');
|
|
@@ -230,12 +232,47 @@ function registerPublishCommand(program) {
|
|
|
230
232
|
skill_files: hasMultipleFiles ? skillFiles : undefined,
|
|
231
233
|
});
|
|
232
234
|
const skillVersion = skillResult.agent?.version || 'v1';
|
|
235
|
+
const skillAgentId = skillResult.agent?.id;
|
|
236
|
+
// Handle pricing for skills
|
|
237
|
+
if (skillAgentId && (options.price || options.pricingMode)) {
|
|
238
|
+
let pricingMode = 'free';
|
|
239
|
+
let pricePerCallCents;
|
|
240
|
+
if (options.price) {
|
|
241
|
+
const priceFloat = parseFloat(options.price);
|
|
242
|
+
if (isNaN(priceFloat) || priceFloat < 0) {
|
|
243
|
+
throw new errors_1.CliError('Price must be a positive number', errors_1.ExitCodes.INVALID_INPUT);
|
|
244
|
+
}
|
|
245
|
+
if (priceFloat === 0) {
|
|
246
|
+
pricingMode = 'free';
|
|
247
|
+
}
|
|
248
|
+
else if (priceFloat < 0.01) {
|
|
249
|
+
throw new errors_1.CliError('Price must be at least $0.01 USD', errors_1.ExitCodes.INVALID_INPUT);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
pricingMode = 'per_call';
|
|
253
|
+
pricePerCallCents = Math.round(priceFloat * 100);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (options.pricingMode) {
|
|
257
|
+
pricingMode = options.pricingMode === 'per_call' ? 'per_call' : 'free';
|
|
258
|
+
}
|
|
259
|
+
// Set pricing
|
|
260
|
+
if (pricingMode === 'per_call' && !pricePerCallCents) {
|
|
261
|
+
throw new errors_1.CliError('--price required when using per_call mode', errors_1.ExitCodes.INVALID_INPUT);
|
|
262
|
+
}
|
|
263
|
+
await (0, api_1.setAgentPricing)(config, skillAgentId, pricingMode, pricePerCallCents);
|
|
264
|
+
}
|
|
233
265
|
await (0, analytics_1.track)('cli_publish', { agent_type: 'skill', multi_file: hasMultipleFiles });
|
|
234
266
|
process.stdout.write(`\nPublished skill: ${org.slug}/${skillData.frontmatter.name}@${skillVersion}\n`);
|
|
235
267
|
if (hasMultipleFiles) {
|
|
236
268
|
process.stdout.write(`Files: ${skillFiles.length} files included\n`);
|
|
237
269
|
}
|
|
238
270
|
process.stdout.write(`Public: ${options.public ? 'yes' : 'no'}\n`);
|
|
271
|
+
// Show pricing info
|
|
272
|
+
if (options.price && parseFloat(options.price) > 0) {
|
|
273
|
+
const price = parseFloat(options.price);
|
|
274
|
+
process.stdout.write(`Pricing: $${price.toFixed(2)} USD per call\n`);
|
|
275
|
+
}
|
|
239
276
|
}
|
|
240
277
|
catch (err) {
|
|
241
278
|
if (handleSecurityFlaggedError(err)) {
|
|
@@ -498,11 +535,45 @@ function registerPublishCommand(program) {
|
|
|
498
535
|
await promises_1.default.rm(tempDir, { recursive: true, force: true });
|
|
499
536
|
}
|
|
500
537
|
}
|
|
538
|
+
// Handle pricing for agents
|
|
539
|
+
if (agentId && (options.price || options.pricingMode)) {
|
|
540
|
+
let pricingMode = 'free';
|
|
541
|
+
let pricePerCallCents;
|
|
542
|
+
if (options.price) {
|
|
543
|
+
const priceFloat = parseFloat(options.price);
|
|
544
|
+
if (isNaN(priceFloat) || priceFloat < 0) {
|
|
545
|
+
throw new errors_1.CliError('Price must be a positive number', errors_1.ExitCodes.INVALID_INPUT);
|
|
546
|
+
}
|
|
547
|
+
if (priceFloat === 0) {
|
|
548
|
+
pricingMode = 'free';
|
|
549
|
+
}
|
|
550
|
+
else if (priceFloat < 0.01) {
|
|
551
|
+
throw new errors_1.CliError('Price must be at least $0.01 USD', errors_1.ExitCodes.INVALID_INPUT);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
pricingMode = 'per_call';
|
|
555
|
+
pricePerCallCents = Math.round(priceFloat * 100);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else if (options.pricingMode) {
|
|
559
|
+
pricingMode = options.pricingMode === 'per_call' ? 'per_call' : 'free';
|
|
560
|
+
}
|
|
561
|
+
// Set pricing
|
|
562
|
+
if (pricingMode === 'per_call' && !pricePerCallCents) {
|
|
563
|
+
throw new errors_1.CliError('--price required when using per_call mode', errors_1.ExitCodes.INVALID_INPUT);
|
|
564
|
+
}
|
|
565
|
+
await (0, api_1.setAgentPricing)(config, agentId, pricingMode, pricePerCallCents);
|
|
566
|
+
}
|
|
501
567
|
await (0, analytics_1.track)('cli_publish', { agent_type: manifest.type, hosted: shouldUploadBundle });
|
|
502
568
|
process.stdout.write(`\nPublished agent: ${org.slug}/${manifest.name}@${assignedVersion}\n`);
|
|
503
569
|
process.stdout.write(`Type: ${manifest.type}${shouldUploadBundle ? ' (hosted)' : ''}\n`);
|
|
504
570
|
process.stdout.write(`Providers: ${supportedProviders.join(', ')}\n`);
|
|
505
571
|
process.stdout.write(`Public: ${options.public ? 'yes' : 'no'}\n`);
|
|
572
|
+
// Show pricing info
|
|
573
|
+
if (options.price && parseFloat(options.price) > 0) {
|
|
574
|
+
const price = parseFloat(options.price);
|
|
575
|
+
process.stdout.write(`Pricing: $${price.toFixed(2)} USD per call\n`);
|
|
576
|
+
}
|
|
506
577
|
if (result.service_key) {
|
|
507
578
|
process.stdout.write(`\nService key (save this - shown only once):\n`);
|
|
508
579
|
process.stdout.write(` ${result.service_key}\n`);
|
package/dist/commands/run.js
CHANGED
|
@@ -74,6 +74,50 @@ async function downloadAgent(config, org, agent, version) {
|
|
|
74
74
|
return await (0, api_1.publicRequest)(config, `/public/agents/${org}/${agent}/${version}/download`);
|
|
75
75
|
}
|
|
76
76
|
catch (err) {
|
|
77
|
+
// Check for paid-agent error
|
|
78
|
+
if (err instanceof api_1.ApiError && err.status === 403) {
|
|
79
|
+
const payload = err.payload;
|
|
80
|
+
if (payload?.error?.code === 'PAID_AGENT_SERVER_ONLY') {
|
|
81
|
+
// Paid agent - try owner path if authenticated
|
|
82
|
+
if (config.apiKey) {
|
|
83
|
+
try {
|
|
84
|
+
const myAgents = await (0, api_1.listMyAgents)(config);
|
|
85
|
+
const matchingAgent = myAgents.find(a => a.name === agent && a.version === version && a.org_slug === org);
|
|
86
|
+
if (matchingAgent) {
|
|
87
|
+
// Owner! Fetch from authenticated endpoint
|
|
88
|
+
const agentData = await (0, api_1.request)(config, 'GET', `/agents/${matchingAgent.id}`);
|
|
89
|
+
// Convert Agent to AgentDownload format
|
|
90
|
+
return {
|
|
91
|
+
id: agentData.id,
|
|
92
|
+
type: agentData.type,
|
|
93
|
+
name: agentData.name,
|
|
94
|
+
version: agentData.version,
|
|
95
|
+
description: agentData.description,
|
|
96
|
+
prompt: agentData.prompt,
|
|
97
|
+
input_schema: agentData.input_schema,
|
|
98
|
+
output_schema: agentData.output_schema,
|
|
99
|
+
supported_providers: agentData.supported_providers || ['any'],
|
|
100
|
+
default_models: agentData.default_models,
|
|
101
|
+
source_url: agentData.source_url,
|
|
102
|
+
pip_package: agentData.pip_package,
|
|
103
|
+
run_command: agentData.run_command,
|
|
104
|
+
url: agentData.url,
|
|
105
|
+
has_bundle: !!agentData.code_bundle_url,
|
|
106
|
+
entrypoint: agentData.entrypoint,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Not owner or other error, fall through
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Non-owner - block with helpful message
|
|
115
|
+
const price = payload.error.price_per_call_cents || 0;
|
|
116
|
+
const priceStr = price ? `$${(price / 100).toFixed(2)}/call` : 'PAID';
|
|
117
|
+
throw new errors_1.CliError(`This agent is paid (${priceStr}) and runs on server only.\n\n` +
|
|
118
|
+
`Use: orch call ${org}/${agent}@${version} --input '{...}'`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
77
121
|
if (!(err instanceof api_1.ApiError) || err.status !== 404)
|
|
78
122
|
throw err;
|
|
79
123
|
}
|
|
@@ -788,6 +832,12 @@ Examples:
|
|
|
788
832
|
orch run orchagent/leak-finder --download-only
|
|
789
833
|
|
|
790
834
|
Note: Use 'run' for local execution, 'call' for server-side execution.
|
|
835
|
+
|
|
836
|
+
Paid Agents:
|
|
837
|
+
Paid agents run on server only for non-owners.
|
|
838
|
+
You CAN download and run your own paid agents for development/testing.
|
|
839
|
+
|
|
840
|
+
For other users' paid agents, use 'orch call' instead.
|
|
791
841
|
`)
|
|
792
842
|
.action(async (agentRef, args, options) => {
|
|
793
843
|
// Merge --data alias into --input
|
package/dist/commands/search.js
CHANGED
|
@@ -5,6 +5,7 @@ const config_1 = require("../lib/config");
|
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const output_1 = require("../lib/output");
|
|
7
7
|
const analytics_1 = require("../lib/analytics");
|
|
8
|
+
const pricing_1 = require("../lib/pricing");
|
|
8
9
|
const DEFAULT_LIMIT = 20;
|
|
9
10
|
function registerSearchCommand(program) {
|
|
10
11
|
program
|
|
@@ -15,7 +16,14 @@ function registerSearchCommand(program) {
|
|
|
15
16
|
.option('--recent', 'Show most recently published')
|
|
16
17
|
.option('--type <type>', 'Filter by type: agents, skills, all (default: all)', 'all')
|
|
17
18
|
.option('--limit <n>', `Max results (default: ${DEFAULT_LIMIT})`, String(DEFAULT_LIMIT))
|
|
19
|
+
.option('--free', 'Show only free agents')
|
|
20
|
+
.option('--paid', 'Show only paid agents')
|
|
18
21
|
.option('--json', 'Output raw JSON')
|
|
22
|
+
.addHelpText('after', `
|
|
23
|
+
Pricing Filters:
|
|
24
|
+
--free Show only free agents
|
|
25
|
+
--paid Show only paid agents
|
|
26
|
+
`)
|
|
19
27
|
.action(async (query, options) => {
|
|
20
28
|
const config = await (0, config_1.getResolvedConfig)();
|
|
21
29
|
const limit = parseInt(options.limit, 10) || DEFAULT_LIMIT;
|
|
@@ -39,6 +47,13 @@ function registerSearchCommand(program) {
|
|
|
39
47
|
else if (options.type === 'skills') {
|
|
40
48
|
agents = agents.filter(a => a.type === 'skill');
|
|
41
49
|
}
|
|
50
|
+
// Filter by pricing if requested
|
|
51
|
+
if (options.free) {
|
|
52
|
+
agents = agents.filter(a => !(0, pricing_1.isPaidAgent)(a));
|
|
53
|
+
}
|
|
54
|
+
if (options.paid) {
|
|
55
|
+
agents = agents.filter(a => (0, pricing_1.isPaidAgent)(a));
|
|
56
|
+
}
|
|
42
57
|
// Sort results
|
|
43
58
|
if (options.popular || (!query && !options.recent)) {
|
|
44
59
|
agents.sort((a, b) => (b.stars_count ?? 0) - (a.stars_count ?? 0));
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerSecurityCommand = registerSecurityCommand;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("../lib/config");
|
|
10
|
+
const api_1 = require("../lib/api");
|
|
11
|
+
const errors_1 = require("../lib/errors");
|
|
12
|
+
const output_1 = require("../lib/output");
|
|
13
|
+
const spinner_1 = require("../lib/spinner");
|
|
14
|
+
const llm_1 = require("../lib/llm");
|
|
15
|
+
const analytics_1 = require("../lib/analytics");
|
|
16
|
+
const DEFAULT_VERSION = 'latest';
|
|
17
|
+
function parseAgentRef(value) {
|
|
18
|
+
const [ref, versionPart] = value.split('@');
|
|
19
|
+
const version = versionPart?.trim() || DEFAULT_VERSION;
|
|
20
|
+
const segments = ref.split('/');
|
|
21
|
+
if (segments.length === 1) {
|
|
22
|
+
return { agent: segments[0], version };
|
|
23
|
+
}
|
|
24
|
+
if (segments.length === 2) {
|
|
25
|
+
return { org: segments[0], agent: segments[1], version };
|
|
26
|
+
}
|
|
27
|
+
if (segments.length === 3) {
|
|
28
|
+
return { org: segments[0], agent: segments[1], version: segments[2] };
|
|
29
|
+
}
|
|
30
|
+
throw new errors_1.CliError('Invalid agent reference. Use org/agent/version or org/agent@version format.');
|
|
31
|
+
}
|
|
32
|
+
// Severity color mapping
|
|
33
|
+
function severityColor(severity) {
|
|
34
|
+
switch (severity.toLowerCase()) {
|
|
35
|
+
case 'critical':
|
|
36
|
+
return chalk_1.default.red.bold(severity.toUpperCase());
|
|
37
|
+
case 'high':
|
|
38
|
+
return chalk_1.default.red(severity.toUpperCase());
|
|
39
|
+
case 'medium':
|
|
40
|
+
return chalk_1.default.yellow(severity.toUpperCase());
|
|
41
|
+
case 'low':
|
|
42
|
+
return chalk_1.default.blue(severity.toUpperCase());
|
|
43
|
+
default:
|
|
44
|
+
return severity;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Risk level color mapping
|
|
48
|
+
function riskLevelColor(level) {
|
|
49
|
+
switch (level.toLowerCase()) {
|
|
50
|
+
case 'critical':
|
|
51
|
+
return chalk_1.default.bgRed.white.bold(` ${level.toUpperCase()} `);
|
|
52
|
+
case 'high':
|
|
53
|
+
return chalk_1.default.bgRed.white(` ${level.toUpperCase()} `);
|
|
54
|
+
case 'medium':
|
|
55
|
+
return chalk_1.default.bgYellow.black(` ${level.toUpperCase()} `);
|
|
56
|
+
case 'low':
|
|
57
|
+
return chalk_1.default.bgBlue.white(` ${level.toUpperCase()} `);
|
|
58
|
+
case 'minimal':
|
|
59
|
+
return chalk_1.default.bgGreen.white(` ${level.toUpperCase()} `);
|
|
60
|
+
default:
|
|
61
|
+
return level;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function formatSummaryOutput(result) {
|
|
65
|
+
process.stdout.write('\n');
|
|
66
|
+
process.stdout.write(chalk_1.default.bold('Security Scan Results\n'));
|
|
67
|
+
process.stdout.write('━'.repeat(50) + '\n\n');
|
|
68
|
+
// Agent info
|
|
69
|
+
process.stdout.write(`${chalk_1.default.bold('Agent:')} ${result.agent_id}\n`);
|
|
70
|
+
process.stdout.write(`${chalk_1.default.bold('Scan Time:')} ${result.scan_timestamp}\n\n`);
|
|
71
|
+
// Risk level banner
|
|
72
|
+
process.stdout.write(`${chalk_1.default.bold('Risk Level:')} ${riskLevelColor(result.risk_level)}\n\n`);
|
|
73
|
+
// Summary stats
|
|
74
|
+
process.stdout.write(`${chalk_1.default.bold('Attacks Tested:')} ${result.total_attacks}\n`);
|
|
75
|
+
process.stdout.write(`${chalk_1.default.bold('Vulnerabilities Found:')} ${result.vulnerabilities_found}\n\n`);
|
|
76
|
+
// Breakdown by severity
|
|
77
|
+
if (Object.keys(result.summary.by_severity).length > 0) {
|
|
78
|
+
process.stdout.write(chalk_1.default.bold('By Severity:\n'));
|
|
79
|
+
const severityOrder = ['critical', 'high', 'medium', 'low'];
|
|
80
|
+
for (const sev of severityOrder) {
|
|
81
|
+
const count = result.summary.by_severity[sev];
|
|
82
|
+
if (count && count > 0) {
|
|
83
|
+
process.stdout.write(` ${severityColor(sev)}: ${count}\n`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
process.stdout.write('\n');
|
|
87
|
+
}
|
|
88
|
+
// Breakdown by category
|
|
89
|
+
if (Object.keys(result.summary.by_category).length > 0) {
|
|
90
|
+
process.stdout.write(chalk_1.default.bold('By Category:\n'));
|
|
91
|
+
for (const [cat, count] of Object.entries(result.summary.by_category)) {
|
|
92
|
+
if (count > 0) {
|
|
93
|
+
process.stdout.write(` ${cat}: ${count}\n`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write('\n');
|
|
97
|
+
}
|
|
98
|
+
// Top vulnerabilities (show first 5)
|
|
99
|
+
if (result.vulnerabilities.length > 0) {
|
|
100
|
+
process.stdout.write(chalk_1.default.bold('Top Issues:\n'));
|
|
101
|
+
const topVulns = result.vulnerabilities.slice(0, 5);
|
|
102
|
+
for (const vuln of topVulns) {
|
|
103
|
+
process.stdout.write(`\n ${severityColor(vuln.severity)} - ${chalk_1.default.bold(vuln.attack_name)}\n`);
|
|
104
|
+
process.stdout.write(` Category: ${vuln.category}\n`);
|
|
105
|
+
if (vuln.attack_description) {
|
|
106
|
+
const desc = vuln.attack_description.length > 100
|
|
107
|
+
? vuln.attack_description.slice(0, 97) + '...'
|
|
108
|
+
: vuln.attack_description;
|
|
109
|
+
process.stdout.write(` ${chalk_1.default.dim(desc)}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (result.vulnerabilities.length > 5) {
|
|
113
|
+
process.stdout.write(`\n ${chalk_1.default.dim(`... and ${result.vulnerabilities.length - 5} more`)}\n`);
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write('\n');
|
|
116
|
+
}
|
|
117
|
+
// Suggestion
|
|
118
|
+
if (result.vulnerabilities_found > 0) {
|
|
119
|
+
process.stdout.write(chalk_1.default.yellow('Tip: Use --output markdown for a detailed report.\n'));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
process.stdout.write(chalk_1.default.green('No vulnerabilities detected. Your agent appears secure.\n'));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function registerSecurityCommand(program) {
|
|
126
|
+
const security = program
|
|
127
|
+
.command('security')
|
|
128
|
+
.description('Security scanning and vulnerability testing for agents');
|
|
129
|
+
security
|
|
130
|
+
.command('test <agent>')
|
|
131
|
+
.description('Run dynamic security test against an agent')
|
|
132
|
+
.option('--categories <cats...>', 'Filter by attack categories')
|
|
133
|
+
.option('--severities <sevs...>', 'Filter by severities (critical, high, medium, low)')
|
|
134
|
+
.option('--max-attacks <n>', 'Limit number of attacks', parseInt)
|
|
135
|
+
.option('--output <format>', 'Output format: json, markdown, summary', 'summary')
|
|
136
|
+
.option('--output-file <path>', 'Write report to file')
|
|
137
|
+
.option('--key <key>', 'LLM API key (overrides env vars)')
|
|
138
|
+
.option('--provider <provider>', 'LLM provider (openai, anthropic, gemini)')
|
|
139
|
+
.addHelpText('after', `
|
|
140
|
+
Examples:
|
|
141
|
+
orch security test my-org/my-agent/1.0.0
|
|
142
|
+
orch security test my-org/my-agent@latest --categories persona_roleplay logic_trap
|
|
143
|
+
orch security test my-org/my-agent/1.0.0 --severities critical high
|
|
144
|
+
orch security test my-org/my-agent/1.0.0 --output markdown --output-file report.md
|
|
145
|
+
orch security test my-org/my-agent/1.0.0 --max-attacks 10 --output json
|
|
146
|
+
`)
|
|
147
|
+
.action(async (agentRef, options) => {
|
|
148
|
+
const resolved = await (0, config_1.getResolvedConfig)();
|
|
149
|
+
if (!resolved.apiKey) {
|
|
150
|
+
throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
|
|
151
|
+
}
|
|
152
|
+
const parsed = parseAgentRef(agentRef);
|
|
153
|
+
const configFile = await (0, config_1.loadConfig)();
|
|
154
|
+
const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
|
|
155
|
+
if (!org) {
|
|
156
|
+
throw new errors_1.CliError('Missing org. Use org/agent or set default org.');
|
|
157
|
+
}
|
|
158
|
+
const agentId = `${org}/${parsed.agent}/${parsed.version}`;
|
|
159
|
+
// Detect LLM key for the scan
|
|
160
|
+
let llmKey;
|
|
161
|
+
let llmProvider;
|
|
162
|
+
if (options.key) {
|
|
163
|
+
if (!options.provider) {
|
|
164
|
+
throw new errors_1.CliError('When using --key, you must also specify --provider (openai, anthropic, or gemini)');
|
|
165
|
+
}
|
|
166
|
+
(0, llm_1.validateProvider)(options.provider);
|
|
167
|
+
llmKey = options.key;
|
|
168
|
+
llmProvider = options.provider;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const detected = await (0, llm_1.detectLlmKey)(['any'], resolved);
|
|
172
|
+
if (detected) {
|
|
173
|
+
llmKey = detected.key;
|
|
174
|
+
llmProvider = detected.provider;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Build request body
|
|
178
|
+
const requestBody = {
|
|
179
|
+
agent_id: agentId,
|
|
180
|
+
};
|
|
181
|
+
if (options.categories && options.categories.length > 0) {
|
|
182
|
+
requestBody.categories = options.categories;
|
|
183
|
+
}
|
|
184
|
+
if (options.severities && options.severities.length > 0) {
|
|
185
|
+
requestBody.severities = options.severities;
|
|
186
|
+
}
|
|
187
|
+
if (options.maxAttacks) {
|
|
188
|
+
requestBody.max_attacks = options.maxAttacks;
|
|
189
|
+
}
|
|
190
|
+
const url = `${resolved.apiUrl.replace(/\/$/, '')}/security/test`;
|
|
191
|
+
// Make the API call with a spinner
|
|
192
|
+
const spinner = (0, spinner_1.createSpinner)(`Scanning ${agentId} for vulnerabilities...`);
|
|
193
|
+
spinner.start();
|
|
194
|
+
// Build headers - LLM key goes in X-LLM-API-Key header
|
|
195
|
+
const headers = {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
Authorization: `Bearer ${resolved.apiKey}`,
|
|
198
|
+
};
|
|
199
|
+
if (llmKey) {
|
|
200
|
+
headers['X-LLM-API-Key'] = llmKey;
|
|
201
|
+
}
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
|
|
205
|
+
method: 'POST',
|
|
206
|
+
headers,
|
|
207
|
+
body: JSON.stringify(requestBody),
|
|
208
|
+
timeoutMs: 300000, // 5 minutes - scans can take time
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
spinner.fail(`Scan failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const text = await response.text();
|
|
217
|
+
let payload;
|
|
218
|
+
try {
|
|
219
|
+
payload = JSON.parse(text);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
payload = text;
|
|
223
|
+
}
|
|
224
|
+
const message = typeof payload === 'object' && payload
|
|
225
|
+
? payload.error?.message ||
|
|
226
|
+
payload.message ||
|
|
227
|
+
response.statusText
|
|
228
|
+
: response.statusText;
|
|
229
|
+
spinner.fail(`Scan failed: ${message}`);
|
|
230
|
+
throw new errors_1.CliError(message);
|
|
231
|
+
}
|
|
232
|
+
spinner.succeed(`Scan completed for ${agentId}`);
|
|
233
|
+
const result = await response.json();
|
|
234
|
+
// Track successful scan
|
|
235
|
+
await (0, analytics_1.track)('cli_security_scan', {
|
|
236
|
+
agent: agentId,
|
|
237
|
+
vulnerabilities_found: result.vulnerabilities_found,
|
|
238
|
+
risk_level: result.risk_level,
|
|
239
|
+
});
|
|
240
|
+
// Handle output
|
|
241
|
+
const outputFormat = options.output || 'summary';
|
|
242
|
+
if (options.outputFile) {
|
|
243
|
+
let content;
|
|
244
|
+
if (outputFormat === 'json') {
|
|
245
|
+
content = JSON.stringify(result, null, 2);
|
|
246
|
+
}
|
|
247
|
+
else if (outputFormat === 'markdown' && result.markdown_report) {
|
|
248
|
+
content = result.markdown_report;
|
|
249
|
+
}
|
|
250
|
+
else if (outputFormat === 'markdown') {
|
|
251
|
+
// Generate basic markdown if server didn't provide one
|
|
252
|
+
content = generateMarkdownReport(result);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// For summary output to file, use markdown
|
|
256
|
+
content = generateMarkdownReport(result);
|
|
257
|
+
}
|
|
258
|
+
await promises_1.default.writeFile(options.outputFile, content, 'utf8');
|
|
259
|
+
process.stdout.write(`Report saved to ${options.outputFile}\n`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
// Print to stdout based on format
|
|
263
|
+
if (outputFormat === 'json') {
|
|
264
|
+
(0, output_1.printJson)(result);
|
|
265
|
+
}
|
|
266
|
+
else if (outputFormat === 'markdown') {
|
|
267
|
+
if (result.markdown_report) {
|
|
268
|
+
process.stdout.write(result.markdown_report);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
process.stdout.write(generateMarkdownReport(result));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// summary format
|
|
276
|
+
formatSummaryOutput(result);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function generateMarkdownReport(result) {
|
|
281
|
+
const lines = [];
|
|
282
|
+
lines.push('# Security Scan Report');
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push(`**Agent:** ${result.agent_id}`);
|
|
285
|
+
lines.push(`**Scan Time:** ${result.scan_timestamp}`);
|
|
286
|
+
lines.push(`**Risk Level:** ${result.risk_level.toUpperCase()}`);
|
|
287
|
+
lines.push('');
|
|
288
|
+
lines.push('## Summary');
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push(`- Total Attacks Tested: ${result.total_attacks}`);
|
|
291
|
+
lines.push(`- Vulnerabilities Found: ${result.vulnerabilities_found}`);
|
|
292
|
+
lines.push('');
|
|
293
|
+
if (Object.keys(result.summary.by_severity).length > 0) {
|
|
294
|
+
lines.push('### By Severity');
|
|
295
|
+
lines.push('');
|
|
296
|
+
for (const [sev, count] of Object.entries(result.summary.by_severity)) {
|
|
297
|
+
if (count > 0) {
|
|
298
|
+
lines.push(`- ${sev.toUpperCase()}: ${count}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
lines.push('');
|
|
302
|
+
}
|
|
303
|
+
if (Object.keys(result.summary.by_category).length > 0) {
|
|
304
|
+
lines.push('### By Category');
|
|
305
|
+
lines.push('');
|
|
306
|
+
for (const [cat, count] of Object.entries(result.summary.by_category)) {
|
|
307
|
+
if (count > 0) {
|
|
308
|
+
lines.push(`- ${cat}: ${count}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
lines.push('');
|
|
312
|
+
}
|
|
313
|
+
if (result.vulnerabilities.length > 0) {
|
|
314
|
+
lines.push('## Vulnerabilities');
|
|
315
|
+
lines.push('');
|
|
316
|
+
for (const vuln of result.vulnerabilities) {
|
|
317
|
+
lines.push(`### ${vuln.attack_name}`);
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push(`- **Severity:** ${vuln.severity.toUpperCase()}`);
|
|
320
|
+
lines.push(`- **Category:** ${vuln.category}`);
|
|
321
|
+
lines.push(`- **Attack ID:** ${vuln.attack_id}`);
|
|
322
|
+
lines.push('');
|
|
323
|
+
if (vuln.attack_description) {
|
|
324
|
+
lines.push(vuln.attack_description);
|
|
325
|
+
lines.push('');
|
|
326
|
+
}
|
|
327
|
+
if (vuln.leaked_content) {
|
|
328
|
+
lines.push('**Leaked Content:**');
|
|
329
|
+
lines.push('```');
|
|
330
|
+
lines.push(vuln.leaked_content);
|
|
331
|
+
lines.push('```');
|
|
332
|
+
lines.push('');
|
|
333
|
+
}
|
|
334
|
+
if (vuln.response_snippet) {
|
|
335
|
+
lines.push('**Response Snippet:**');
|
|
336
|
+
lines.push('```');
|
|
337
|
+
lines.push(vuln.response_snippet);
|
|
338
|
+
lines.push('```');
|
|
339
|
+
lines.push('');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return lines.join('\n');
|
|
344
|
+
}
|