agent-security-scanner-mcp 3.20.1 → 4.0.1
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/README.md +102 -12
- package/code-review-agent/.env.example +8 -0
- package/code-review-agent/README.md +142 -0
- package/code-review-agent/TODO.md +149 -0
- package/code-review-agent/bin/cr-agent.ts +313 -0
- package/code-review-agent/dist/bin/cr-agent.d.ts +3 -0
- package/code-review-agent/dist/bin/cr-agent.d.ts.map +1 -0
- package/code-review-agent/dist/bin/cr-agent.js +299 -0
- package/code-review-agent/dist/bin/cr-agent.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts +16 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/engine.js +298 -0
- package/code-review-agent/dist/src/analyzer/engine.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/intent.d.ts +10 -0
- package/code-review-agent/dist/src/analyzer/intent.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/intent.js +40 -0
- package/code-review-agent/dist/src/analyzer/intent.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts +19 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.js +150 -0
- package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -0
- package/code-review-agent/dist/src/context/assembler.d.ts +16 -0
- package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/assembler.js +135 -0
- package/code-review-agent/dist/src/context/assembler.js.map +1 -0
- package/code-review-agent/dist/src/context/file.d.ts +6 -0
- package/code-review-agent/dist/src/context/file.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/file.js +139 -0
- package/code-review-agent/dist/src/context/file.js.map +1 -0
- package/code-review-agent/dist/src/context/project.d.ts +4 -0
- package/code-review-agent/dist/src/context/project.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/project.js +252 -0
- package/code-review-agent/dist/src/context/project.js.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts +11 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.js +102 -0
- package/code-review-agent/dist/src/graph/dependency.js.map +1 -0
- package/code-review-agent/dist/src/graph/resolver.d.ts +9 -0
- package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -0
- package/code-review-agent/dist/src/graph/resolver.js +124 -0
- package/code-review-agent/dist/src/graph/resolver.js.map +1 -0
- package/code-review-agent/dist/src/index.d.ts +21 -0
- package/code-review-agent/dist/src/index.d.ts.map +1 -0
- package/code-review-agent/dist/src/index.js +21 -0
- package/code-review-agent/dist/src/index.js.map +1 -0
- package/code-review-agent/dist/src/llm/anthropic.d.ts +13 -0
- package/code-review-agent/dist/src/llm/anthropic.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/anthropic.js +83 -0
- package/code-review-agent/dist/src/llm/anthropic.js.map +1 -0
- package/code-review-agent/dist/src/llm/claude-cli.d.ts +13 -0
- package/code-review-agent/dist/src/llm/claude-cli.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/claude-cli.js +141 -0
- package/code-review-agent/dist/src/llm/claude-cli.js.map +1 -0
- package/code-review-agent/dist/src/llm/openai.d.ts +13 -0
- package/code-review-agent/dist/src/llm/openai.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/openai.js +78 -0
- package/code-review-agent/dist/src/llm/openai.js.map +1 -0
- package/code-review-agent/dist/src/llm/provider.d.ts +18 -0
- package/code-review-agent/dist/src/llm/provider.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/provider.js +11 -0
- package/code-review-agent/dist/src/llm/provider.js.map +1 -0
- package/code-review-agent/dist/src/llm/router.d.ts +14 -0
- package/code-review-agent/dist/src/llm/router.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/router.js +67 -0
- package/code-review-agent/dist/src/llm/router.js.map +1 -0
- package/code-review-agent/dist/src/llm/schemas.d.ts +18 -0
- package/code-review-agent/dist/src/llm/schemas.d.ts.map +1 -0
- package/code-review-agent/dist/src/llm/schemas.js +91 -0
- package/code-review-agent/dist/src/llm/schemas.js.map +1 -0
- package/code-review-agent/dist/src/types/analysis.d.ts +56 -0
- package/code-review-agent/dist/src/types/analysis.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/analysis.js +2 -0
- package/code-review-agent/dist/src/types/analysis.js.map +1 -0
- package/code-review-agent/dist/src/types/config.d.ts +24 -0
- package/code-review-agent/dist/src/types/config.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/config.js +42 -0
- package/code-review-agent/dist/src/types/config.js.map +1 -0
- package/code-review-agent/dist/src/types/findings.d.ts +236 -0
- package/code-review-agent/dist/src/types/findings.d.ts.map +1 -0
- package/code-review-agent/dist/src/types/findings.js +64 -0
- package/code-review-agent/dist/src/types/findings.js.map +1 -0
- package/code-review-agent/package.json +36 -0
- package/code-review-agent/src/analyzer/engine.ts +374 -0
- package/code-review-agent/src/analyzer/intent.ts +49 -0
- package/code-review-agent/src/analyzer/semantic.ts +222 -0
- package/code-review-agent/src/context/assembler.ts +165 -0
- package/code-review-agent/src/context/file.ts +145 -0
- package/code-review-agent/src/context/project.ts +253 -0
- package/code-review-agent/src/graph/dependency.ts +116 -0
- package/code-review-agent/src/graph/resolver.ts +138 -0
- package/code-review-agent/src/index.ts +58 -0
- package/code-review-agent/src/llm/anthropic.ts +106 -0
- package/code-review-agent/src/llm/claude-cli.ts +187 -0
- package/code-review-agent/src/llm/openai.ts +95 -0
- package/code-review-agent/src/llm/provider.ts +33 -0
- package/code-review-agent/src/llm/router.ts +86 -0
- package/code-review-agent/src/llm/schemas.ts +125 -0
- package/code-review-agent/src/types/analysis.ts +62 -0
- package/code-review-agent/src/types/config.ts +72 -0
- package/code-review-agent/src/types/findings.ts +81 -0
- package/code-review-agent/tests/analyzer/engine.test.ts +194 -0
- package/code-review-agent/tests/analyzer/intent.test.ts +76 -0
- package/code-review-agent/tests/analyzer/semantic.test.ts +131 -0
- package/code-review-agent/tests/context/file.test.ts +21 -0
- package/code-review-agent/tests/context/project.test.ts +20 -0
- package/code-review-agent/tests/fixtures/safe-build-tool/README.md +19 -0
- package/code-review-agent/tests/fixtures/safe-build-tool/builder.js +52 -0
- package/code-review-agent/tests/fixtures/safe-file-manager/README.md +16 -0
- package/code-review-agent/tests/fixtures/safe-file-manager/organizer.py +70 -0
- package/code-review-agent/tests/fixtures/vuln-api-server/README.md +17 -0
- package/code-review-agent/tests/fixtures/vuln-api-server/server.js +52 -0
- package/code-review-agent/tests/fixtures/vuln-ecommerce/README.md +18 -0
- package/code-review-agent/tests/fixtures/vuln-ecommerce/checkout.js +63 -0
- package/code-review-agent/tests/graph/dependency.test.ts +136 -0
- package/code-review-agent/tests/helpers/mock-provider.ts +48 -0
- package/code-review-agent/tests/llm/claude-cli.test.ts +251 -0
- package/code-review-agent/tests/llm/router.test.ts +77 -0
- package/code-review-agent/tests/llm/schemas.test.ts +142 -0
- package/code-review-agent/tsconfig.json +20 -0
- package/code-review-agent/vitest.config.ts +11 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +15 -4
- package/scripts/postinstall.js +43 -4
- package/server.json +2 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { SchemaValidationError } from '../../src/llm/provider.js';
|
|
4
|
+
|
|
5
|
+
// Mock child_process.spawn before importing the provider
|
|
6
|
+
const mockStdin = { write: vi.fn(), end: vi.fn(), on: vi.fn() };
|
|
7
|
+
const mockStdout = { on: vi.fn() };
|
|
8
|
+
const mockStderr = { on: vi.fn() };
|
|
9
|
+
const mockChild = {
|
|
10
|
+
stdin: mockStdin,
|
|
11
|
+
stdout: mockStdout,
|
|
12
|
+
stderr: mockStderr,
|
|
13
|
+
on: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
vi.mock('node:child_process', () => ({
|
|
17
|
+
spawn: vi.fn(() => mockChild),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { ClaudeCliProvider } from '../../src/llm/claude-cli.js';
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
|
|
23
|
+
function simulateClaudeResponse(result: string, isError = false) {
|
|
24
|
+
const jsonOutput = JSON.stringify({
|
|
25
|
+
type: 'result',
|
|
26
|
+
subtype: 'success',
|
|
27
|
+
is_error: isError,
|
|
28
|
+
result,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// When stdout.on('data', cb) is called, capture the callback
|
|
32
|
+
mockStdout.on.mockImplementation((event: string, cb: (data: Buffer) => void) => {
|
|
33
|
+
if (event === 'data') {
|
|
34
|
+
cb(Buffer.from(jsonOutput));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
mockStderr.on.mockImplementation(() => {});
|
|
39
|
+
|
|
40
|
+
// When child.on('close', cb) is called, trigger immediately
|
|
41
|
+
mockChild.on.mockImplementation((event: string, cb: (code: number) => void) => {
|
|
42
|
+
if (event === 'close') {
|
|
43
|
+
cb(0);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function simulateClaudeError(code: number, stderrMsg = '') {
|
|
49
|
+
mockStdout.on.mockImplementation(() => {});
|
|
50
|
+
mockStderr.on.mockImplementation((event: string, cb: (data: Buffer) => void) => {
|
|
51
|
+
if (event === 'data' && stderrMsg) {
|
|
52
|
+
cb(Buffer.from(stderrMsg));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
mockChild.on.mockImplementation((event: string, cb: (code: number) => void) => {
|
|
56
|
+
if (event === 'close') {
|
|
57
|
+
cb(code);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function simulateSpawnError(message: string) {
|
|
63
|
+
mockStdout.on.mockImplementation(() => {});
|
|
64
|
+
mockStderr.on.mockImplementation(() => {});
|
|
65
|
+
mockChild.on.mockImplementation((event: string, cb: unknown) => {
|
|
66
|
+
if (event === 'error') {
|
|
67
|
+
(cb as (err: Error) => void)(new Error(message));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('ClaudeCliProvider', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('constructor', () => {
|
|
78
|
+
it('defaults to sonnet model', () => {
|
|
79
|
+
const provider = new ClaudeCliProvider();
|
|
80
|
+
expect(provider.modelId).toBe('sonnet');
|
|
81
|
+
expect(provider.providerName).toBe('claude-cli');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('accepts custom model', () => {
|
|
85
|
+
const provider = new ClaudeCliProvider('haiku');
|
|
86
|
+
expect(provider.modelId).toBe('haiku');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('chat', () => {
|
|
91
|
+
it('sends prompt via stdin and returns result', async () => {
|
|
92
|
+
simulateClaudeResponse('Hello world');
|
|
93
|
+
const provider = new ClaudeCliProvider();
|
|
94
|
+
const result = await provider.chat([
|
|
95
|
+
{ role: 'user', content: 'Say hello' },
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(result).toBe('Hello world');
|
|
99
|
+
expect(spawn).toHaveBeenCalledWith('claude', [
|
|
100
|
+
'-p', '-',
|
|
101
|
+
'--output-format', 'json',
|
|
102
|
+
'--model', 'sonnet',
|
|
103
|
+
'--no-session-persistence',
|
|
104
|
+
], expect.any(Object));
|
|
105
|
+
expect(mockStdin.write).toHaveBeenCalled();
|
|
106
|
+
expect(mockStdin.end).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('formats system messages correctly', async () => {
|
|
110
|
+
simulateClaudeResponse('ok');
|
|
111
|
+
const provider = new ClaudeCliProvider();
|
|
112
|
+
await provider.chat([
|
|
113
|
+
{ role: 'system', content: 'You are a helper' },
|
|
114
|
+
{ role: 'user', content: 'Help me' },
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const writtenPrompt = mockStdin.write.mock.calls[0][0] as string;
|
|
118
|
+
expect(writtenPrompt).toContain('[System Instructions]');
|
|
119
|
+
expect(writtenPrompt).toContain('You are a helper');
|
|
120
|
+
expect(writtenPrompt).toContain('Help me');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('formats assistant messages correctly', async () => {
|
|
124
|
+
simulateClaudeResponse('final answer');
|
|
125
|
+
const provider = new ClaudeCliProvider();
|
|
126
|
+
await provider.chat([
|
|
127
|
+
{ role: 'user', content: 'Question 1' },
|
|
128
|
+
{ role: 'assistant', content: 'Answer 1' },
|
|
129
|
+
{ role: 'user', content: 'Question 2' },
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const writtenPrompt = mockStdin.write.mock.calls[0][0] as string;
|
|
133
|
+
expect(writtenPrompt).toContain('[Previous response]');
|
|
134
|
+
expect(writtenPrompt).toContain('Answer 1');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('handles large prompts via stdin', async () => {
|
|
138
|
+
const largeContent = 'x'.repeat(100_000);
|
|
139
|
+
simulateClaudeResponse('analyzed');
|
|
140
|
+
const provider = new ClaudeCliProvider();
|
|
141
|
+
const result = await provider.chat([
|
|
142
|
+
{ role: 'user', content: largeContent },
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
expect(result).toBe('analyzed');
|
|
146
|
+
const writtenPrompt = mockStdin.write.mock.calls[0][0] as string;
|
|
147
|
+
expect(writtenPrompt.length).toBeGreaterThanOrEqual(100_000);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('rejects on non-zero exit code', async () => {
|
|
151
|
+
simulateClaudeError(1, 'something went wrong');
|
|
152
|
+
const provider = new ClaudeCliProvider();
|
|
153
|
+
|
|
154
|
+
await expect(provider.chat([
|
|
155
|
+
{ role: 'user', content: 'test' },
|
|
156
|
+
])).rejects.toThrow('claude CLI exited with code 1');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('rejects on spawn error', async () => {
|
|
160
|
+
simulateSpawnError('command not found');
|
|
161
|
+
const provider = new ClaudeCliProvider();
|
|
162
|
+
|
|
163
|
+
await expect(provider.chat([
|
|
164
|
+
{ role: 'user', content: 'test' },
|
|
165
|
+
])).rejects.toThrow('claude CLI failed to start');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('rejects when CLI returns is_error', async () => {
|
|
169
|
+
simulateClaudeResponse('API rate limit exceeded', true);
|
|
170
|
+
const provider = new ClaudeCliProvider();
|
|
171
|
+
|
|
172
|
+
await expect(provider.chat([
|
|
173
|
+
{ role: 'user', content: 'test' },
|
|
174
|
+
])).rejects.toThrow('claude CLI error');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('chatStructured', () => {
|
|
179
|
+
const TestSchema = z.object({
|
|
180
|
+
name: z.string(),
|
|
181
|
+
value: z.number(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('parses valid JSON response', async () => {
|
|
185
|
+
simulateClaudeResponse('{"name": "test", "value": 42}');
|
|
186
|
+
const provider = new ClaudeCliProvider();
|
|
187
|
+
const result = await provider.chatStructured(
|
|
188
|
+
[{ role: 'user', content: 'Give me data' }],
|
|
189
|
+
TestSchema,
|
|
190
|
+
'test_schema',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result).toEqual({ name: 'test', value: 42 });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('extracts JSON from markdown code blocks', async () => {
|
|
197
|
+
simulateClaudeResponse('```json\n{"name": "test", "value": 42}\n```');
|
|
198
|
+
const provider = new ClaudeCliProvider();
|
|
199
|
+
const result = await provider.chatStructured(
|
|
200
|
+
[{ role: 'user', content: 'Give me data' }],
|
|
201
|
+
TestSchema,
|
|
202
|
+
'test_schema',
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result).toEqual({ name: 'test', value: 42 });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('extracts JSON from surrounding text', async () => {
|
|
209
|
+
simulateClaudeResponse('Here is the result: {"name": "test", "value": 42} done.');
|
|
210
|
+
const provider = new ClaudeCliProvider();
|
|
211
|
+
const result = await provider.chatStructured(
|
|
212
|
+
[{ role: 'user', content: 'Give me data' }],
|
|
213
|
+
TestSchema,
|
|
214
|
+
'test_schema',
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(result).toEqual({ name: 'test', value: 42 });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('throws SchemaValidationError after max retries with invalid JSON', async () => {
|
|
221
|
+
simulateClaudeResponse('not json at all');
|
|
222
|
+
const provider = new ClaudeCliProvider();
|
|
223
|
+
|
|
224
|
+
await expect(provider.chatStructured(
|
|
225
|
+
[{ role: 'user', content: 'Give me data' }],
|
|
226
|
+
TestSchema,
|
|
227
|
+
'test_schema',
|
|
228
|
+
)).rejects.toThrow(SchemaValidationError);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('throws SchemaValidationError after max retries with wrong schema', async () => {
|
|
232
|
+
simulateClaudeResponse('{"wrong": "shape"}');
|
|
233
|
+
const provider = new ClaudeCliProvider();
|
|
234
|
+
|
|
235
|
+
await expect(provider.chatStructured(
|
|
236
|
+
[{ role: 'user', content: 'Give me data' }],
|
|
237
|
+
TestSchema,
|
|
238
|
+
'test_schema',
|
|
239
|
+
)).rejects.toThrow(SchemaValidationError);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('countTokens', () => {
|
|
244
|
+
it('approximates token count from text length', () => {
|
|
245
|
+
const provider = new ClaudeCliProvider();
|
|
246
|
+
expect(provider.countTokens('hello world')).toBe(3); // ceil(11/4)
|
|
247
|
+
expect(provider.countTokens('')).toBe(0);
|
|
248
|
+
expect(provider.countTokens('a'.repeat(100))).toBe(25);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ModelRouter } from '../../src/llm/router.js';
|
|
3
|
+
import type { AnalysisOptions } from '../../src/types/config.js';
|
|
4
|
+
|
|
5
|
+
function makeOptions(overrides: Partial<AnalysisOptions> = {}): AnalysisOptions {
|
|
6
|
+
return {
|
|
7
|
+
provider: 'anthropic',
|
|
8
|
+
confidenceThreshold: 0.7,
|
|
9
|
+
format: 'text',
|
|
10
|
+
verbose: false,
|
|
11
|
+
projectRoot: process.cwd(),
|
|
12
|
+
exclude: [],
|
|
13
|
+
concurrencyLimit: 5,
|
|
14
|
+
maxFileSize: 512 * 1024,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ModelRouter', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.stubEnv('ANTHROPIC_API_KEY', 'test-key');
|
|
22
|
+
vi.stubEnv('OPENAI_API_KEY', 'test-key');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('creates anthropic triage provider with haiku model', () => {
|
|
26
|
+
const router = new ModelRouter(makeOptions({ provider: 'anthropic' }));
|
|
27
|
+
const provider = router.getTriageProvider();
|
|
28
|
+
expect(provider.providerName).toBe('anthropic');
|
|
29
|
+
expect(provider.modelId).toBe('claude-haiku-4-5-20251001');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('creates anthropic analysis provider with sonnet model', () => {
|
|
33
|
+
const router = new ModelRouter(makeOptions({ provider: 'anthropic' }));
|
|
34
|
+
const provider = router.getAnalysisProvider();
|
|
35
|
+
expect(provider.providerName).toBe('anthropic');
|
|
36
|
+
expect(provider.modelId).toBe('claude-sonnet-4-20250514');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('creates openai triage provider with gpt-4o-mini', () => {
|
|
40
|
+
const router = new ModelRouter(makeOptions({ provider: 'openai' }));
|
|
41
|
+
const provider = router.getTriageProvider();
|
|
42
|
+
expect(provider.providerName).toBe('openai');
|
|
43
|
+
expect(provider.modelId).toBe('gpt-4o-mini');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('creates openai analysis provider with gpt-4o', () => {
|
|
47
|
+
const router = new ModelRouter(makeOptions({ provider: 'openai' }));
|
|
48
|
+
const provider = router.getAnalysisProvider();
|
|
49
|
+
expect(provider.providerName).toBe('openai');
|
|
50
|
+
expect(provider.modelId).toBe('gpt-4o');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses custom model override', () => {
|
|
54
|
+
const router = new ModelRouter(makeOptions({ provider: 'anthropic', model: 'claude-opus-4-20250514' }));
|
|
55
|
+
const provider = router.getAnalysisProvider();
|
|
56
|
+
expect(provider.modelId).toBe('claude-opus-4-20250514');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('caches providers', () => {
|
|
60
|
+
const router = new ModelRouter(makeOptions());
|
|
61
|
+
const a = router.getTriageProvider();
|
|
62
|
+
const b = router.getTriageProvider();
|
|
63
|
+
expect(a).toBe(b);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('estimates cost', () => {
|
|
67
|
+
const router = new ModelRouter(makeOptions());
|
|
68
|
+
const cost = router.estimateCost(1_000_000);
|
|
69
|
+
expect(cost).toBeGreaterThan(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('throws on missing API key', () => {
|
|
73
|
+
vi.stubEnv('ANTHROPIC_API_KEY', '');
|
|
74
|
+
const router = new ModelRouter(makeOptions({ provider: 'anthropic' }));
|
|
75
|
+
expect(() => router.getAnalysisProvider()).toThrow('Missing API key');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
zodToJsonSchema,
|
|
5
|
+
zodToAnthropicTool,
|
|
6
|
+
zodToOpenAIResponseFormat,
|
|
7
|
+
} from '../../src/llm/schemas.js';
|
|
8
|
+
|
|
9
|
+
describe('zodToJsonSchema', () => {
|
|
10
|
+
it('converts string schema', () => {
|
|
11
|
+
const result = zodToJsonSchema(z.string());
|
|
12
|
+
expect(result).toEqual({ type: 'string' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('converts number schema with min/max', () => {
|
|
16
|
+
const result = zodToJsonSchema(z.number().min(0).max(1));
|
|
17
|
+
expect(result).toEqual({ type: 'number', minimum: 0, maximum: 1 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('converts boolean schema', () => {
|
|
21
|
+
const result = zodToJsonSchema(z.boolean());
|
|
22
|
+
expect(result).toEqual({ type: 'boolean' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('converts enum schema', () => {
|
|
26
|
+
const result = zodToJsonSchema(z.enum(['a', 'b', 'c']));
|
|
27
|
+
expect(result).toEqual({ type: 'string', enum: ['a', 'b', 'c'] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('converts array schema', () => {
|
|
31
|
+
const result = zodToJsonSchema(z.array(z.string()));
|
|
32
|
+
expect(result).toEqual({ type: 'array', items: { type: 'string' } });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('converts object schema', () => {
|
|
36
|
+
const schema = z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
age: z.number(),
|
|
39
|
+
});
|
|
40
|
+
const result = zodToJsonSchema(schema);
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
name: { type: 'string' },
|
|
45
|
+
age: { type: 'number' },
|
|
46
|
+
},
|
|
47
|
+
required: ['name', 'age'],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('handles optional fields', () => {
|
|
53
|
+
const schema = z.object({
|
|
54
|
+
name: z.string(),
|
|
55
|
+
bio: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
const result = zodToJsonSchema(schema);
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
name: { type: 'string' },
|
|
62
|
+
bio: { type: 'string' },
|
|
63
|
+
},
|
|
64
|
+
required: ['name'],
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('converts nested object schema', () => {
|
|
70
|
+
const schema = z.object({
|
|
71
|
+
location: z.object({
|
|
72
|
+
file: z.string(),
|
|
73
|
+
line: z.number(),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const result = zodToJsonSchema(schema);
|
|
77
|
+
expect(result).toEqual({
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
location: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
file: { type: 'string' },
|
|
84
|
+
line: { type: 'number' },
|
|
85
|
+
},
|
|
86
|
+
required: ['file', 'line'],
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ['location'],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('converts literal schema', () => {
|
|
96
|
+
const result = zodToJsonSchema(z.literal('analyze'));
|
|
97
|
+
expect(result).toEqual({ type: 'string', const: 'analyze' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws for unsupported zod types instead of returning an empty schema', () => {
|
|
101
|
+
expect(() => zodToJsonSchema(z.tuple([z.string()]))).toThrow(
|
|
102
|
+
'unsupported Zod type',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('zodToAnthropicTool', () => {
|
|
108
|
+
it('wraps schema as Anthropic tool definition', () => {
|
|
109
|
+
const schema = z.object({ name: z.string() });
|
|
110
|
+
const result = zodToAnthropicTool(schema, 'test_tool', 'A test tool');
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
name: 'test_tool',
|
|
113
|
+
description: 'A test tool',
|
|
114
|
+
input_schema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: { name: { type: 'string' } },
|
|
117
|
+
required: ['name'],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('zodToOpenAIResponseFormat', () => {
|
|
125
|
+
it('wraps schema as OpenAI response_format', () => {
|
|
126
|
+
const schema = z.object({ value: z.number() });
|
|
127
|
+
const result = zodToOpenAIResponseFormat(schema, 'test_format');
|
|
128
|
+
expect(result).toEqual({
|
|
129
|
+
type: 'json_schema',
|
|
130
|
+
json_schema: {
|
|
131
|
+
name: 'test_format',
|
|
132
|
+
strict: true,
|
|
133
|
+
schema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: { value: { type: 'number' } },
|
|
136
|
+
required: ['value'],
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "bin/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
20
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Security scanner for OpenClaw: prompt injection firewall, package hallucination detection, code vulnerability scanning, auto-fix",
|
|
5
5
|
"author": "Sinewave AI",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
|
-
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages),
|
|
5
|
+
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1700+ vulnerability rules with AST & taint analysis, LLM-powered semantic code review, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"agent-security-scanner-mcp": "index.js"
|
|
9
|
+
"agent-security-scanner-mcp": "index.js",
|
|
10
|
+
"cr-agent": "code-review-agent/dist/bin/cr-agent.js"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
13
|
"start": "node index.js",
|
|
@@ -112,7 +113,17 @@
|
|
|
112
113
|
"daemon.py",
|
|
113
114
|
"python_taint_fallback.py",
|
|
114
115
|
"src/lib/*.js",
|
|
115
|
-
"compliance/**"
|
|
116
|
+
"compliance/**",
|
|
117
|
+
"code-review-agent/README.md",
|
|
118
|
+
"code-review-agent/TODO.md",
|
|
119
|
+
"code-review-agent/package.json",
|
|
120
|
+
"code-review-agent/tsconfig.json",
|
|
121
|
+
"code-review-agent/vitest.config.ts",
|
|
122
|
+
"code-review-agent/.env.example",
|
|
123
|
+
"code-review-agent/bin/**",
|
|
124
|
+
"code-review-agent/src/**",
|
|
125
|
+
"code-review-agent/dist/**",
|
|
126
|
+
"code-review-agent/tests/**"
|
|
116
127
|
],
|
|
117
128
|
"devDependencies": {
|
|
118
129
|
"all-the-package-names": "^2.0.2349",
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* postinstall.js -
|
|
4
|
-
*
|
|
3
|
+
* postinstall.js - Setup script for agent-security-scanner-mcp
|
|
4
|
+
* 1. Install Python dependencies for tree-sitter AST engine (optional)
|
|
5
|
+
* 2. Install and build code-review-agent dependencies (optional)
|
|
5
6
|
*/
|
|
6
|
-
import { execFileSync } from "child_process";
|
|
7
|
+
import { execFileSync, execSync } from "child_process";
|
|
7
8
|
import { join, dirname } from "path";
|
|
8
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import { existsSync } from "fs";
|
|
9
11
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const
|
|
13
|
+
const rootDir = join(__dirname, "..");
|
|
14
|
+
const requirementsPath = join(rootDir, "requirements.txt");
|
|
15
|
+
const codeReviewAgentDir = join(rootDir, "code-review-agent");
|
|
12
16
|
|
|
13
17
|
// Check if Python 3 is available
|
|
14
18
|
function findPython() {
|
|
@@ -33,6 +37,7 @@ function isTreeSitterInstalled(pythonCmd) {
|
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
// Setup Python dependencies
|
|
36
41
|
const pythonCmd = findPython();
|
|
37
42
|
|
|
38
43
|
if (!pythonCmd) {
|
|
@@ -58,3 +63,37 @@ if (!pythonCmd) {
|
|
|
58
63
|
);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
|
|
67
|
+
// Setup code-review-agent (LLM-powered semantic analysis)
|
|
68
|
+
if (existsSync(codeReviewAgentDir)) {
|
|
69
|
+
const distExists = existsSync(join(codeReviewAgentDir, "dist", "bin", "cr-agent.js"));
|
|
70
|
+
|
|
71
|
+
if (distExists) {
|
|
72
|
+
console.log("[postinstall] code-review-agent already built — cr-agent CLI available.");
|
|
73
|
+
} else {
|
|
74
|
+
console.log("[postinstall] Setting up code-review-agent (LLM-powered code review)...");
|
|
75
|
+
try {
|
|
76
|
+
// Install dependencies
|
|
77
|
+
execSync("npm install --omit=dev", {
|
|
78
|
+
cwd: codeReviewAgentDir,
|
|
79
|
+
timeout: 180000,
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Build TypeScript
|
|
84
|
+
execSync("npm run build", {
|
|
85
|
+
cwd: codeReviewAgentDir,
|
|
86
|
+
timeout: 60000,
|
|
87
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log("[postinstall] code-review-agent installed — run: npx cr-agent --help");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.log(
|
|
93
|
+
"[postinstall] Could not set up code-review-agent (optional LLM-powered review).\n" +
|
|
94
|
+
" The main scanner still works. To set up manually:\n" +
|
|
95
|
+
" cd node_modules/agent-security-scanner-mcp/code-review-agent && npm install && npm run build"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
package/server.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
4
|
-
"description": "MCP security scanner with prompt injection firewall, package hallucination detection, and auto-fix.",
|
|
5
|
-
"version": "
|
|
4
|
+
"description": "MCP security scanner with prompt injection firewall, package hallucination detection, LLM-powered code review, and auto-fix.",
|
|
5
|
+
"version": "4.0.0",
|
|
6
6
|
"transport": "stdio",
|
|
7
7
|
"registry": "npm"
|
|
8
8
|
}
|