Merge pull request #70 from jmagly/fix/60-62-auth-github-deepseek
Add authenticated repo scans and DeepSeek provider
This commit is contained in:
14 files changed
+357
-90
No files matched your search
@@ -6,6 +6,7 @@ OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|||||||
VENICE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
VENICE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
GROQ_API_KEY=
|
GROQ_API_KEY=
|
||||||
TOGETHER_API_KEY=
|
TOGETHER_API_KEY=
|
||||||
HUGGINGFACE_TOKEN=
|
HUGGINGFACE_TOKEN=
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ The framework is an 8-operator kill chain, and this table won't blow smoke about
|
|||||||
| Re-derivable measurement (`verify-claims`) | ✅ Stable | every headline recomputes from committed artifacts |
|
| Re-derivable measurement (`verify-claims`) | ✅ Stable | every headline recomputes from committed artifacts |
|
||||||
| Recon engine | ✅ Stable | drives nmap / DNS / HTTP / fingerprinting; every finding traces to real tool output |
|
| Recon engine | ✅ Stable | drives nmap / DNS / HTTP / fingerprinting; every finding traces to real tool output |
|
||||||
| Mission engine + War Room + Op Admiral | ✅ Stable | keyless through a connected local agent |
|
| Mission engine + War Room + Op Admiral | ✅ Stable | keyless through a connected local agent |
|
||||||
| Arsenal, MCP server, HTTP API | ✅ Stable | 35 built-in tools by default; 102 with the opt-in `T3MP3ST_FULL_ARSENAL` (+67 adapters, with dangerous/catalog-only drivers — metasploit, hydra, pacu, frida — behind narrow approved paths rather than generic execution) — both counts re-derive via `verify-claims`. `security_recon` over MCP |
|
| Arsenal, MCP server, HTTP API | ✅ Stable | 35 built-in tools by default; 108 with the opt-in `T3MP3ST_FULL_ARSENAL` (+73 adapters, with dangerous/catalog-only drivers — metasploit, hydra, pacu, frida — behind narrow approved paths rather than generic execution) — both counts re-derive via `verify-claims`. `security_recon` over MCP |
|
||||||
| Egress-scope containment | ✅ Stable (on by default) | once a mission target is set, built-in networked tools refuse off-scope public hosts — not the target/subdomains, not loopback/private (`SCOPE DENIED`) — a tightened default, not a bare tool runner |
|
| Egress-scope containment | ✅ Stable (on by default) | once a mission target is set, built-in networked tools refuse off-scope public hosts — not the target/subdomains, not loopback/private (`SCOPE DENIED`) — a tightened default, not a bare tool runner |
|
||||||
| Coordinated-disclosure pipeline | ✅ Stable | OSV novelty + live PoC + refuter panel + CVSS; drafts only, a human sends |
|
| Coordinated-disclosure pipeline | ✅ Stable | OSV novelty + live PoC + refuter panel + CVSS; drafts only, a human sends |
|
||||||
| White-box source analysis | ⚠️ Experimental | Python-only regex ingest; multi-model decomposition costs more tokens, not fewer |
|
| White-box source analysis | ⚠️ Experimental | Python-only regex ingest; multi-model decomposition costs more tokens, not fewer |
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ check('VERIFY gate implemented', /VERIFY gate|never appeared in tool output/.tes
|
|||||||
check('REFLECT gate implemented', /REFLECT gate/.test(bench), 'forced mid-run pivot');
|
check('REFLECT gate implemented', /REFLECT gate/.test(bench), 'forced mid-run pivot');
|
||||||
|
|
||||||
// ── CLAIM 4: capability breadth ─────────────────────────────────────────────
|
// ── CLAIM 4: capability breadth ─────────────────────────────────────────────
|
||||||
console.log('\nCLAIM 4 — capability: 102 tools, 8-operator kill-chain');
|
console.log('\nCLAIM 4 — capability: 108 tools, 8-operator kill-chain');
|
||||||
// DISTINCT tools = external-binary adapters (catalog.ts TOOL_ADAPTERS id:) + custom
|
// DISTINCT tools = external-binary adapters (catalog.ts TOOL_ADAPTERS id:) + custom
|
||||||
// built-in/external tools (index.ts top-level name:). NOT a name:/id: regex count
|
// built-in/external tools (index.ts top-level name:). NOT a name:/id: regex count
|
||||||
// (that double-counts each tool's id+name AND every parameter name).
|
// (that double-counts each tool's id+name AND every parameter name).
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('API key environment handling hardening', () => {
|
|||||||
it('exportConfig redacts every supported provider key slot', () => {
|
it('exportConfig redacts every supported provider key slot', () => {
|
||||||
const block = exportConfigBlock();
|
const block = exportConfigBlock();
|
||||||
|
|
||||||
for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai']) {
|
for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'deepseek', 'litellm']) {
|
||||||
expect(block).toContain(`${provider}: settings.apiKeys.${provider} ? '***REDACTED***' : undefined`);
|
expect(block).toContain(`${provider}: settings.apiKeys.${provider} ? '***REDACTED***' : undefined`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// ARSENAL COUNT HONESTY — the "advertised = real, and nothing is silently dead".
|
// ARSENAL COUNT HONESTY — the "advertised = real, and nothing is silently dead".
|
||||||
//
|
//
|
||||||
// The arsenal advertises "102 tools". verify-claims defends that number by counting
|
// The arsenal advertises "108 tools". verify-claims defends that number by counting
|
||||||
// `id:`/`name:` source lines — a SOURCE-LINE count, which would keep passing even if
|
// `id:`/`name:` source lines — a SOURCE-LINE count, which would keep passing even if
|
||||||
// an entry became uncallable. This test locks the number to the REAL registered arrays
|
// an entry became uncallable. This test locks the number to the REAL registered arrays
|
||||||
// and, more importantly, pins the invariant that makes the count honest:
|
// and, more importantly, pins the invariant that makes the count honest:
|
||||||
@@ -34,16 +34,16 @@ const isGated = (a: ToolAdapter) =>
|
|||||||
a.execution === 'catalog_only' || a.execution === 'import_only';
|
a.execution === 'catalog_only' || a.execution === 'import_only';
|
||||||
|
|
||||||
describe('arsenal count honesty (advertised = real registered surface)', () => {
|
describe('arsenal count honesty (advertised = real registered surface)', () => {
|
||||||
it('the advertised "102 tools" is the real registered surface, not a source-line count', () => {
|
it('the advertised "108 tools" is the real registered surface, not a source-line count', () => {
|
||||||
const total = TOOL_ADAPTERS.length + BUILTIN_TOOLS.length + EXTERNAL_TOOLS.length;
|
const total = TOOL_ADAPTERS.length + BUILTIN_TOOLS.length + EXTERNAL_TOOLS.length;
|
||||||
// Locks the headline to code. If the arsenal grows/shrinks, update this AND the
|
// Locks the headline to code. If the arsenal grows/shrinks, update this AND the
|
||||||
// README / verify-claims headline together — that is the point of the lock.
|
// README / verify-claims headline together — that is the point of the lock.
|
||||||
expect(
|
expect(
|
||||||
total,
|
total,
|
||||||
`arsenal size drifted from the advertised 102 (adapters=${TOOL_ADAPTERS.length}, ` +
|
`arsenal size drifted from the advertised 108 (adapters=${TOOL_ADAPTERS.length}, ` +
|
||||||
`built-ins=${BUILTIN_TOOLS.length}, externals=${EXTERNAL_TOOLS.length}) — ` +
|
`built-ins=${BUILTIN_TOOLS.length}, externals=${EXTERNAL_TOOLS.length}) — ` +
|
||||||
'update the README / verify-claims headline to match',
|
'update the README / verify-claims headline to match',
|
||||||
).toBe(102);
|
).toBe(108);
|
||||||
expect(total).toBeGreaterThanOrEqual(80); // stays consistent with verify-claims' `>= 80` gate
|
expect(total).toBeGreaterThanOrEqual(80); // stays consistent with verify-claims' `>= 80` gate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it, afterEach } from 'vitest';
|
||||||
|
import { config, AVAILABLE_MODELS } from '../config/index.js';
|
||||||
|
import { createDeepSeekBackbone } from '../llm/index.js';
|
||||||
|
|
||||||
|
const KEY = 'deepseek-key-abcdef123456';
|
||||||
|
|
||||||
|
describe('DeepSeek provider wiring (#62)', () => {
|
||||||
|
afterEach(() => { delete process.env.DEEPSEEK_API_KEY; });
|
||||||
|
|
||||||
|
it('resolves DeepSeek base URL, default model, and key from DEEPSEEK_API_KEY', () => {
|
||||||
|
process.env.DEEPSEEK_API_KEY = KEY;
|
||||||
|
const cfg = config.getLLMConfig('deepseek');
|
||||||
|
expect(cfg.provider).toBe('deepseek');
|
||||||
|
expect(cfg.baseUrl).toBe('https://api.deepseek.com/v1');
|
||||||
|
expect(cfg.model).toBe('deepseek-chat');
|
||||||
|
expect(cfg.apiKey).toBe(KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes through the OpenAI-compatible backbone and validates with a key', () => {
|
||||||
|
process.env.DEEPSEEK_API_KEY = KEY;
|
||||||
|
const bb = createDeepSeekBackbone();
|
||||||
|
expect(bb.getProvider()).toBe('deepseek');
|
||||||
|
expect(bb.validateConfig().valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces native DeepSeek models and configured provider state', () => {
|
||||||
|
process.env.DEEPSEEK_API_KEY = KEY;
|
||||||
|
expect(AVAILABLE_MODELS.deepseek?.map(m => m.id)).toEqual(
|
||||||
|
expect.arrayContaining(['deepseek-chat', 'deepseek-reasoner']),
|
||||||
|
);
|
||||||
|
expect(config.getConfiguredProviders()).toContain('deepseek');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,11 +14,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||||
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, symlinkSync, realpathSync } from 'fs';
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync, symlinkSync, realpathSync, readFileSync } from 'fs';
|
||||||
import { statSync } from 'node:fs';
|
import { statSync } from 'node:fs';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { resolveContainedRepoPath, RepoPathError } from '../recon/whitebox.js';
|
import { resolveContainedRepoPath, resolveRepoSourceForAnalysis, RepoCloneError, RepoPathError } from '../recon/whitebox.js';
|
||||||
|
|
||||||
|
const whiteboxSource = readFileSync('src/recon/whitebox.ts', 'utf8');
|
||||||
|
|
||||||
// Mock node:fs (what whitebox.ts imports) so we can force statSync to throw AFTER realpath
|
// Mock node:fs (what whitebox.ts imports) so we can force statSync to throw AFTER realpath
|
||||||
// succeeds — the TOCTOU race path. Every other export keeps its real implementation, and
|
// succeeds — the TOCTOU race path. Every other export keeps its real implementation, and
|
||||||
@@ -98,6 +100,29 @@ describe('resolveContainedRepoPath (B-03 path containment)', () => {
|
|||||||
expect(() => resolveContainedRepoPath(link)).toThrow(RepoPathError);
|
expect(() => resolveContainedRepoPath(link)).toThrow(RepoPathError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats GitHub HTTPS repo URLs as remote sources and requires a GitHub token before cloning', () => {
|
||||||
|
const prev = process.env.GITHUB_TOKEN;
|
||||||
|
const prevT3 = process.env.T3MP3ST_GITHUB_TOKEN;
|
||||||
|
delete process.env.GITHUB_TOKEN;
|
||||||
|
delete process.env.T3MP3ST_GITHUB_TOKEN;
|
||||||
|
try {
|
||||||
|
expect(() => resolveRepoSourceForAnalysis('https://github.com/example/private-repo')).toThrow(RepoCloneError);
|
||||||
|
expect(() => resolveRepoSourceForAnalysis('https://github.com/example/private-repo')).toThrow(/T3MP3ST_GITHUB_TOKEN or GITHUB_TOKEN/);
|
||||||
|
} finally {
|
||||||
|
if (prev === undefined) delete process.env.GITHUB_TOKEN;
|
||||||
|
else process.env.GITHUB_TOKEN = prev;
|
||||||
|
if (prevT3 === undefined) delete process.env.T3MP3ST_GITHUB_TOKEN;
|
||||||
|
else process.env.T3MP3ST_GITHUB_TOKEN = prevT3;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not embed GitHub tokens in the git clone URL argument', () => {
|
||||||
|
expect(whiteboxSource).toContain('GIT_ASKPASS');
|
||||||
|
expect(whiteboxSource).toContain('T3MP3ST_GIT_TOKEN');
|
||||||
|
expect(whiteboxSource).not.toContain('githubCloneUrlWithToken');
|
||||||
|
expect(whiteboxSource).not.toContain('url.password = token');
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes a statSync failure (TOCTOU delete after realpath) to RepoPathError', () => {
|
it('normalizes a statSync failure (TOCTOU delete after realpath) to RepoPathError', () => {
|
||||||
// The dir realpath-resolves (exists) but statSync then throws — simulating a deletion in
|
// The dir realpath-resolves (exists) but statSync then throws — simulating a deletion in
|
||||||
// the race window between realpath and stat. The wrap must convert it to a RepoPathError
|
// the race window between realpath and stat. The wrap must convert it to a RepoPathError
|
||||||
|
|||||||
+3
-1
@@ -523,6 +523,7 @@ async function openSettings(): Promise<void> {
|
|||||||
console.log(chalk.cyan(' Venice:'), hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set'));
|
console.log(chalk.cyan(' Venice:'), hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set'));
|
||||||
console.log(chalk.cyan(' Anthropic:'), hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set'));
|
console.log(chalk.cyan(' Anthropic:'), hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set'));
|
||||||
console.log(chalk.cyan(' OpenAI:'), hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set'));
|
console.log(chalk.cyan(' OpenAI:'), hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set'));
|
||||||
|
console.log(chalk.cyan(' DeepSeek:'), hasApiKey('deepseek') ? chalk.green('configured') : chalk.red('not set'));
|
||||||
console.log('');
|
console.log('');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -575,7 +576,7 @@ async function openSettings(): Promise<void> {
|
|||||||
type: 'list',
|
type: 'list',
|
||||||
name: 'keyProvider',
|
name: 'keyProvider',
|
||||||
message: 'Which provider?',
|
message: 'Which provider?',
|
||||||
choices: ['openrouter', 'venice', 'anthropic', 'openai'],
|
choices: ['openrouter', 'venice', 'anthropic', 'openai', 'deepseek'],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const { apiKey } = await inquirer.prompt([
|
const { apiKey } = await inquirer.prompt([
|
||||||
@@ -642,6 +643,7 @@ program
|
|||||||
[chalk.cyan('Venice API Key'), hasApiKey('venice') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
[chalk.cyan('Venice API Key'), hasApiKey('venice') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||||
[chalk.cyan('Anthropic API Key'), hasApiKey('anthropic') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
[chalk.cyan('Anthropic API Key'), hasApiKey('anthropic') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||||
[chalk.cyan('OpenAI API Key'), hasApiKey('openai') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
[chalk.cyan('OpenAI API Key'), hasApiKey('openai') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||||
|
[chalk.cyan('DeepSeek API Key'), hasApiKey('deepseek') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||||
[chalk.cyan('Config Path'), config.getConfigPath()],
|
[chalk.cyan('Config Path'), config.getConfigPath()],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+53
-3
@@ -11,7 +11,7 @@ import { join } from 'path';
|
|||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import type { LLMProvider, LLMConfig, FallbackEntry, OpsecLevel } from '../types/index.js';
|
import type { LLMProvider, LLMConfig, FallbackEntry, OpsecLevel } from '../types/index.js';
|
||||||
|
|
||||||
type ApiKeyProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'local';
|
type ApiKeyProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'local';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// CONFIGURATION SCHEMA
|
// CONFIGURATION SCHEMA
|
||||||
@@ -26,6 +26,7 @@ export interface TempestSettings {
|
|||||||
openai?: string;
|
openai?: string;
|
||||||
xai?: string;
|
xai?: string;
|
||||||
gemini?: string;
|
gemini?: string;
|
||||||
|
deepseek?: string;
|
||||||
litellm?: string;
|
litellm?: string;
|
||||||
local?: string;
|
local?: string;
|
||||||
};
|
};
|
||||||
@@ -78,6 +79,12 @@ export interface TempestSettings {
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// DeepSeek — OpenAI-compatible API
|
||||||
|
deepseek: {
|
||||||
|
baseUrl: string;
|
||||||
|
defaultModel: string;
|
||||||
|
};
|
||||||
|
|
||||||
// Codex CLI/account subscription backend
|
// Codex CLI/account subscription backend
|
||||||
codex: {
|
codex: {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -154,6 +161,11 @@ const DEFAULT_SETTINGS: TempestSettings = {
|
|||||||
defaultModel: 'gemini-2.5-flash',
|
defaultModel: 'gemini-2.5-flash',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deepseek: {
|
||||||
|
baseUrl: 'https://api.deepseek.com/v1',
|
||||||
|
defaultModel: 'deepseek-chat',
|
||||||
|
},
|
||||||
|
|
||||||
codex: {
|
codex: {
|
||||||
command: 'codex',
|
command: 'codex',
|
||||||
defaultModel: 'codex-default',
|
defaultModel: 'codex-default',
|
||||||
@@ -490,6 +502,24 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
|||||||
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast', 'tools'],
|
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast', 'tools'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
deepseek: [
|
||||||
|
{
|
||||||
|
id: 'deepseek-chat',
|
||||||
|
name: 'DeepSeek V3 (native)',
|
||||||
|
provider: 'DeepSeek',
|
||||||
|
contextWindow: 64000,
|
||||||
|
maxOutput: 8192,
|
||||||
|
capabilities: ['reasoning', 'code', 'analysis', 'tools'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek-reasoner',
|
||||||
|
name: 'DeepSeek R1 (native)',
|
||||||
|
provider: 'DeepSeek',
|
||||||
|
contextWindow: 64000,
|
||||||
|
maxOutput: 8192,
|
||||||
|
capabilities: ['reasoning', 'code', 'analysis', 'complex-tasks'],
|
||||||
|
},
|
||||||
|
],
|
||||||
local: [
|
local: [
|
||||||
{
|
{
|
||||||
id: 'local-model',
|
id: 'local-model',
|
||||||
@@ -568,7 +598,7 @@ class ConfigManager {
|
|||||||
const envContent = readFileSync(envPath, 'utf-8');
|
const envContent = readFileSync(envPath, 'utf-8');
|
||||||
const lines = envContent.split('\n');
|
const lines = envContent.split('\n');
|
||||||
// Validation variables added
|
// Validation variables added
|
||||||
const VALID_PROVIDERS = ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'litellm', 'local'];
|
const VALID_PROVIDERS = ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'litellm', 'deepseek', 'local'];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -653,6 +683,7 @@ class ConfigManager {
|
|||||||
openai: 'OPENAI_API_KEY',
|
openai: 'OPENAI_API_KEY',
|
||||||
xai: 'XAI_API_KEY',
|
xai: 'XAI_API_KEY',
|
||||||
gemini: 'GEMINI_API_KEY',
|
gemini: 'GEMINI_API_KEY',
|
||||||
|
deepseek: 'DEEPSEEK_API_KEY',
|
||||||
litellm: 'LITELLM_API_KEY',
|
litellm: 'LITELLM_API_KEY',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -709,6 +740,7 @@ class ConfigManager {
|
|||||||
if (this.hasApiKey('xai')) providers.push('xai');
|
if (this.hasApiKey('xai')) providers.push('xai');
|
||||||
if (this.hasLiteLLMProxy()) providers.push('litellm');
|
if (this.hasLiteLLMProxy()) providers.push('litellm');
|
||||||
if (this.hasApiKey('gemini')) providers.push('gemini');
|
if (this.hasApiKey('gemini')) providers.push('gemini');
|
||||||
|
if (this.hasApiKey('deepseek')) providers.push('deepseek');
|
||||||
|
|
||||||
// Codex uses the local Codex CLI/account auth instead of API-key storage.
|
// Codex uses the local Codex CLI/account auth instead of API-key storage.
|
||||||
providers.push('codex');
|
providers.push('codex');
|
||||||
@@ -776,6 +808,12 @@ class ConfigManager {
|
|||||||
baseUrl = this.config.get('gemini').baseUrl;
|
baseUrl = this.config.get('gemini').baseUrl;
|
||||||
actualModel = model || this.config.get('gemini').defaultModel;
|
actualModel = model || this.config.get('gemini').defaultModel;
|
||||||
break;
|
break;
|
||||||
|
case 'deepseek':
|
||||||
|
// DeepSeek native API is OpenAI-compatible.
|
||||||
|
apiKey = this.getApiKey('deepseek');
|
||||||
|
baseUrl = this.config.get('deepseek').baseUrl;
|
||||||
|
actualModel = model || this.config.get('deepseek').defaultModel;
|
||||||
|
break;
|
||||||
case 'codex':
|
case 'codex':
|
||||||
actualModel = model || this.config.get('codex').defaultModel;
|
actualModel = model || this.config.get('codex').defaultModel;
|
||||||
break;
|
break;
|
||||||
@@ -825,7 +863,7 @@ class ConfigManager {
|
|||||||
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
|
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
|
||||||
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
|
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
|
||||||
const chain: FallbackEntry[] = [];
|
const chain: FallbackEntry[] = [];
|
||||||
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm') => {
|
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek') => {
|
||||||
if (p === primary) return;
|
if (p === primary) return;
|
||||||
if (p === 'litellm' ? !this.hasLiteLLMProxy() : !this.hasApiKey(p)) return;
|
if (p === 'litellm' ? !this.hasLiteLLMProxy() : !this.hasApiKey(p)) return;
|
||||||
chain.push({
|
chain.push({
|
||||||
@@ -842,6 +880,7 @@ class ConfigManager {
|
|||||||
add('openai');
|
add('openai');
|
||||||
add('xai');
|
add('xai');
|
||||||
add('gemini');
|
add('gemini');
|
||||||
|
add('deepseek');
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,6 +904,9 @@ class ConfigManager {
|
|||||||
case 'openai':
|
case 'openai':
|
||||||
this.config.set('defaultModel', this.config.get('openai').defaultModel);
|
this.config.set('defaultModel', this.config.get('openai').defaultModel);
|
||||||
break;
|
break;
|
||||||
|
case 'deepseek':
|
||||||
|
this.config.set('defaultModel', this.config.get('deepseek').defaultModel);
|
||||||
|
break;
|
||||||
case 'xai':
|
case 'xai':
|
||||||
this.config.set('defaultModel', this.config.get('xai').defaultModel);
|
this.config.set('defaultModel', this.config.get('xai').defaultModel);
|
||||||
break;
|
break;
|
||||||
@@ -900,6 +942,9 @@ class ConfigManager {
|
|||||||
case 'openai':
|
case 'openai':
|
||||||
this.config.set('openai', { ...this.config.get('openai'), defaultModel: model });
|
this.config.set('openai', { ...this.config.get('openai'), defaultModel: model });
|
||||||
break;
|
break;
|
||||||
|
case 'deepseek':
|
||||||
|
this.config.set('deepseek', { ...this.config.get('deepseek'), defaultModel: model });
|
||||||
|
break;
|
||||||
case 'xai':
|
case 'xai':
|
||||||
this.config.set('xai', { ...this.config.get('xai'), defaultModel: model });
|
this.config.set('xai', { ...this.config.get('xai'), defaultModel: model });
|
||||||
break;
|
break;
|
||||||
@@ -948,6 +993,7 @@ class ConfigManager {
|
|||||||
openai: settings.apiKeys.openai ? '***REDACTED***' : undefined,
|
openai: settings.apiKeys.openai ? '***REDACTED***' : undefined,
|
||||||
xai: settings.apiKeys.xai ? '***REDACTED***' : undefined,
|
xai: settings.apiKeys.xai ? '***REDACTED***' : undefined,
|
||||||
gemini: settings.apiKeys.gemini ? '***REDACTED***' : undefined,
|
gemini: settings.apiKeys.gemini ? '***REDACTED***' : undefined,
|
||||||
|
deepseek: settings.apiKeys.deepseek ? '***REDACTED***' : undefined,
|
||||||
litellm: settings.apiKeys.litellm ? '***REDACTED***' : undefined,
|
litellm: settings.apiKeys.litellm ? '***REDACTED***' : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -992,6 +1038,10 @@ LITELLM_MODEL=gpt-4o
|
|||||||
# Get your key at: https://aistudio.google.com/apikey
|
# Get your key at: https://aistudio.google.com/apikey
|
||||||
GEMINI_API_KEY=
|
GEMINI_API_KEY=
|
||||||
|
|
||||||
|
# DeepSeek API Key (direct DeepSeek API via its OpenAI-compatible endpoint)
|
||||||
|
# Get your key at: https://platform.deepseek.com/api_keys
|
||||||
|
DEEPSEEK_API_KEY=
|
||||||
|
|
||||||
# Local model (Ollama / LM Studio / vLLM / llama.cpp, or any OpenAI-compatible server)
|
# Local model (Ollama / LM Studio / vLLM / llama.cpp, or any OpenAI-compatible server)
|
||||||
# Point TEMPEST_LOCAL_BASE_URL at the server root (Ollama default shown below).
|
# Point TEMPEST_LOCAL_BASE_URL at the server root (Ollama default shown below).
|
||||||
# For an OpenAI-compatible server, use a versioned path: LM Studio :1234/v1,
|
# For an OpenAI-compatible server, use a versioned path: LM Studio :1234/v1,
|
||||||
|
|||||||
+12
-1
@@ -1237,6 +1237,8 @@ export class LLMBackbone extends EventEmitter<LLMEvents> {
|
|||||||
return new OpenAIAdapter(config); // xAI (Grok Build / grok-*) is OpenAI-compatible
|
return new OpenAIAdapter(config); // xAI (Grok Build / grok-*) is OpenAI-compatible
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return new OpenAIAdapter(config); // Gemini via Google's OpenAI-compatible endpoint (baseUrl ends in /v1beta/openai)
|
return new OpenAIAdapter(config); // Gemini via Google's OpenAI-compatible endpoint (baseUrl ends in /v1beta/openai)
|
||||||
|
case 'deepseek':
|
||||||
|
return new OpenAIAdapter(config); // DeepSeek native API is OpenAI-compatible
|
||||||
case 'codex':
|
case 'codex':
|
||||||
return new CodexAdapter(config);
|
return new CodexAdapter(config);
|
||||||
case 'mock':
|
case 'mock':
|
||||||
@@ -1511,6 +1513,12 @@ export function createOpenAIBackbone(apiKey?: string, model?: string): LLMBackbo
|
|||||||
return new LLMBackbone(llmConfig);
|
return new LLMBackbone(llmConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDeepSeekBackbone(apiKey?: string, model?: string): LLMBackbone {
|
||||||
|
const llmConfig = config.getLLMConfig('deepseek', model);
|
||||||
|
if (apiKey) llmConfig.apiKey = apiKey;
|
||||||
|
return new LLMBackbone(llmConfig);
|
||||||
|
}
|
||||||
|
|
||||||
export function createLiteLLMBackbone(apiKey?: string, model?: string, baseUrl?: string): LLMBackbone {
|
export function createLiteLLMBackbone(apiKey?: string, model?: string, baseUrl?: string): LLMBackbone {
|
||||||
const llmConfig = config.getLLMConfig('litellm', model);
|
const llmConfig = config.getLLMConfig('litellm', model);
|
||||||
if (apiKey) llmConfig.apiKey = apiKey;
|
if (apiKey) llmConfig.apiKey = apiKey;
|
||||||
@@ -1541,7 +1549,7 @@ export function createLocalBackbone(model?: string, baseUrl?: string): LLMBackbo
|
|||||||
* Create the best available backbone based on configured API keys
|
* Create the best available backbone based on configured API keys
|
||||||
*/
|
*/
|
||||||
export function createBestAvailableBackbone(): LLMBackbone {
|
export function createBestAvailableBackbone(): LLMBackbone {
|
||||||
// Priority: OpenRouter > Venice > LiteLLM > Anthropic > OpenAI > Local > Mock
|
// Priority: OpenRouter > Venice > LiteLLM > Anthropic > OpenAI > DeepSeek > Local > Mock
|
||||||
const providers = config.getConfiguredProviders();
|
const providers = config.getConfiguredProviders();
|
||||||
|
|
||||||
if (providers.includes('openrouter')) {
|
if (providers.includes('openrouter')) {
|
||||||
@@ -1559,6 +1567,9 @@ export function createBestAvailableBackbone(): LLMBackbone {
|
|||||||
if (providers.includes('openai')) {
|
if (providers.includes('openai')) {
|
||||||
return createOpenAIBackbone();
|
return createOpenAIBackbone();
|
||||||
}
|
}
|
||||||
|
if (providers.includes('deepseek')) {
|
||||||
|
return createDeepSeekBackbone();
|
||||||
|
}
|
||||||
|
|
||||||
// Default to mock if no API keys configured
|
// Default to mock if no API keys configured
|
||||||
console.warn('No API keys configured. Using mock provider.');
|
console.warn('No API keys configured. Using mock provider.');
|
||||||
|
|||||||
+140
-37
@@ -23,9 +23,10 @@ import {
|
|||||||
import { DecompositionOrchestrator } from '../orchestration/index.js';
|
import { DecompositionOrchestrator } from '../orchestration/index.js';
|
||||||
import type { DecompositionConfig } from '../orchestration/index.js';
|
import type { DecompositionConfig } from '../orchestration/index.js';
|
||||||
import type { LLMProvider } from '../types/index.js';
|
import type { LLMProvider } from '../types/index.js';
|
||||||
import { realpathSync, statSync } from 'node:fs';
|
import { mkdtempSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
import { homedir, tmpdir } from 'node:os';
|
||||||
|
import { join as joinPath, isAbsolute, relative, resolve, sep } from 'node:path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default worker-source token budget when building a source blob directly from a
|
* Default worker-source token budget when building a source blob directly from a
|
||||||
@@ -57,6 +58,100 @@ export class RepoPathError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RepoCloneError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'RepoCloneError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedRepoSource {
|
||||||
|
repoPath: string;
|
||||||
|
source: 'local' | 'github';
|
||||||
|
cleanup(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGitHubHttpsRepoUrl(input: string): boolean {
|
||||||
|
try {
|
||||||
|
const url = new URL(input);
|
||||||
|
return url.protocol === 'https:' &&
|
||||||
|
url.hostname.toLowerCase() === 'github.com' &&
|
||||||
|
/^\/[^/]+\/[^/]+(?:\.git)?\/?$/.test(url.pathname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGitHubRepoToken(): string | undefined {
|
||||||
|
return process.env.T3MP3ST_GITHUB_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneGitHubRepoForAnalysis(repoUrl: string): ResolvedRepoSource {
|
||||||
|
if (!isGitHubHttpsRepoUrl(repoUrl)) {
|
||||||
|
throw new RepoCloneError('repoUrl must be an https://github.com/<owner>/<repo> URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = getGitHubRepoToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new RepoCloneError('private GitHub repo scans require T3MP3ST_GITHUB_TOKEN or GITHUB_TOKEN');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRoot = mkdtempSync(joinPath(tmpdir(), 't3mp3st-repo-'));
|
||||||
|
const cloneDir = joinPath(tempRoot, 'repo');
|
||||||
|
const askPassPath = joinPath(tempRoot, 'git-askpass.sh');
|
||||||
|
try {
|
||||||
|
writeFileSync(
|
||||||
|
askPassPath,
|
||||||
|
[
|
||||||
|
'#!/bin/sh',
|
||||||
|
'case "$1" in',
|
||||||
|
"*Username*) printf '%s\\n' 'x-access-token' ;;",
|
||||||
|
'*) printf "%s\\n" "$T3MP3ST_GIT_TOKEN" ;;',
|
||||||
|
'esac',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
{ mode: 0o700 },
|
||||||
|
);
|
||||||
|
execFileSync('git', [
|
||||||
|
'-c', 'credential.helper=',
|
||||||
|
'clone',
|
||||||
|
'--depth', '1',
|
||||||
|
'--filter=blob:none',
|
||||||
|
repoUrl,
|
||||||
|
cloneDir,
|
||||||
|
], {
|
||||||
|
stdio: ['ignore', 'ignore', 'pipe'],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GIT_ASKPASS: askPassPath,
|
||||||
|
GIT_TERMINAL_PROMPT: '0',
|
||||||
|
T3MP3ST_GIT_TOKEN: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
repoPath: realpathSync(cloneDir),
|
||||||
|
source: 'github',
|
||||||
|
cleanup: () => { rmSync(tempRoot, { recursive: true, force: true }); },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
const message = error instanceof Error ? error.message.replaceAll(token, '[REDACTED]') : 'git clone failed';
|
||||||
|
throw new RepoCloneError(`GitHub repo clone failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRepoSourceForAnalysis(input: unknown): ResolvedRepoSource {
|
||||||
|
if (typeof input === 'string' && isGitHubHttpsRepoUrl(input.trim())) {
|
||||||
|
return cloneGitHubRepoForAnalysis(input.trim());
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
repoPath: resolveContainedRepoPath(input),
|
||||||
|
source: 'local',
|
||||||
|
cleanup: () => { /* local repos are caller-owned; nothing to remove */ },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* B-03 — path containment for the caller-supplied repoPath.
|
* B-03 — path containment for the caller-supplied repoPath.
|
||||||
*
|
*
|
||||||
@@ -114,27 +209,31 @@ export function resolveContainedRepoPath(input: unknown): string {
|
|||||||
* suitable for handing to `TempestCommand.setWhiteboxSource` (or any consumer
|
* suitable for handing to `TempestCommand.setWhiteboxSource` (or any consumer
|
||||||
* that wants the highest-priority code first).
|
* that wants the highest-priority code first).
|
||||||
*
|
*
|
||||||
* @param repoPath Absolute path to a LOCAL repo you own.
|
* @param repoPath Absolute path to a LOCAL repo you own, or a GitHub HTTPS repo URL with token auth.
|
||||||
* @param tokenBudget Packing budget in tokens (default 24000).
|
* @param tokenBudget Packing budget in tokens (default 24000).
|
||||||
*/
|
*/
|
||||||
export function ingestRepoToSourceContext(
|
export function ingestRepoToSourceContext(
|
||||||
repoPath: string,
|
repoPath: string,
|
||||||
tokenBudget = 24000,
|
tokenBudget = 24000,
|
||||||
): RepoSourceContext {
|
): RepoSourceContext {
|
||||||
const safePath = resolveContainedRepoPath(repoPath);
|
const source = resolveRepoSourceForAnalysis(repoPath);
|
||||||
const result = ingestRepository(createPythonIngestConfig(safePath));
|
try {
|
||||||
const packed = packAnalysisUnits(result.analysisUnits, tokenBudget);
|
const result = ingestRepository(createPythonIngestConfig(source.repoPath));
|
||||||
return {
|
const packed = packAnalysisUnits(result.analysisUnits, tokenBudget);
|
||||||
sourceContext: packed.text,
|
return {
|
||||||
stats: result.stats,
|
sourceContext: packed.text,
|
||||||
includedUnits: packed.includedUnits.length,
|
stats: result.stats,
|
||||||
droppedUnits: packed.droppedUnits.length,
|
includedUnits: packed.includedUnits.length,
|
||||||
};
|
droppedUnits: packed.droppedUnits.length,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
source.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for a full white-box decomposition analysis run. */
|
/** Options for a full white-box decomposition analysis run. */
|
||||||
export interface WhiteboxAnalysisOptions {
|
export interface WhiteboxAnalysisOptions {
|
||||||
/** Absolute path to a LOCAL repo you own. */
|
/** Absolute path to a LOCAL repo you own, or a GitHub HTTPS repo URL with token auth. */
|
||||||
repoPath: string;
|
repoPath: string;
|
||||||
/** The offensive objective — only the orchestrator model sees this. */
|
/** The offensive objective — only the orchestrator model sees this. */
|
||||||
objective: string;
|
objective: string;
|
||||||
@@ -199,41 +298,45 @@ export async function runWhiteboxAnalysis(
|
|||||||
opts: WhiteboxAnalysisOptions,
|
opts: WhiteboxAnalysisOptions,
|
||||||
): Promise<WhiteboxAnalysisResult> {
|
): Promise<WhiteboxAnalysisResult> {
|
||||||
const { repoPath, objective, maxRounds } = opts;
|
const { repoPath, objective, maxRounds } = opts;
|
||||||
const safePath = resolveContainedRepoPath(repoPath);
|
const source = resolveRepoSourceForAnalysis(repoPath);
|
||||||
|
|
||||||
// 1) ingest LOCAL source, security-rank the blocks
|
// 1) ingest source, security-rank the blocks
|
||||||
const result = ingestRepository(createPythonIngestConfig(safePath));
|
try {
|
||||||
|
const result = ingestRepository(createPythonIngestConfig(source.repoPath));
|
||||||
|
|
||||||
// 2) pack into a generous source view (orchestrator re-packs per-worker itself)
|
// 2) pack into a generous source view (orchestrator re-packs per-worker itself)
|
||||||
const packed = packAnalysisUnits(result.analysisUnits, DEFAULT_ANALYSIS_SOURCE_BUDGET);
|
const packed = packAnalysisUnits(result.analysisUnits, DEFAULT_ANALYSIS_SOURCE_BUDGET);
|
||||||
const sourceContext = packed.text;
|
const sourceContext = packed.text;
|
||||||
|
|
||||||
// Fail loud ONLY when there is genuinely NO source to analyze: 0 ingestable source files
|
// Fail loud ONLY when there is genuinely NO source to analyze: 0 ingestable source files
|
||||||
// (non-Python or empty repo). A valid Python repo that has files but no def/class (a
|
// (non-Python or empty repo). A valid Python repo that has files but no def/class (a
|
||||||
// module-level-only script) yields 0 blocks -> empty packed text; that used to run the
|
// module-level-only script) yields 0 blocks -> empty packed text; that used to run the
|
||||||
// orchestrator, so we do NOT regress it into a 500 — gate the throw on the file count, not
|
// orchestrator, so we do NOT regress it into a 500 — gate the throw on the file count, not
|
||||||
// on empty packed text, and let a files>0 repo proceed as before.
|
// on empty packed text, and let a files>0 repo proceed as before.
|
||||||
if (result.stats.files === 0) {
|
if (result.stats.files === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'white-box analysis has no analyzable source: the repo has 0 ingestable source files ' +
|
'white-box analysis has no analyzable source: the repo has 0 ingestable source files ' +
|
||||||
'(non-Python, or empty). Point repoPath at a Python codebase.',
|
'(non-Python, or empty). Point repoPath at a Python codebase.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) construct the orchestrator with a sane default config (mirrors decompose.mjs)
|
// 3) construct the orchestrator with a sane default config (mirrors decompose.mjs)
|
||||||
const decompConfig = buildDecompositionConfig();
|
const decompConfig = buildDecompositionConfig();
|
||||||
if (typeof maxRounds === 'number' && maxRounds > 0) {
|
if (typeof maxRounds === 'number' && maxRounds > 0) {
|
||||||
decompConfig.maxRounds = maxRounds;
|
decompConfig.maxRounds = maxRounds;
|
||||||
}
|
}
|
||||||
const orch = new DecompositionOrchestrator(decompConfig);
|
const orch = new DecompositionOrchestrator(decompConfig);
|
||||||
|
|
||||||
// 4) run the decomposition loop (LLM calls happen HERE)
|
// 4) run the decomposition loop (LLM calls happen HERE)
|
||||||
const decomposition = await orch.run(objective, sourceContext);
|
const decomposition = await orch.run(objective, sourceContext);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ingestStats: result.stats,
|
ingestStats: result.stats,
|
||||||
entryPoints: result.entryPoints,
|
entryPoints: result.entryPoints,
|
||||||
unitCount: result.analysisUnits.length,
|
unitCount: result.analysisUnits.length,
|
||||||
decomposition,
|
decomposition,
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
source.cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+17
-37
@@ -30,7 +30,7 @@ import { OPERATOR_SYSTEM_PROMPTS, PLINIAN_OPERATOR_DOCTRINE, THE_FIXER_SYSTEM_PR
|
|||||||
import { createTargetFromUrl, createTargetFromIP } from './target/index.js';
|
import { createTargetFromUrl, createTargetFromIP } from './target/index.js';
|
||||||
import type { OperatorArchetype } from './types/index.js';
|
import type { OperatorArchetype } from './types/index.js';
|
||||||
import { listOperatorPrompts, setOperatorOverride, resetOperatorOverride, type OperatorOverride } from './operators/index.js';
|
import { listOperatorPrompts, setOperatorOverride, resetOperatorOverride, type OperatorOverride } from './operators/index.js';
|
||||||
import { ingestRepoToSourceContext, runWhiteboxAnalysis, resolveContainedRepoPath, RepoPathError } from './recon/whitebox.js';
|
import { ingestRepoToSourceContext, runWhiteboxAnalysis, resolveRepoSourceForAnalysis, RepoCloneError, RepoPathError } from './recon/whitebox.js';
|
||||||
import { redactCredential } from './evidence/index.js';
|
import { redactCredential } from './evidence/index.js';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -6215,27 +6215,15 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
|
|||||||
if (!guard.allowed) { blockForApproval(res, guard); return; }
|
if (!guard.allowed) { blockForApproval(res, guard); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === 'local-agent') {
|
let repoSource: ReturnType<typeof resolveRepoSourceForAnalysis> | undefined;
|
||||||
const localAgent = await requireLiveLocalAgent(model);
|
|
||||||
if (!localAgent.ok) {
|
|
||||||
res.status(503).json({ error: localAgent.error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// B-03: if an OPTIONAL white-box repoPath was supplied, containment-check it HERE
|
|
||||||
// (canonicalize + confine to the allowed root) before we build/start the command,
|
|
||||||
// so a bad/escaping path 400s cleanly instead of half-starting a run.
|
|
||||||
let containedRepoPath: string | undefined;
|
|
||||||
if (typeof repoPath === 'string' && repoPath.trim()) {
|
if (typeof repoPath === 'string' && repoPath.trim()) {
|
||||||
try {
|
try {
|
||||||
containedRepoPath = resolveContainedRepoPath(repoPath);
|
repoSource = resolveRepoSourceForAnalysis(repoPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof RepoPathError) { res.status(400).json({ error: `repoPath rejected: ${e.message}` }); return; }
|
if (e instanceof RepoPathError) { res.status(400).json({ error: `repoPath rejected: ${e.message}` }); return; }
|
||||||
// Guarantee a response even for an unexpected error — a bare `throw` in this async
|
if (e instanceof RepoCloneError) { res.status(400).json({ error: e.message }); return; }
|
||||||
// handler becomes an unhandledRejection (Express 4 won't route it), hanging the client.
|
console.error('[T3MP3ST] repo source validation error:', e);
|
||||||
console.error('[T3MP3ST] repoPath validation error:', e);
|
res.status(500).json({ error: 'repo source validation failed' });
|
||||||
res.status(500).json({ error: 'repoPath validation failed' });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6277,13 +6265,13 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
|
|||||||
// exists on disk, ingest + security-rank it and feed the packed source into
|
// exists on disk, ingest + security-rank it and feed the packed source into
|
||||||
// the command before it starts, so operators analyze real source you own
|
// the command before it starts, so operators analyze real source you own
|
||||||
// rather than probing a black box. Reads LOCAL disk only — no network target.
|
// rather than probing a black box. Reads LOCAL disk only — no network target.
|
||||||
let whitebox: { includedUnits: number; droppedUnits: number; stats: unknown } | undefined;
|
let whitebox: { includedUnits: number; droppedUnits: number; stats: unknown; source: 'local' | 'github' } | undefined;
|
||||||
if (containedRepoPath) {
|
if (repoSource) {
|
||||||
const wb = ingestRepoToSourceContext(containedRepoPath);
|
const wb = ingestRepoToSourceContext(repoSource.repoPath);
|
||||||
// Only feed a NON-empty source (0 ingestable units → don't overwrite the operators'
|
// Only feed a NON-empty source (0 ingestable units → don't overwrite the operators'
|
||||||
// black-box view with an empty blob; the includedUnits:0 in the response signals it).
|
// black-box view with an empty blob; the includedUnits:0 in the response signals it).
|
||||||
if (wb.sourceContext.trim()) cmd.setWhiteboxSource(wb.sourceContext);
|
if (wb.sourceContext.trim()) cmd.setWhiteboxSource(wb.sourceContext);
|
||||||
whitebox = { includedUnits: wb.includedUnits, droppedUnits: wb.droppedUnits, stats: wb.stats };
|
whitebox = { includedUnits: wb.includedUnits, droppedUnits: wb.droppedUnits, stats: wb.stats, source: repoSource.source };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the command loop (auto-creates mission, auto-dispatches tasks)
|
// Start the command loop (auto-creates mission, auto-dispatches tasks)
|
||||||
@@ -6307,6 +6295,8 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[T3MP3ST] Mission start failed:', error);
|
console.error('[T3MP3ST] Mission start failed:', error);
|
||||||
res.status(500).json({ error: error.message || 'Failed to start mission' });
|
res.status(500).json({ error: error.message || 'Failed to start mission' });
|
||||||
|
} finally {
|
||||||
|
repoSource?.cleanup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6330,29 +6320,19 @@ app.post('/api/whitebox/analyze', async (req: Request, res: Response): Promise<v
|
|||||||
res.status(400).json({ error: 'objective required' });
|
res.status(400).json({ error: 'objective required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// B-03: containment-check the operator-supplied repoPath (canonicalize + confine
|
|
||||||
// to the allowed root) BEFORE any disk read; a bad/escaping path 400s, not 500s.
|
|
||||||
let safeRepoPath: string;
|
|
||||||
try {
|
|
||||||
safeRepoPath = resolveContainedRepoPath(repoPath);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof RepoPathError) { res.status(400).json({ error: e.message }); return; }
|
|
||||||
// Guarantee a response even for an unexpected error — a bare `throw` in this async
|
|
||||||
// handler becomes an unhandledRejection (Express 4 won't route it), hanging the client.
|
|
||||||
console.error('[T3MP3ST] repoPath validation error:', e);
|
|
||||||
res.status(500).json({ error: 'repoPath validation failed' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runWhiteboxAnalysis({
|
const result = await runWhiteboxAnalysis({
|
||||||
repoPath: safeRepoPath,
|
repoPath: typeof repoPath === 'string' ? repoPath : '',
|
||||||
objective,
|
objective,
|
||||||
maxRounds: typeof maxRounds === 'number' ? maxRounds : undefined,
|
maxRounds: typeof maxRounds === 'number' ? maxRounds : undefined,
|
||||||
});
|
});
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[T3MP3ST] White-box analysis failed:', error);
|
console.error('[T3MP3ST] White-box analysis failed:', error);
|
||||||
|
if (error instanceof RepoPathError || error instanceof RepoCloneError) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
res.status(500).json({ error: error?.message || 'White-box analysis failed' });
|
res.status(500).json({ error: error?.message || 'White-box analysis failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+63
-1
@@ -194,6 +194,53 @@ async function setupAnthropicKey(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupDeepSeekKey(): Promise<boolean> {
|
||||||
|
console.log('');
|
||||||
|
showInfo('DeepSeek provides OpenAI-compatible chat and reasoning models.');
|
||||||
|
showInfo('Get your API key at: ' + chalk.underline('https://platform.deepseek.com/api_keys'));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const { apiKey } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'apiKey',
|
||||||
|
message: 'Enter your DeepSeek API key:',
|
||||||
|
mask: '*',
|
||||||
|
validate: (input: string) => {
|
||||||
|
if (!input || input.length < 10) {
|
||||||
|
return 'Please enter a valid API key';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const spinner = ora('Testing API key...').start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const llm = new LLMBackbone({
|
||||||
|
provider: 'deepseek',
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
baseUrl: 'https://api.deepseek.com/v1',
|
||||||
|
apiKey,
|
||||||
|
maxTokens: 10,
|
||||||
|
temperature: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await llm.prompt('Hello', undefined, { maxTokens: 10 });
|
||||||
|
spinner.succeed('API key is valid!');
|
||||||
|
|
||||||
|
setApiKey('deepseek', apiKey);
|
||||||
|
showSuccess('DeepSeek API key saved successfully!');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
spinner.fail('API key validation failed');
|
||||||
|
showError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function setupOpenAIKey(): Promise<boolean> {
|
async function setupOpenAIKey(): Promise<boolean> {
|
||||||
console.log('');
|
console.log('');
|
||||||
showInfo('OpenAI provides access to GPT models.');
|
showInfo('OpenAI provides access to GPT models.');
|
||||||
@@ -368,6 +415,7 @@ You can use a local model with no API key, or add a key from one of these provid
|
|||||||
• ${chalk.cyan('OpenRouter')} (recommended) - Access multiple models
|
• ${chalk.cyan('OpenRouter')} (recommended) - Access multiple models
|
||||||
• ${chalk.cyan('Anthropic')} - Direct Claude access
|
• ${chalk.cyan('Anthropic')} - Direct Claude access
|
||||||
• ${chalk.cyan('OpenAI')} - GPT models
|
• ${chalk.cyan('OpenAI')} - GPT models
|
||||||
|
• ${chalk.cyan('DeepSeek')} - OpenAI-compatible chat/reasoning models
|
||||||
• ${chalk.cyan('LiteLLM')} - AI gateway proxy (100+ providers)
|
• ${chalk.cyan('LiteLLM')} - AI gateway proxy (100+ providers)
|
||||||
|
|
||||||
The setup will guide you through:
|
The setup will guide you through:
|
||||||
@@ -381,13 +429,17 @@ The setup will guide you through:
|
|||||||
const hasOpenRouter = hasApiKey('openrouter');
|
const hasOpenRouter = hasApiKey('openrouter');
|
||||||
const hasAnthropic = hasApiKey('anthropic');
|
const hasAnthropic = hasApiKey('anthropic');
|
||||||
const hasOpenAI = hasApiKey('openai');
|
const hasOpenAI = hasApiKey('openai');
|
||||||
|
const hasDeepSeek = hasApiKey('deepseek');
|
||||||
|
const hasLiteLLM = config.hasLiteLLMProxy();
|
||||||
|
|
||||||
if (hasOpenRouter || hasAnthropic || hasOpenAI) {
|
if (hasOpenRouter || hasAnthropic || hasOpenAI || hasDeepSeek || hasLiteLLM) {
|
||||||
console.log('');
|
console.log('');
|
||||||
showInfo('Existing API keys detected:');
|
showInfo('Existing API keys detected:');
|
||||||
if (hasOpenRouter) showSuccess(' OpenRouter: configured');
|
if (hasOpenRouter) showSuccess(' OpenRouter: configured');
|
||||||
if (hasAnthropic) showSuccess(' Anthropic: configured');
|
if (hasAnthropic) showSuccess(' Anthropic: configured');
|
||||||
if (hasOpenAI) showSuccess(' OpenAI: configured');
|
if (hasOpenAI) showSuccess(' OpenAI: configured');
|
||||||
|
if (hasDeepSeek) showSuccess(' DeepSeek: configured');
|
||||||
|
if (hasLiteLLM) showSuccess(' LiteLLM Proxy: configured');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { action } = await inquirer.prompt([
|
const { action } = await inquirer.prompt([
|
||||||
@@ -467,6 +519,10 @@ async function setupApiKeys(): Promise<void> {
|
|||||||
name: `OpenAI ${hasApiKey('openai') ? chalk.green('(configured)') : ''}`,
|
name: `OpenAI ${hasApiKey('openai') ? chalk.green('(configured)') : ''}`,
|
||||||
value: 'openai',
|
value: 'openai',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: `DeepSeek ${hasApiKey('deepseek') ? chalk.green('(configured)') : ''}`,
|
||||||
|
value: 'deepseek',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: `LiteLLM Proxy ${config.hasLiteLLMProxy() ? chalk.green('(configured)') : chalk.gray('(100+ providers via gateway)')}`,
|
name: `LiteLLM Proxy ${config.hasLiteLLMProxy() ? chalk.green('(configured)') : chalk.gray('(100+ providers via gateway)')}`,
|
||||||
value: 'litellm',
|
value: 'litellm',
|
||||||
@@ -489,6 +545,9 @@ async function setupApiKeys(): Promise<void> {
|
|||||||
case 'openai':
|
case 'openai':
|
||||||
await setupOpenAIKey();
|
await setupOpenAIKey();
|
||||||
break;
|
break;
|
||||||
|
case 'deepseek':
|
||||||
|
await setupDeepSeekKey();
|
||||||
|
break;
|
||||||
case 'litellm':
|
case 'litellm':
|
||||||
await setupLiteLLMProxy();
|
await setupLiteLLMProxy();
|
||||||
break;
|
break;
|
||||||
@@ -504,6 +563,7 @@ async function setupProvider(): Promise<void> {
|
|||||||
if (hasApiKey('venice')) configuredProviders.push({ name: 'Venice', value: 'venice' });
|
if (hasApiKey('venice')) configuredProviders.push({ name: 'Venice', value: 'venice' });
|
||||||
if (hasApiKey('anthropic')) configuredProviders.push({ name: 'Anthropic', value: 'anthropic' });
|
if (hasApiKey('anthropic')) configuredProviders.push({ name: 'Anthropic', value: 'anthropic' });
|
||||||
if (hasApiKey('openai')) configuredProviders.push({ name: 'OpenAI', value: 'openai' });
|
if (hasApiKey('openai')) configuredProviders.push({ name: 'OpenAI', value: 'openai' });
|
||||||
|
if (hasApiKey('deepseek')) configuredProviders.push({ name: 'DeepSeek', value: 'deepseek' });
|
||||||
if (config.hasLiteLLMProxy()) configuredProviders.push({ name: 'LiteLLM Proxy', value: 'litellm' });
|
if (config.hasLiteLLMProxy()) configuredProviders.push({ name: 'LiteLLM Proxy', value: 'litellm' });
|
||||||
|
|
||||||
const { provider } = await inquirer.prompt([
|
const { provider } = await inquirer.prompt([
|
||||||
@@ -535,6 +595,8 @@ function viewConfiguration(): void {
|
|||||||
console.log(' Venice: ' + (hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set')));
|
console.log(' Venice: ' + (hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set')));
|
||||||
console.log(' Anthropic: ' + (hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set')));
|
console.log(' Anthropic: ' + (hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set')));
|
||||||
console.log(' OpenAI: ' + (hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set')));
|
console.log(' OpenAI: ' + (hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set')));
|
||||||
|
console.log(' DeepSeek: ' + (hasApiKey('deepseek') ? chalk.green('configured') : chalk.red('not set')));
|
||||||
|
console.log(' LiteLLM Proxy: ' + (config.hasLiteLLMProxy() ? chalk.green('configured') : chalk.red('not set')));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.cyan(' Config Path: ') + config.getConfigPath());
|
console.log(chalk.cyan(' Config Path: ') + config.getConfigPath());
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
// LLM CONFIGURATION
|
// LLM CONFIGURATION
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'codex' | 'mock' | 'local' | 'local-agent';
|
export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'codex' | 'mock' | 'local' | 'local-agent';
|
||||||
|
|
||||||
export interface LLMConfig {
|
export interface LLMConfig {
|
||||||
provider: LLMProvider;
|
provider: LLMProvider;
|
||||||
|
|||||||
Reference in new issue
Block a user