* feat(llm): add Hugging Face provider + selectable local Claude Code models Adds two operator-selectable LLM backbones, wired through both the backend and the War Room UI. Hugging Face (OpenAI-compatible Inference Providers router): - New `huggingface` provider reusing OpenAIAdapter (router.huggingface.co/v1) - HF_TOKEN / HUGGINGFACE_API_KEY / HUGGINGFACE_TOKEN env resolution, config, setup wizard, CLI, factory, best-available, and .env.example - Settings key card + 4 inference models in the picker + direct client routing - Tests: src/__tests__/huggingface-provider.test.ts Local Claude from Claude Code: - Explicit "active backbone" pin in Settings so a connected agent (Claude Code) can be chosen; a pin outranks stored API keys in both the client resolver and mission dispatch - Claude Code model picker (Account default / Opus / Sonnet / Haiku), threaded to the CLI's --model via an "agentId::model" encoding parsed by LocalAgentAdapter; requireLiveLocalAgent strips the suffix for connected-agent validation * fix(llm): address PR #127 review — self-contained keyless path + centralized pin/model routing Finding 1 — keyless mission path was not self-contained: - Add the `local-agent` case to ConfigManager.getLLMConfig so provider:'local-agent' resolves (keyless, agent id — optionally "agentId::model" — in `model`) instead of hitting the default "Unknown provider" throw. Finding 2 — inconsistent pin/model routing across dispatch surfaces: - Centralize backend agent resolution in window.t3mpDispatchAgent (single source of truth). It honors an explicit pin OVER a stored API key and encodes the picked local model as "agentId::model" so it is never dropped. - Route all three surfaces through it: mission dispatch, General (getGeneralConfig), and Admiral (_admBackbone). Previously General/Admiral only picked an agent when no key existed and sent the bare agent id, so a stored key overrode the pin and the selected Claude model was lost. Tests: - getLLMConfig('local-agent') regression coverage (self-contained keyless path) - LocalAgentAdapter "agentId::model" passthrough reaches the CLI --model flag - t3mpDispatchAgent behavior: pin>key precedence, model encoding, fallbacks (evaluated from the shipped docs/index.html) - centralization invariant: mission/General/Admiral all route through t3mpDispatchAgent - update the Op-Admiral keyless-routing static test to the centralized resolver --------- Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
80 lines
3.3 KiB
TypeScript
80 lines
3.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { readFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const configSource = readFileSync(join(process.cwd(), 'src/config/index.ts'), 'utf8');
|
|
const serverSource = readFileSync(join(process.cwd(), 'src/server.ts'), 'utf8');
|
|
const setupScript = readFileSync(join(process.cwd(), 'scripts/setup-api.sh'), 'utf8');
|
|
|
|
function sourceBlock(startMarker: string, endMarker: string): string {
|
|
const start = configSource.indexOf(startMarker);
|
|
expect(start, `missing start marker ${startMarker}`).toBeGreaterThanOrEqual(0);
|
|
const end = configSource.indexOf(endMarker, start);
|
|
expect(end, `missing end marker ${endMarker}`).toBeGreaterThan(start);
|
|
return configSource.slice(start, end);
|
|
}
|
|
|
|
function configLoadEnvBlock(): string {
|
|
return sourceBlock('private loadEnvVariables(): void', '\n /**\n * Get all settings');
|
|
}
|
|
|
|
function exportConfigBlock(): string {
|
|
return sourceBlock('exportConfig(filePath: string): void', '\n /**\n * Create a .env template file');
|
|
}
|
|
|
|
function envTemplateBlock(): string {
|
|
return sourceBlock('createEnvTemplate(filePath:', '\n writeFileSync(filePath, template);');
|
|
}
|
|
|
|
describe('API key environment handling hardening', () => {
|
|
it('ConfigManager does not implicitly read .env from the caller working directory', () => {
|
|
const block = configLoadEnvBlock();
|
|
|
|
expect(block).not.toContain("join(process.cwd(), '.env')");
|
|
expect(block).toContain("join(homedir(), '.t3mp3st', '.env')");
|
|
});
|
|
|
|
it('the API server does not re-enable caller-cwd dotenv loading', () => {
|
|
expect(serverSource).not.toMatch(/from ['"]dotenv['"]/);
|
|
expect(serverSource).not.toMatch(/\bdotenv\.config\s*\(/);
|
|
});
|
|
|
|
it('setup-api.sh writes the same T3MP3ST-owned env file that ConfigManager reads', () => {
|
|
const block = configLoadEnvBlock();
|
|
|
|
expect(setupScript).not.toMatch(/dirname "\$0"\).*\.\.\/\.env/);
|
|
expect(setupScript).toMatch(/\.t3mp3st/);
|
|
expect(setupScript).toMatch(/mkdir\s+-p\s+"\$\(dirname "\$ENV_FILE"\)"/);
|
|
expect(block).toContain("join(homedir(), '.t3mp3st', '.env')");
|
|
});
|
|
|
|
it('env template points users at the T3MP3ST-owned env file, not a caller-cwd .env', () => {
|
|
const block = envTemplateBlock();
|
|
|
|
expect(block).toContain('~/.t3mp3st/.env');
|
|
expect(block).not.toContain('Copy this file to .env');
|
|
});
|
|
|
|
it('exportConfig redacts every supported provider key slot', () => {
|
|
const block = exportConfigBlock();
|
|
|
|
for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'deepseek', 'huggingface', 'litellm']) {
|
|
expect(block).toContain(`${provider}: settings.apiKeys.${provider} ? '***REDACTED***' : undefined`);
|
|
}
|
|
});
|
|
|
|
it('ConfigManager uses env keys in-memory and does not persist imported env keys', () => {
|
|
const block = configLoadEnvBlock();
|
|
|
|
expect(block).not.toMatch(/setApiKey\(/);
|
|
expect(block).not.toMatch(/config\.set\(['"]apiKeys['"]/);
|
|
});
|
|
|
|
it('setup-api.sh never sources .env and reads API keys silently into a 0600 file', () => {
|
|
expect(setupScript).not.toMatch(/\bsource\s+["']?\$ENV_FILE/);
|
|
expect(setupScript).toMatch(/umask\s+077/);
|
|
expect(setupScript).toMatch(/read\s+-rsp\s+"Enter your OpenRouter API key:/);
|
|
expect(setupScript).toMatch(/chmod\s+600\s+"\$ENV_FILE"/);
|
|
});
|
|
});
|