ship-safe 9.1.2 → 9.2.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/cli/agents/llm-redteam.js +24 -2
- package/cli/bin/ship-safe.js +51 -7
- package/cli/commands/agent-fix.js +960 -0
- package/cli/commands/audit.js +22 -6
- package/cli/commands/shell.js +415 -0
- package/cli/commands/undo.js +143 -0
- package/cli/providers/llm-provider.js +113 -16
- package/package.json +1 -1
|
@@ -27,6 +27,8 @@ class BaseLLMProvider {
|
|
|
27
27
|
this.apiKey = apiKey;
|
|
28
28
|
this.model = options.model || null;
|
|
29
29
|
this.baseUrl = options.baseUrl || null;
|
|
30
|
+
this.think = options.think || false;
|
|
31
|
+
this.thinkLevel = options.thinkLevel || 'high';
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -36,6 +38,16 @@ class BaseLLMProvider {
|
|
|
36
38
|
throw new Error(`${this.name}.complete() not implemented`);
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Stream a completion as an async iterable of text chunks.
|
|
43
|
+
* Default implementation falls back to complete() and yields the whole
|
|
44
|
+
* response at once — providers that support real streaming should override.
|
|
45
|
+
*/
|
|
46
|
+
async *stream(systemPrompt, userPrompt, options = {}) {
|
|
47
|
+
const text = await this.complete(systemPrompt, userPrompt, options);
|
|
48
|
+
if (text) yield text;
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
/**
|
|
40
52
|
* Classify security findings using the LLM.
|
|
41
53
|
*/
|
|
@@ -44,7 +56,7 @@ class BaseLLMProvider {
|
|
|
44
56
|
const response = await this.complete(
|
|
45
57
|
'You are a security expert. Respond with JSON only, no markdown.',
|
|
46
58
|
prompt,
|
|
47
|
-
{ maxTokens: 4096 }
|
|
59
|
+
{ maxTokens: 4096, think: this.think, thinkLevel: this.thinkLevel }
|
|
48
60
|
);
|
|
49
61
|
return this.parseJSON(response);
|
|
50
62
|
}
|
|
@@ -172,25 +184,32 @@ class AnthropicProvider extends BaseLLMProvider {
|
|
|
172
184
|
class OpenAIProvider extends BaseLLMProvider {
|
|
173
185
|
constructor(apiKey, options = {}) {
|
|
174
186
|
super('OpenAI', apiKey, options);
|
|
175
|
-
this.model = options.model || 'gpt-
|
|
187
|
+
this.model = options.model || 'gpt-5.5';
|
|
176
188
|
this.baseUrl = options.baseUrl || 'https://api.openai.com/v1/chat/completions';
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
async complete(systemPrompt, userPrompt, options = {}) {
|
|
192
|
+
const body = {
|
|
193
|
+
model: this.model,
|
|
194
|
+
max_tokens: options.maxTokens || 2048,
|
|
195
|
+
messages: [
|
|
196
|
+
{ role: 'system', content: systemPrompt },
|
|
197
|
+
{ role: 'user', content: userPrompt },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (options.think) {
|
|
202
|
+
body.reasoning_effort = options.thinkLevel || 'high';
|
|
203
|
+
body.max_tokens = options.maxTokens || 16384;
|
|
204
|
+
}
|
|
205
|
+
|
|
180
206
|
const response = await fetch(this.baseUrl, {
|
|
181
207
|
method: 'POST',
|
|
182
208
|
headers: {
|
|
183
209
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
184
210
|
'Content-Type': 'application/json',
|
|
185
211
|
},
|
|
186
|
-
body: JSON.stringify(
|
|
187
|
-
model: this.model,
|
|
188
|
-
max_tokens: options.maxTokens || 2048,
|
|
189
|
-
messages: [
|
|
190
|
-
{ role: 'system', content: systemPrompt },
|
|
191
|
-
{ role: 'user', content: userPrompt },
|
|
192
|
-
],
|
|
193
|
-
}),
|
|
212
|
+
body: JSON.stringify(body),
|
|
194
213
|
});
|
|
195
214
|
|
|
196
215
|
if (!response.ok) {
|
|
@@ -200,6 +219,71 @@ class OpenAIProvider extends BaseLLMProvider {
|
|
|
200
219
|
const data = await response.json();
|
|
201
220
|
return data.choices?.[0]?.message?.content || '';
|
|
202
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Streaming variant for the OpenAI Chat Completions SSE protocol.
|
|
225
|
+
* Yields content tokens as they arrive. Inherited by every
|
|
226
|
+
* OpenAI-compatible provider (DeepSeek, Kimi, xAI, OpenRouter).
|
|
227
|
+
*/
|
|
228
|
+
async *stream(systemPrompt, userPrompt, options = {}) {
|
|
229
|
+
const body = {
|
|
230
|
+
model: this.model,
|
|
231
|
+
max_tokens: options.maxTokens || 2048,
|
|
232
|
+
messages: [
|
|
233
|
+
{ role: 'system', content: systemPrompt },
|
|
234
|
+
{ role: 'user', content: userPrompt },
|
|
235
|
+
],
|
|
236
|
+
stream: true,
|
|
237
|
+
};
|
|
238
|
+
if (options.think) {
|
|
239
|
+
body.reasoning_effort = options.thinkLevel || 'high';
|
|
240
|
+
body.max_tokens = options.maxTokens || 16384;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const response = await fetch(this.baseUrl, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: {
|
|
246
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
247
|
+
'Content-Type': 'application/json',
|
|
248
|
+
'Accept': 'text/event-stream',
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify(body),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
const errBody = await response.text().catch(() => '');
|
|
255
|
+
throw new Error(`${this.name} API error: HTTP ${response.status} ${errBody.slice(0, 200)}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const reader = response.body.getReader();
|
|
259
|
+
const decoder = new TextDecoder('utf-8');
|
|
260
|
+
let buffer = '';
|
|
261
|
+
|
|
262
|
+
while (true) {
|
|
263
|
+
const { value, done } = await reader.read();
|
|
264
|
+
if (done) break;
|
|
265
|
+
buffer += decoder.decode(value, { stream: true });
|
|
266
|
+
|
|
267
|
+
// SSE events are separated by blank lines; parse line-by-line and only
|
|
268
|
+
// act on `data: ` payloads. Everything else (event:, id:, comments) ignored.
|
|
269
|
+
const lines = buffer.split('\n');
|
|
270
|
+
buffer = lines.pop() ?? ''; // keep trailing partial line for next chunk
|
|
271
|
+
|
|
272
|
+
for (const raw of lines) {
|
|
273
|
+
const line = raw.trim();
|
|
274
|
+
if (!line.startsWith('data:')) continue;
|
|
275
|
+
const payload = line.slice(5).trim();
|
|
276
|
+
if (!payload || payload === '[DONE]') continue;
|
|
277
|
+
try {
|
|
278
|
+
const evt = JSON.parse(payload);
|
|
279
|
+
const delta = evt.choices?.[0]?.delta;
|
|
280
|
+
// Token text (delta.content) — yield as-is. Some providers also send
|
|
281
|
+
// tool_calls; ignored here since the REPL doesn't use tools yet.
|
|
282
|
+
if (delta?.content) yield delta.content;
|
|
283
|
+
} catch { /* malformed chunk — skip */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
203
287
|
}
|
|
204
288
|
|
|
205
289
|
// =============================================================================
|
|
@@ -356,6 +440,11 @@ class GemmaProvider extends OllamaProvider {
|
|
|
356
440
|
|
|
357
441
|
// Well-known OpenAI-compatible base URLs and their default models.
|
|
358
442
|
const OPENAI_COMPATIBLE_PRESETS = {
|
|
443
|
+
'gpt-5.5': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.5', envKey: 'OPENAI_API_KEY' },
|
|
444
|
+
'gpt-5.5-pro': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.5-pro', envKey: 'OPENAI_API_KEY' },
|
|
445
|
+
'gpt-5.4': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4', envKey: 'OPENAI_API_KEY' },
|
|
446
|
+
'gpt-5.4-mini': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4-mini', envKey: 'OPENAI_API_KEY' },
|
|
447
|
+
'gpt-5.4-nano': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4-nano', envKey: 'OPENAI_API_KEY' },
|
|
359
448
|
groq: { baseUrl: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile', envKey: 'GROQ_API_KEY' },
|
|
360
449
|
together: { baseUrl: 'https://api.together.xyz/v1/chat/completions', model: 'meta-llama/Llama-3-70b-chat-hf', envKey: 'TOGETHER_API_KEY' },
|
|
361
450
|
mistral: { baseUrl: 'https://api.mistral.ai/v1/chat/completions', model: 'mistral-large-latest', envKey: 'MISTRAL_API_KEY' },
|
|
@@ -381,7 +470,7 @@ class OpenAICompatibleProvider extends OpenAIProvider {
|
|
|
381
470
|
|
|
382
471
|
/** Models known to support OpenAI function calling reliably */
|
|
383
472
|
get supportsStructuredOutput() {
|
|
384
|
-
return /kimi|moonshot|gpt-4|grok|deepseek|mistral-large/i.test(this.model || '');
|
|
473
|
+
return /kimi|moonshot|gpt-4|gpt-5|grok|deepseek|mistral-large/i.test(this.model || '');
|
|
385
474
|
}
|
|
386
475
|
|
|
387
476
|
async complete(systemPrompt, userPrompt, options = {}) {
|
|
@@ -394,6 +483,10 @@ class OpenAICompatibleProvider extends OpenAIProvider {
|
|
|
394
483
|
],
|
|
395
484
|
};
|
|
396
485
|
if (options.jsonMode) body.response_format = { type: 'json_object' };
|
|
486
|
+
if (options.think) {
|
|
487
|
+
body.reasoning_effort = options.thinkLevel || 'high';
|
|
488
|
+
body.max_tokens = options.maxTokens || 16384;
|
|
489
|
+
}
|
|
397
490
|
|
|
398
491
|
const response = await fetch(this.baseUrl, {
|
|
399
492
|
method: 'POST',
|
|
@@ -512,8 +605,10 @@ export function createProvider(provider, apiKey, options = {}) {
|
|
|
512
605
|
name.charAt(0).toUpperCase() + name.slice(1),
|
|
513
606
|
apiKey,
|
|
514
607
|
{
|
|
515
|
-
baseUrl:
|
|
516
|
-
model:
|
|
608
|
+
baseUrl: options.baseUrl || preset.baseUrl,
|
|
609
|
+
model: options.model || preset.model || 'default',
|
|
610
|
+
think: options.think,
|
|
611
|
+
thinkLevel: options.thinkLevel,
|
|
517
612
|
}
|
|
518
613
|
);
|
|
519
614
|
}
|
|
@@ -526,7 +621,7 @@ export function createProvider(provider, apiKey, options = {}) {
|
|
|
526
621
|
throw new Error(
|
|
527
622
|
`Unknown LLM provider: "${provider}".\n` +
|
|
528
623
|
`Built-in: anthropic, openai, google, ollama\n` +
|
|
529
|
-
`Presets: groq, together, mistral, cohere, deepseek, deepseek-flash, perplexity, lmstudio, xai, kimi\n` +
|
|
624
|
+
`Presets: gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, groq, together, mistral, cohere, deepseek, deepseek-flash, perplexity, lmstudio, xai, kimi\n` +
|
|
530
625
|
`Custom: pass any name with --base-url <url>`
|
|
531
626
|
);
|
|
532
627
|
}
|
|
@@ -542,8 +637,10 @@ export function autoDetectProvider(rootPath, options = {}) {
|
|
|
542
637
|
if (options.provider) {
|
|
543
638
|
const apiKey = resolveApiKey(options.provider, rootPath);
|
|
544
639
|
return createProvider(options.provider, apiKey, {
|
|
545
|
-
model:
|
|
546
|
-
baseUrl:
|
|
640
|
+
model: options.model,
|
|
641
|
+
baseUrl: options.baseUrl,
|
|
642
|
+
think: options.think,
|
|
643
|
+
thinkLevel: options.thinkLevel,
|
|
547
644
|
});
|
|
548
645
|
}
|
|
549
646
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ship-safe",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|