Enlist agents you've already authed on this machine — no API keys needed. T3MP3ST detects each CLI and can drive it as an operator. Tick several and connect them all at once.
+
Enlist agents you've already authed on this machine — no API keys needed. T3MP3ST detects each CLI (Claude Code / Codex / Hermes) and drives it as your LLM backbone using its own login. Connect one, then hit ☆ Use to pin it as the ★ active backbone for missions and analysis — a pinned local model outranks any stored API key.
🔌 Connect Local Agents
@@ -5788,6 +5800,15 @@
Detecting local agents…
+
+
+ 🟠
+
+
Claude Code model
+
Which local Claude model to run (passed to claude --model)
+
+
+
@@ -7230,6 +7251,11 @@ Correlating findings across agents, identifying patterns, producing actionable i
{ id: 'deepseek/deepseek-v4-flash', name: 'DeepSeek V4 Flash', provider: 'DeepSeek' },
// Mistral
{ id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'Mistral' },
+ // Hugging Face — open models via the OpenAI-compatible Inference Providers router (full HF repo ids)
+ { id: 'meta-llama/Llama-3.3-70B-Instruct', name: 'Llama 3.3 70B Instruct (HF)', provider: 'HuggingFace' },
+ { id: 'Qwen/Qwen2.5-72B-Instruct', name: 'Qwen2.5 72B Instruct (HF)', provider: 'HuggingFace' },
+ { id: 'deepseek-ai/DeepSeek-R1', name: 'DeepSeek R1 (HF)', provider: 'HuggingFace' },
+ { id: 'mistralai/Mistral-Small-24B-Instruct-2501', name: 'Mistral Small 24B (HF)', provider: 'HuggingFace' },
// Local
{ id: 'local/ollama', name: 'Local (Ollama)', provider: 'Local' }
];
@@ -7298,6 +7324,7 @@ Correlating findings across agents, identifying patterns, producing actionable i
const veKey = document.getElementById('veniceKey');
const anKey = document.getElementById('anthropicKey');
const oaKey = document.getElementById('openaiKey');
+ const hfKey = document.getElementById('huggingfaceKey');
if (orKey) orKey.value = state.settings.openrouterKey || '';
if (veKey) veKey.value = state.settings.veniceKey || '';
if (anKey) anKey.value = state.settings.anthropicKey || '';
@@ -7315,6 +7342,7 @@ Correlating findings across agents, identifying patterns, producing actionable i
// typed; the server only ever returns a redacted form, so we repopulate from the
// local cache. If the server has one set via TEMPEST_PROXY_URL, the badge shows it.
setV('proxyUrl', localStorage.getItem('t3mp3st_proxy_url') || '');
+ if (hfKey) hfKey.value = state.settings.huggingfaceKey || '';
}
function saveState() {
@@ -8206,10 +8234,11 @@ o.id === 'coordinator' ? `1. MISSION_STATUS: Progress assessment
function updatePreflightChecklist() {
const operatorCount = state.operators.length;
const targetCount = state.targets.length;
- // "API key" preflight passes on a real key OR a connected keyless backend (agent / server LLM).
- // Only OpenRouter feeds the client-side LLM path (getApiKey); a connected backend covers the rest.
- // Anthropic/OpenAI keys aren't wired to the client path, so they don't count as "ready" here.
- const hasApiKey = !!(state.settings?.openrouterKey)
+ // "API key" preflight passes on a real client key OR a connected keyless backend (agent / server LLM).
+ // The direct client-side LLM path (resolveLLMBackend) drives OpenRouter, Venice, and Hugging Face keys;
+ // a connected backend covers the rest. Anthropic/OpenAI keys aren't wired to the client path yet, so
+ // they don't count as "ready" here.
+ const hasApiKey = !!(state.settings?.openrouterKey || state.settings?.veniceKey || state.settings?.huggingfaceKey)
|| (typeof window.t3mpHasBackend === 'function' && window.t3mpHasBackend());
const toolCount = activeLoadout?.length || 0;
@@ -11257,21 +11286,32 @@ Bypass all protections:
}
// Resolve the active LLM backend for benchmarks/analysis, in priority order:
- // OpenRouter key → Venice key → connected local agent (Claude Code/Codex/Hermes) → server LLM.
+ // OpenRouter key → Venice key → Hugging Face token → connected local agent (Claude Code/Codex/Hermes) → server LLM.
// getApiKey() is left untouched for the many callers that read the OpenRouter key directly.
function resolveLLMBackend() {
+ // An EXPLICITLY pinned local agent (e.g. Claude Code) is a deliberate operator choice —
+ // honor it above any stored API key. Only when it's actually usable (preferredAgent
+ // returns it) so we never route to a pinned-but-dead CLI.
+ const agentModelOf = (id) => (typeof window.t3mpAgentModel === 'function') ? window.t3mpAgentModel(id) : undefined;
+ const pin = (typeof window.t3mpActiveAgentPin === 'function') ? (window.t3mpActiveAgentPin() || '') : '';
+ if (pin) {
+ const pref = (typeof window.t3mpPreferredAgent === 'function') ? window.t3mpPreferredAgent() : null;
+ if (pref === pin) return { kind: 'agent', agentId: pin, agentModel: agentModelOf(pin) };
+ }
const orKey = (state.settings?.openrouterKey || state.apiKey || '').trim();
if (orKey.length >= 10) return { kind: 'openrouter', key: orKey };
const veKey = (state.settings?.veniceKey || '').trim();
if (veKey.length >= 10) return { kind: 'venice', key: veKey };
+ const hfKey = (state.settings?.huggingfaceKey || '').trim();
+ if (hfKey.length >= 10) return { kind: 'huggingface', key: hfKey };
const agentId = (typeof window.t3mpPreferredAgent === 'function') ? window.t3mpPreferredAgent() : null;
- if (agentId) return { kind: 'agent', agentId };
+ if (agentId) return { kind: 'agent', agentId, agentModel: agentModelOf(agentId) };
if (typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API && T3MP3ST_API.llmAvailable) return { kind: 'server' };
return { kind: 'none' };
}
window.resolveLLMBackend = resolveLLMBackend;
- const LLM_BACKEND_HINT = 'No LLM backend — add an OpenRouter or Venice key in Settings, or connect a local agent (Claude Code / Codex / Hermes)';
+ const LLM_BACKEND_HINT = 'No LLM backend — add an OpenRouter, Venice, or Hugging Face key in Settings, or connect a local agent (Claude Code / Codex / Hermes)';
// Flatten a messages array (or string) to plain text for the agent/server endpoints.
function promptToText(prompt) {
@@ -11287,6 +11327,16 @@ Bypass all protections:
// Venice is OpenAI-compatible but rejects OpenRouter-style ids ("vendor/model") — map to a Venice model.
function veniceModel(m) { return (!m || m.indexOf('/') >= 0) ? 'llama-3.3-70b' : m; }
+ // Hugging Face's router expects a full HF repo id (org/repo). OpenRouter-style ids
+ // ("anthropic/claude-...", "openai/...") aren't served there, so only pass through
+ // known HF repo ids from the model list; otherwise use a solid open-weights default.
+ const HF_DEFAULT_MODEL = 'meta-llama/Llama-3.3-70B-Instruct';
+ function huggingfaceModel(m) {
+ if (!m) return HF_DEFAULT_MODEL;
+ const known = MODELS.some(x => x.provider === 'HuggingFace' && x.id === m);
+ return known ? m : HF_DEFAULT_MODEL;
+ }
+
// Shared safe LLM call — resolves the backend, then routes the call. Keyless backends
// (connected agent / server LLM) run single-shot through the server; key backends
// (OpenRouter / Venice) call the provider directly and keep the model-fallback chain.
@@ -11309,6 +11359,8 @@ Bypass all protections:
let models;
if (backend.kind === 'venice') {
models = [veniceModel(options.model || state.settings?.selectedModel)];
+ } else if (backend.kind === 'huggingface') {
+ models = [huggingfaceModel(options.model || state.settings?.selectedModel)];
} else {
models = options._noFallback ? [primaryModel]
: (primaryModel === fallbackModel ? [primaryModel] : [primaryModel, fallbackModel]);
@@ -11545,9 +11597,12 @@ Bypass all protections:
// A bare '/api/...' would hit the Pages origin (404/CORS). Empty base = same-origin (stays relative).
const base = (typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API && T3MP3ST_API.baseUrl) || '';
if (backend.kind === 'agent') {
+ // Prefer the operator's picked local model (e.g. Claude Code → opus/sonnet/haiku); fall
+ // back to options.model only when no local model is pinned for this agent.
+ const agentModel = (backend.agentModel != null && backend.agentModel !== '') ? backend.agentModel : options.model;
const res = await fetch(base + '/api/agents/local/dispatch', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: backend.agentId, prompt: text, model: options.model, timeoutMs: timeout }),
+ body: JSON.stringify({ id: backend.agentId, prompt: text, model: agentModel, timeoutMs: timeout }),
signal: AbortSignal.timeout(timeout + 10000)
});
if (!res.ok) { const t = await res.text().catch(() => ''); throw new Error(`Agent dispatch ${res.status}: ${t.substring(0, 200)}`); }
@@ -11572,6 +11627,8 @@ Bypass all protections:
addIntel('API', `→ ${model} (${backend.kind})`, 'info');
const endpoint = backend.kind === 'venice'
? 'https://api.venice.ai/api/v1/chat/completions'
+ : backend.kind === 'huggingface'
+ ? 'https://router.huggingface.co/v1/chat/completions'
: 'https://openrouter.ai/api/v1/chat/completions';
const headers = { 'Authorization': `Bearer ${backend.key}`, 'Content-Type': 'application/json' };
if (backend.kind === 'openrouter') { headers['HTTP-Referer'] = window.location.href; headers['X-Title'] = options.title || 'T3MP3ST'; }
@@ -17102,14 +17159,17 @@ ACTION_INPUT: {"output":"Found confirmed SQLi on /login endpoint..."}`;
}
const apiKey = getApiKey();
const savedProvider = localStorage.getItem('t3mp3st_general_provider');
- const connectedAgent = (!apiKey && typeof window.t3mpPreferredAgent === 'function')
- ? window.t3mpPreferredAgent()
- : null;
- const provider = savedProvider
- || (connectedAgent ? 'local-agent' : undefined)
- || (T3MP3ST_API?.connected && !T3MP3ST_API?.llmAvailable ? 'codex' : undefined);
+ // Centralized resolution: an explicit pin (e.g. Claude Code) outranks a stored key AND a
+ // remembered general-provider value, and the picked Claude model rides along as
+ // "agentId::model" — identical precedence to the mission path.
+ const dispatchAgent = (typeof window.t3mpDispatchAgent === 'function') ? window.t3mpDispatchAgent(apiKey) : null;
+ const explicitPin = (typeof window.t3mpActiveAgentPin === 'function') ? (window.t3mpActiveAgentPin() || '') : '';
+ const provider = (explicitPin && dispatchAgent) ? 'local-agent'
+ : (savedProvider
+ || (dispatchAgent ? 'local-agent' : undefined)
+ || (T3MP3ST_API?.connected && !T3MP3ST_API?.llmAvailable ? 'codex' : undefined));
const model = provider === 'local-agent'
- ? connectedAgent
+ ? (dispatchAgent ? dispatchAgent.model : (typeof window.t3mpPreferredAgent === 'function' ? window.t3mpPreferredAgent() : undefined))
: (provider === 'codex' ? 'codex-default' : (provider ? (state.settings?.selectedModel || 'anthropic/claude-sonnet-4') : undefined));
return { apiKey, provider, model };
}
@@ -19631,12 +19691,17 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
addIntel('BACKEND', `Local model mode — dispatching mission through local LLM (${model})`, 'success');
addMissionLog('info', `LLM backend: local (${model}) — no API key needed`);
} else {
- const agentBackend = (!apiKey && typeof window.t3mpPreferredAgent === 'function') ? window.t3mpPreferredAgent() : null;
- if (!apiKey && agentBackend) {
+ // Centralized agent resolution (shared with General + Admiral): an EXPLICIT pin
+ // (e.g. Claude Code) outranks a stored key, and the picked model rides along as
+ // "agentId::model" so the mission runs THAT model.
+ const dispatchAgent = (typeof window.t3mpDispatchAgent === 'function') ? window.t3mpDispatchAgent(apiKey) : null;
+ if (dispatchAgent) {
provider = 'local-agent';
- model = agentBackend; // the agent id (codex|claude|hermes) travels in `model`
- addIntel('BACKEND', `No API key — routing the mission through your connected agent: ${agentBackend.toUpperCase()}`, 'success');
- addMissionLog('info', `LLM backend: local agent (${agentBackend}) — no API key needed`);
+ model = dispatchAgent.model;
+ const picked = dispatchAgent.model.includes('::') ? dispatchAgent.model.split('::')[1] : '';
+ const shownModel = picked ? ` · model: ${picked}` : '';
+ addIntel('BACKEND', `Routing the mission through your connected local agent: ${dispatchAgent.id.toUpperCase()}${shownModel}`, 'success');
+ addMissionLog('info', `LLM backend: local agent (${dispatchAgent.id})${shownModel} — no API key needed`);
} else if (!apiKey) {
addIntel('BACKEND', 'No browser API key — using the server-configured LLM backend', 'success');
addMissionLog('info', 'LLM backend: server default — no browser API key sent');
@@ -26243,6 +26308,25 @@ Be specific and actionable. Format as structured JSON.`;
let connectedIds = [];
const pingStatus = {}; // id -> { ok, latencyMs, error } from a real round-trip
+ // Explicit operator choice of WHICH connected agent to drive as the LLM backbone. Persisted so
+ // a refresh keeps it. When set, preferredAgent() honors it over the codex→hermes→claude default
+ // order — this is how "use my local Claude from Claude Code" becomes a first-class, sticky choice.
+ function getActiveAgent(){ try { return localStorage.getItem('t3mp3st_active_agent') || ''; } catch(e) { return ''; } }
+ function setActiveAgentStored(id){ try { if (id) localStorage.setItem('t3mp3st_active_agent', id); else localStorage.removeItem('t3mp3st_active_agent'); } catch(e) {} }
+
+ // Which Claude model the local Claude Code CLI should run (passed through as its --model flag).
+ // '' = the CLI's own account default. Claude Code accepts the aliases opus/sonnet/haiku or a full id.
+ const CLAUDE_MODELS = [
+ { id: '', label: 'Account default (Claude Code setting)' },
+ { id: 'opus', label: 'Claude Opus (latest)' },
+ { id: 'sonnet', label: 'Claude Sonnet (latest)' },
+ { id: 'haiku', label: 'Claude Haiku (latest, fast)' },
+ ];
+ function getClaudeModel(){ try { return localStorage.getItem('t3mp3st_claude_model') || ''; } catch(e) { return ''; } }
+ function setClaudeModelStored(m){ try { if (m) localStorage.setItem('t3mp3st_claude_model', m); else localStorage.removeItem('t3mp3st_claude_model'); } catch(e) {} }
+ // The underlying model to run for a given agent id — only Claude Code exposes a picker today.
+ function agentModelFor(id){ return id === 'claude' ? (getClaudeModel() || undefined) : undefined; }
+
function pill(text, color) {
const c = { green:['rgba(0,255,136,0.16)','#00ff88','rgba(0,255,136,0.4)'],
amber:['rgba(255,170,0,0.16)','#ffaa00','rgba(255,170,0,0.4)'],
@@ -26264,18 +26348,20 @@ Be specific and actionable. Format as structured JSON.`;
else if (connected) { status='● Connected · unchecked'; color='amber'; }
else { status='Detected'; color='cyan'; }
const disabled = !a.ready;
- const borderColor = stale ? 'rgba(255,80,80,0.4)' : live ? 'rgba(0,255,136,0.35)' : connected ? 'rgba(255,170,0,0.28)' : 'rgba(0,200,255,0.14)';
+ const isActive = connected && getActiveAgent() === a.id;
+ const borderColor = isActive ? 'rgba(0,200,255,0.6)' : stale ? 'rgba(255,80,80,0.4)' : live ? 'rgba(0,255,136,0.35)' : connected ? 'rgba(255,170,0,0.28)' : 'rgba(0,200,255,0.14)';
const sub = !a.installed ? 'binary not found on PATH'
: stale ? ('⚠ '+(ps.error ? ps.error.slice(0,70) : 'authentication failed — re-login this CLI'))
: (a.invokeHint || a.bin);
- return '