agent-security-scanner-mcp 3.20.0 → 4.0.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/README.md +144 -43
- 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 +142 -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 +188 -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/index.js +18 -18
- package/openclaw.plugin.json +2 -2
- package/package.json +13 -3
- package/server.json +3 -3
- package/src/cli/init-hooks.js +3 -3
- package/src/cli/init.js +1 -1
|
@@ -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/index.js
CHANGED
|
@@ -47,7 +47,7 @@ try {
|
|
|
47
47
|
// Create MCP Server
|
|
48
48
|
const server = new McpServer(
|
|
49
49
|
{
|
|
50
|
-
name: "agent-security
|
|
50
|
+
name: "prooflayer-agent-security",
|
|
51
51
|
version: _pkgVersion,
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -306,7 +306,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
306
306
|
// CLI mode: scan-prompt <text> [--verbosity minimal|compact|full]
|
|
307
307
|
const text = cliArgs[1];
|
|
308
308
|
if (!text) {
|
|
309
|
-
console.error('Usage: agent-security
|
|
309
|
+
console.error('Usage: prooflayer-agent-security scan-prompt <text> [--verbosity minimal|compact|full]');
|
|
310
310
|
process.exit(1);
|
|
311
311
|
}
|
|
312
312
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -326,7 +326,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
326
326
|
// CLI mode: scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]
|
|
327
327
|
const filePath = cliArgs[1];
|
|
328
328
|
if (!filePath) {
|
|
329
|
-
console.error('Usage: agent-security
|
|
329
|
+
console.error('Usage: prooflayer-agent-security scan-security <file> [--verbosity minimal|compact|full] [--format json|sarif]');
|
|
330
330
|
process.exit(1);
|
|
331
331
|
}
|
|
332
332
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -348,7 +348,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
348
348
|
const packageName = cliArgs[1];
|
|
349
349
|
const ecosystem = cliArgs[2];
|
|
350
350
|
if (!packageName || !ecosystem) {
|
|
351
|
-
console.error('Usage: agent-security
|
|
351
|
+
console.error('Usage: prooflayer-agent-security check-package <name> <ecosystem>');
|
|
352
352
|
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
353
353
|
process.exit(1);
|
|
354
354
|
}
|
|
@@ -367,7 +367,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
367
367
|
const filePath = cliArgs[1];
|
|
368
368
|
const ecosystem = cliArgs[2];
|
|
369
369
|
if (!filePath || !ecosystem) {
|
|
370
|
-
console.error('Usage: agent-security
|
|
370
|
+
console.error('Usage: prooflayer-agent-security scan-packages <file> <ecosystem> [--verbosity minimal|compact|full]');
|
|
371
371
|
console.error('Ecosystems: npm, pypi, rubygems, crates, dart, perl, raku');
|
|
372
372
|
process.exit(1);
|
|
373
373
|
}
|
|
@@ -387,7 +387,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
387
387
|
// CLI mode: scan-project <dir> [--recursive] [--diff-only] [--cross-file] [--include '*.py'] [--exclude '*.test.js'] [--verbosity minimal|compact|full]
|
|
388
388
|
const dirPath = cliArgs[1];
|
|
389
389
|
if (!dirPath || dirPath.startsWith('--')) {
|
|
390
|
-
console.error('Usage: agent-security
|
|
390
|
+
console.error('Usage: prooflayer-agent-security scan-project <directory> [--recursive] [--diff-only] [--cross-file] [--include <pattern>] [--exclude <pattern>] [--verbosity minimal|compact|full]');
|
|
391
391
|
process.exit(1);
|
|
392
392
|
}
|
|
393
393
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -455,7 +455,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
455
455
|
// CLI mode: scan-mcp <path> [--verbosity minimal|compact|full]
|
|
456
456
|
const serverPath = cliArgs[1];
|
|
457
457
|
if (!serverPath) {
|
|
458
|
-
console.error('Usage: agent-security
|
|
458
|
+
console.error('Usage: prooflayer-agent-security scan-mcp <server-path> [--verbosity minimal|compact|full]');
|
|
459
459
|
process.exit(1);
|
|
460
460
|
}
|
|
461
461
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -474,7 +474,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
474
474
|
const actionType = cliArgs[1];
|
|
475
475
|
const actionValue = cliArgs[2];
|
|
476
476
|
if (!actionType || !actionValue) {
|
|
477
|
-
console.error('Usage: agent-security
|
|
477
|
+
console.error('Usage: prooflayer-agent-security scan-action <type> <value> [--verbosity minimal|compact|full]');
|
|
478
478
|
console.error('Types: bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker');
|
|
479
479
|
process.exit(1);
|
|
480
480
|
}
|
|
@@ -492,7 +492,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
492
492
|
} else if (cliArgs[0] === 'scan-skill') {
|
|
493
493
|
const skillPath = cliArgs[1];
|
|
494
494
|
if (!skillPath) {
|
|
495
|
-
console.error('Usage: agent-security
|
|
495
|
+
console.error('Usage: prooflayer-agent-security scan-skill <skill-path> [--verbosity minimal|compact|full] [--baseline]');
|
|
496
496
|
process.exit(1);
|
|
497
497
|
}
|
|
498
498
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
|
@@ -529,7 +529,7 @@ const cliArgs = process.argv.slice(2);
|
|
|
529
529
|
await import('./src/cli/scan-clawhub-safe.js');
|
|
530
530
|
// Exit is handled by scan-clawhub-safe.js
|
|
531
531
|
} else if (cliArgs[0] === '--help' || cliArgs[0] === '-h' || cliArgs[0] === 'help') {
|
|
532
|
-
console.log('\n agent-security
|
|
532
|
+
console.log('\n prooflayer-agent-security\n');
|
|
533
533
|
console.log(' Commands:');
|
|
534
534
|
console.log(' init [client] Set up MCP config for a client');
|
|
535
535
|
console.log(' init-hooks Install Claude Code hooks for auto-scanning');
|
|
@@ -557,14 +557,14 @@ const cliArgs = process.argv.slice(2);
|
|
|
557
557
|
console.log(' --include <pattern> Include only matching files (scan-project)');
|
|
558
558
|
console.log(' --exclude <pattern> Exclude matching files (scan-project)\n');
|
|
559
559
|
console.log(' Examples:');
|
|
560
|
-
console.log(' npx agent-security
|
|
561
|
-
console.log(' npx agent-security
|
|
562
|
-
console.log(' npx agent-security
|
|
563
|
-
console.log(' npx agent-security
|
|
564
|
-
console.log(' npx agent-security
|
|
565
|
-
console.log(' npx agent-security
|
|
566
|
-
console.log(' npx agent-security
|
|
567
|
-
console.log(' npx agent-security
|
|
560
|
+
console.log(' npx prooflayer-agent-security init');
|
|
561
|
+
console.log(' npx prooflayer-agent-security scan-prompt "ignore previous instructions"');
|
|
562
|
+
console.log(' npx prooflayer-agent-security scan-security ./app.py --verbosity minimal');
|
|
563
|
+
console.log(' npx prooflayer-agent-security check-package flask pypi');
|
|
564
|
+
console.log(' npx prooflayer-agent-security scan-project ./src --verbosity minimal');
|
|
565
|
+
console.log(' npx prooflayer-agent-security scan-diff HEAD~1');
|
|
566
|
+
console.log(' npx prooflayer-agent-security report ./src --json');
|
|
567
|
+
console.log(' npx prooflayer-agent-security benchmark --save --compare-latest\n');
|
|
568
568
|
process.exit(0);
|
|
569
569
|
} else {
|
|
570
570
|
// Normal MCP server mode
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "agent-security
|
|
3
|
-
"version": "
|
|
2
|
+
"name": "prooflayer-agent-security",
|
|
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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
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": {
|
|
@@ -112,7 +112,17 @@
|
|
|
112
112
|
"daemon.py",
|
|
113
113
|
"python_taint_fallback.py",
|
|
114
114
|
"src/lib/*.js",
|
|
115
|
-
"compliance/**"
|
|
115
|
+
"compliance/**",
|
|
116
|
+
"code-review-agent/README.md",
|
|
117
|
+
"code-review-agent/TODO.md",
|
|
118
|
+
"code-review-agent/package.json",
|
|
119
|
+
"code-review-agent/tsconfig.json",
|
|
120
|
+
"code-review-agent/vitest.config.ts",
|
|
121
|
+
"code-review-agent/.env.example",
|
|
122
|
+
"code-review-agent/bin/**",
|
|
123
|
+
"code-review-agent/src/**",
|
|
124
|
+
"code-review-agent/dist/**",
|
|
125
|
+
"code-review-agent/tests/**"
|
|
116
126
|
],
|
|
117
127
|
"devDependencies": {
|
|
118
128
|
"all-the-package-names": "^2.0.2349",
|
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
|
-
"name": "io.github.sinewaveai/agent-security
|
|
4
|
-
"description": "MCP security scanner with prompt injection firewall, package hallucination detection, and auto-fix.",
|
|
5
|
-
"version": "
|
|
3
|
+
"name": "io.github.sinewaveai/prooflayer-agent-security",
|
|
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
|
}
|