ship-safe 9.1.1 → 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/agents/stateful-watcher.js +4 -7
- package/cli/agents/swarm-orchestrator.js +27 -65
- package/cli/bin/ship-safe.js +62 -7
- package/cli/commands/agent-fix.js +960 -0
- package/cli/commands/audit.js +24 -11
- package/cli/commands/red-team.js +10 -6
- package/cli/commands/shell.js +415 -0
- package/cli/commands/team-report.js +415 -0
- package/cli/commands/undo.js +143 -0
- package/cli/providers/llm-provider.js +149 -18
- package/cli/utils/output.js +21 -0
- 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,11 +440,17 @@ 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' },
|
|
362
451
|
cohere: { baseUrl: 'https://api.cohere.com/compatibility/v1/chat/completions', model: 'command-r-plus', envKey: 'COHERE_API_KEY' },
|
|
363
|
-
deepseek: { baseUrl: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-
|
|
452
|
+
deepseek: { baseUrl: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-v4-pro', envKey: 'DEEPSEEK_API_KEY' },
|
|
453
|
+
'deepseek-flash': { baseUrl: 'https://api.deepseek.com/v1/chat/completions', model: 'deepseek-v4-flash', envKey: 'DEEPSEEK_API_KEY' },
|
|
364
454
|
perplexity: { baseUrl: 'https://api.perplexity.ai/chat/completions', model: 'llama-3.1-sonar-large-128k-online', envKey: 'PERPLEXITY_API_KEY' },
|
|
365
455
|
lmstudio: { baseUrl: 'http://localhost:1234/v1/chat/completions', model: null, envKey: null },
|
|
366
456
|
xai: { baseUrl: 'https://api.x.ai/v1/chat/completions', model: 'grok-3-mini', envKey: 'XAI_API_KEY' },
|
|
@@ -380,7 +470,44 @@ class OpenAICompatibleProvider extends OpenAIProvider {
|
|
|
380
470
|
|
|
381
471
|
/** Models known to support OpenAI function calling reliably */
|
|
382
472
|
get supportsStructuredOutput() {
|
|
383
|
-
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 || '');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async complete(systemPrompt, userPrompt, options = {}) {
|
|
477
|
+
const body = {
|
|
478
|
+
model: options.model || this.model,
|
|
479
|
+
max_tokens: options.maxTokens || 2048,
|
|
480
|
+
messages: [
|
|
481
|
+
{ role: 'system', content: systemPrompt },
|
|
482
|
+
{ role: 'user', content: userPrompt },
|
|
483
|
+
],
|
|
484
|
+
};
|
|
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
|
+
}
|
|
490
|
+
|
|
491
|
+
const response = await fetch(this.baseUrl, {
|
|
492
|
+
method: 'POST',
|
|
493
|
+
headers: {
|
|
494
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
495
|
+
'Content-Type': 'application/json',
|
|
496
|
+
},
|
|
497
|
+
body: JSON.stringify(body),
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!response.ok) {
|
|
501
|
+
const errBody = await response.text().catch(() => '');
|
|
502
|
+
throw new Error(`${this.name} API error: HTTP ${response.status} ${errBody.slice(0, 200)}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const data = await response.json();
|
|
506
|
+
const msg = data.choices?.[0]?.message;
|
|
507
|
+
// Kimi K2.6 thinking mode: actual answer in `content`; `reasoning_content` is internal thinking only
|
|
508
|
+
// With jsonMode, rely only on content (json_object format guarantees it); otherwise fall back to reasoning
|
|
509
|
+
if (options.jsonMode) return msg?.content || '';
|
|
510
|
+
return msg?.content || msg?.reasoning_content || '';
|
|
384
511
|
}
|
|
385
512
|
|
|
386
513
|
/**
|
|
@@ -409,7 +536,7 @@ class OpenAICompatibleProvider extends OpenAIProvider {
|
|
|
409
536
|
parameters: inputSchema,
|
|
410
537
|
},
|
|
411
538
|
}],
|
|
412
|
-
tool_choice:
|
|
539
|
+
tool_choice: 'required',
|
|
413
540
|
}),
|
|
414
541
|
});
|
|
415
542
|
|
|
@@ -478,8 +605,10 @@ export function createProvider(provider, apiKey, options = {}) {
|
|
|
478
605
|
name.charAt(0).toUpperCase() + name.slice(1),
|
|
479
606
|
apiKey,
|
|
480
607
|
{
|
|
481
|
-
baseUrl:
|
|
482
|
-
model:
|
|
608
|
+
baseUrl: options.baseUrl || preset.baseUrl,
|
|
609
|
+
model: options.model || preset.model || 'default',
|
|
610
|
+
think: options.think,
|
|
611
|
+
thinkLevel: options.thinkLevel,
|
|
483
612
|
}
|
|
484
613
|
);
|
|
485
614
|
}
|
|
@@ -492,7 +621,7 @@ export function createProvider(provider, apiKey, options = {}) {
|
|
|
492
621
|
throw new Error(
|
|
493
622
|
`Unknown LLM provider: "${provider}".\n` +
|
|
494
623
|
`Built-in: anthropic, openai, google, ollama\n` +
|
|
495
|
-
`Presets: groq, together, mistral, cohere, deepseek, 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` +
|
|
496
625
|
`Custom: pass any name with --base-url <url>`
|
|
497
626
|
);
|
|
498
627
|
}
|
|
@@ -508,8 +637,10 @@ export function autoDetectProvider(rootPath, options = {}) {
|
|
|
508
637
|
if (options.provider) {
|
|
509
638
|
const apiKey = resolveApiKey(options.provider, rootPath);
|
|
510
639
|
return createProvider(options.provider, apiKey, {
|
|
511
|
-
model:
|
|
512
|
-
baseUrl:
|
|
640
|
+
model: options.model,
|
|
641
|
+
baseUrl: options.baseUrl,
|
|
642
|
+
think: options.think,
|
|
643
|
+
thinkLevel: options.thinkLevel,
|
|
513
644
|
});
|
|
514
645
|
}
|
|
515
646
|
|
package/cli/utils/output.js
CHANGED
|
@@ -228,3 +228,24 @@ export function progress(text) {
|
|
|
228
228
|
export function clearLine() {
|
|
229
229
|
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
230
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Print the Ship Safe ASCII banner.
|
|
234
|
+
* Call at the top of any command that should show branding.
|
|
235
|
+
*/
|
|
236
|
+
export function printBanner(version) {
|
|
237
|
+
console.log();
|
|
238
|
+
console.log(chalk.cyan('███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗'));
|
|
239
|
+
console.log(chalk.cyan('██╔════╝██║ ██║██║██╔══██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝'));
|
|
240
|
+
console.log(chalk.cyan('███████╗███████║██║██████╔╝ ███████╗███████║█████╗ █████╗ '));
|
|
241
|
+
console.log(chalk.cyan('╚════██║██╔══██║██║██╔═══╝ ╚════██║██╔══██║██╔══╝ ██╔══╝ '));
|
|
242
|
+
console.log(chalk.cyan('███████║██║ ██║██║██║ ███████║██║ ██║██║ ███████╗'));
|
|
243
|
+
console.log(chalk.cyan('╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝'));
|
|
244
|
+
console.log();
|
|
245
|
+
if (version) {
|
|
246
|
+
console.log(chalk.gray(` v${version} · 23 agents · 80+ attack classes · shipsafecli.com`));
|
|
247
|
+
} else {
|
|
248
|
+
console.log(chalk.gray(' 23 agents · 80+ attack classes · shipsafecli.com'));
|
|
249
|
+
}
|
|
250
|
+
console.log();
|
|
251
|
+
}
|
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": {
|