feat(local-agent): support OpenCode and Oh My Pi (#144)
This commit is contained in:
1 parent
d471da6603
commit
8f658891f2
8 files changed
+150
-24
No files matched your search
@@ -24,7 +24,7 @@
|
||||
|
||||
**Your AI coding agent is already a hacker — T3MP3ST hands it an arsenal.**
|
||||
|
||||
Point it at an authorized target and the kill chain runs itself: **recon → exploit → report**, from a browser War Room or the CLI, driven by the agent you're *already* signed into — Claude Code, Codex, Hermes — or a model you run **fully offline** (Ollama, LM Studio, vLLM). No new API keys, no cloud tenant, no second bill. Your agent is the brain; T3MP3ST is the war machine bolted around it. **Self-hosted storm. Keyless warfare.** ⚡
|
||||
Point it at an authorized target and the kill chain runs itself: **recon → exploit → report**, from a browser War Room or the CLI, driven by the agent you're *already* signed into — Claude Code, Codex, Hermes, OpenCode, Oh My Pi — or a model you run **fully offline** (Ollama, LM Studio, vLLM). No new API keys, no cloud tenant, no second bill. Your agent is the brain; T3MP3ST is the war machine bolted around it. **Self-hosted storm. Keyless warfare.** ⚡
|
||||
|
||||
And it won't ask you to take its word for it. On **XBOW's own 104-challenge suite it scores 90.1% pass@1** — above XBOW's self-reported 85% — alongside hint-free CTF solves and a **cold hunt on real, post-cutoff CVEs the model had never seen**. Every number in this README recomputes from committed data with one command (`npm run verify-claims`). Loud about the mission, honest about the build — the [status table](#what-ships-today) says exactly what's live, what's scaffolding, and what's still roadmap; full receipts in [Benchmarks](#benchmarks).
|
||||
|
||||
@@ -68,7 +68,7 @@ npm install
|
||||
npm run server # War Room → http://127.0.0.1:3333/ui/
|
||||
```
|
||||
|
||||
In the War Room, open **Settings** and connect a local agent (Claude Code / Codex / Hermes). Then describe a target to **Op Admiral** in plain English and launch. The agent you connected is the brain. No key required.
|
||||
In the War Room, open **Settings** and connect a local agent (Claude Code / Codex / Hermes / OpenCode / Oh My Pi). Then describe a target to **Op Admiral** in plain English and launch. The agent you connected is the brain. No key required.
|
||||
|
||||
Prefer to bring a key? Set one and skip the connect step:
|
||||
|
||||
@@ -296,7 +296,7 @@ Operators map to MITRE ATT&CK and Cyber Kill Chain phases (recon is live; later
|
||||
| **Coordinator** | Command & Control | TA0011 | mission control, orchestration |
|
||||
| **Analyst** | Analysis | — | pattern analysis, reporting |
|
||||
|
||||
**Providers:** OpenRouter, Venice, Anthropic, OpenAI, or a keyless local agent (Claude Code / Codex / Hermes). Set `OPENROUTER_API_KEY` / `VENICE_API_KEY` / `ANTHROPIC_API_KEY`, or connect an agent in Settings.
|
||||
**Providers:** OpenRouter, Venice, Anthropic, OpenAI, or a keyless local agent (Claude Code / Codex / Hermes / OpenCode / Oh My Pi). Set `OPENROUTER_API_KEY` / `VENICE_API_KEY` / `ANTHROPIC_API_KEY`, or connect an agent in Settings.
|
||||
|
||||
**Integrations:** `node dist/mcp-server.js` exposes `security_recon` to MCP-aware agents. `npm run server` starts the HTTP API (`POST /api/mission/start`, `GET /api/mission/status`, and more). Full reference in [docs/](docs/).
|
||||
|
||||
|
||||
+4
-4
@@ -26303,9 +26303,9 @@ Be specific and actionable. Format as structured JSON.`;
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// ═══════════ Connect Local Agents — detect + enlist already-authed CLIs (Claude Code / Codex / Hermes) ═══════════
|
||||
// ═══════════ Connect Local Agents — detect + enlist already-authed CLIs ═══════════
|
||||
(function(){
|
||||
const ICONS = { claude: '🟠', codex: '🟢', hermes: '🔵' };
|
||||
const ICONS = { claude: '🟠', codex: '🟢', hermes: '🔵', opencode: '🟣', omp: '🟡' };
|
||||
let lastDetected = [];
|
||||
let connectedIds = [];
|
||||
const pingStatus = {}; // id -> { ok, latencyMs, error } from a real round-trip
|
||||
@@ -26536,9 +26536,9 @@ Be specific and actionable. Format as structured JSON.`;
|
||||
}
|
||||
window.t3mpHasBackend = hasBackend;
|
||||
// Pick the best connected agent to serve as the mission LLM backend: prefer a live-pinged one, and
|
||||
// prefer codex/hermes over claude (claude's standalone CLI is often auth-stale). Returns id | null.
|
||||
// Use a stable preference order when the operator has not pinned a connected agent. Returns id | null.
|
||||
function preferredAgent(){
|
||||
var order = ['codex','hermes','claude'];
|
||||
var order = ['codex','opencode','omp','hermes','claude'];
|
||||
var connected = (connectedIds || []).slice();
|
||||
var live = connected.filter(function(id){ return pingStatus[id] && pingStatus[id].ok; });
|
||||
var pool = live;
|
||||
|
||||
@@ -24,6 +24,16 @@ const FAKE_CLI = '#!/bin/sh\necho "1.2.3"\n';
|
||||
// For the chat path, which writes the prompt to the child's stdin: drain it before replying so the
|
||||
// parent's write()/end() completes cleanly (no EPIPE races), then emit a clean, non-error reply.
|
||||
const FAKE_CLI_STDIN = '#!/bin/sh\ncat >/dev/null 2>&1\necho "1.2.3"\n';
|
||||
const FAKE_OPENCODE = `#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then echo "opencode 1.2.3"; exit 0; fi
|
||||
prompt=$(cat)
|
||||
printf '%s\\n' "$OPENCODE_PERMISSION|$*|$prompt"
|
||||
`;
|
||||
const FAKE_OMP = `#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then echo "omp 1.2.3"; exit 0; fi
|
||||
prompt=$(cat)
|
||||
printf '%s\\n' "$*|$prompt"
|
||||
`;
|
||||
const tmpDirs: string[] = [];
|
||||
const savedEnv = { PATH: process.env.PATH, AGENT_HOME: process.env.T3MP3ST_AGENT_HOME };
|
||||
|
||||
@@ -63,6 +73,7 @@ describe('resolveBin — macOS/POSIX well-known-dir resolution (issue #78)', ()
|
||||
it.each([
|
||||
['.asdf/shims'],
|
||||
['.bun/bin'],
|
||||
['.opencode/bin'],
|
||||
['.local/share/pnpm'],
|
||||
])('resolves a CLI installed under the well-known dir %s', (rel) => {
|
||||
const home = scratch();
|
||||
@@ -185,6 +196,24 @@ describe('detectLocalAgents — wiring: a well-known-dir CLI is reported install
|
||||
// codex/hermes are absent from this temp home → still not installed (no false positives).
|
||||
expect(agents.find((a) => a.id === 'codex')?.installed).toBe(false);
|
||||
});
|
||||
|
||||
it('detects authenticated OpenCode and Oh My Pi installations (#136)', async () => {
|
||||
const home = scratch();
|
||||
const binDir = join(home, '.local', 'bin');
|
||||
putExe(binDir, 'opencode', FAKE_OPENCODE);
|
||||
putExe(binDir, 'omp', FAKE_OMP);
|
||||
mkdirSync(join(home, '.local', 'share', 'opencode'), { recursive: true });
|
||||
writeFileSync(join(home, '.local', 'share', 'opencode', 'auth.json'), '{}');
|
||||
mkdirSync(join(home, '.omp', 'agent'), { recursive: true });
|
||||
writeFileSync(join(home, '.omp', 'agent', 'agent.db'), 'fixture');
|
||||
process.env.T3MP3ST_AGENT_HOME = home;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
delete process.env.T3MP3ST_DISABLE_LOCAL_AGENTS;
|
||||
|
||||
const agents = await detectLocalAgents();
|
||||
expect(agents.find((a) => a.id === 'opencode')).toMatchObject({ installed: true, authed: true, ready: true, version: '1.2.3' });
|
||||
expect(agents.find((a) => a.id === 'omp')).toMatchObject({ installed: true, authed: true, ready: true, version: '1.2.3' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('spawn call-sites use the resolved path (issue #78 — detected-but-unspawnable would be worse)', () => {
|
||||
@@ -201,6 +230,28 @@ describe('spawn call-sites use the resolved path (issue #78 — detected-but-uns
|
||||
expect(res.output).toContain('1.2.3');
|
||||
});
|
||||
|
||||
it('keeps OpenCode tools denied on the one-shot ping/dispatch path (#136)', async () => {
|
||||
const home = scratch();
|
||||
putExe(join(home, '.local', 'bin'), 'opencode', FAKE_OPENCODE);
|
||||
process.env.T3MP3ST_AGENT_HOME = home;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
|
||||
const res = await runLocalAgent('opencode', 'ping', { model: 'openai/gpt-5', timeoutMs: 4000 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.output).toBe('{"*":"deny"}|run --model openai/gpt-5 ping|');
|
||||
});
|
||||
|
||||
it('keeps Oh My Pi tools disabled on the one-shot ping/dispatch path (#136)', async () => {
|
||||
const home = scratch();
|
||||
putExe(join(home, '.local', 'bin'), 'omp', FAKE_OMP);
|
||||
process.env.T3MP3ST_AGENT_HOME = home;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
|
||||
const res = await runLocalAgent('omp', 'ping', { model: 'anthropic/claude-sonnet-4-5', timeoutMs: 4000 });
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.output).toBe('--no-tools -p --model anthropic/claude-sonnet-4-5 ping|');
|
||||
});
|
||||
|
||||
it('localAgentChat launches the well-known-dir CLI (not the bare name)', async () => {
|
||||
const home = scratch();
|
||||
putExe(join(home, '.local', 'bin'), 'claude', FAKE_CLI_STDIN);
|
||||
@@ -209,4 +260,26 @@ describe('spawn call-sites use the resolved path (issue #78 — detected-but-uns
|
||||
|
||||
await expect(localAgentChat('claude', 'ping', { timeoutMs: 4000 })).resolves.toContain('1.2.3');
|
||||
});
|
||||
|
||||
it('drives OpenCode through stdin with model routing and all internal tools denied (#136)', async () => {
|
||||
const home = scratch();
|
||||
putExe(join(home, '.local', 'bin'), 'opencode', FAKE_OPENCODE);
|
||||
process.env.T3MP3ST_AGENT_HOME = home;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
|
||||
await expect(localAgentChat('opencode', 'long planning prompt', {
|
||||
model: 'anthropic/claude-sonnet-4-5', timeoutMs: 4000,
|
||||
})).resolves.toBe('{"*":"deny"}|run --model anthropic/claude-sonnet-4-5|long planning prompt');
|
||||
});
|
||||
|
||||
it('drives Oh My Pi through stdin with model routing and internal tools disabled (#136)', async () => {
|
||||
const home = scratch();
|
||||
putExe(join(home, '.local', 'bin'), 'omp', FAKE_OMP);
|
||||
process.env.T3MP3ST_AGENT_HOME = home;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
|
||||
await expect(localAgentChat('omp', 'long planning prompt', {
|
||||
model: 'openai-codex/gpt-5', timeoutMs: 4000,
|
||||
})).resolves.toBe('--no-tools --model openai-codex/gpt-5|long planning prompt');
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ describe("local-agent provider wiring (#118)", () => {
|
||||
});
|
||||
|
||||
it('preserves any supported agent id passed as the model', () => {
|
||||
for (const agent of ['codex', 'claude', 'hermes']) {
|
||||
for (const agent of ['codex', 'claude', 'hermes', 'opencode', 'omp']) {
|
||||
const cfg = config.getLLMConfig('local-agent', agent);
|
||||
expect(cfg.provider).toBe('local-agent');
|
||||
expect(cfg.model).toBe(agent);
|
||||
@@ -38,7 +38,7 @@ describe("local-agent provider wiring (#118)", () => {
|
||||
|
||||
it('surfaces the connected-agent ids as available local-agent models', () => {
|
||||
expect(AVAILABLE_MODELS['local-agent']?.map(m => m.id)).toEqual(
|
||||
expect.arrayContaining(['codex', 'claude', 'hermes']),
|
||||
expect.arrayContaining(['codex', 'claude', 'hermes', 'opencode', 'omp']),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
// LOCAL AGENT CONNECTORS — "bring your own already-authed agent"
|
||||
// =============================================================================
|
||||
// Detect + connect agent CLIs that already live, authenticated, on the operator's machine
|
||||
// (Claude Code, Codex, Hermes) and drive them headlessly as t3mp3st operators.
|
||||
// (Claude Code, Codex, Hermes, OpenCode, Oh My Pi) and drive them headlessly as t3mp3st operators.
|
||||
//
|
||||
// SECURITY POSTURE (important):
|
||||
// - We NEVER read, print, log, or transmit credential contents. Auth is detected purely by the
|
||||
@@ -80,7 +80,7 @@ function childEnv(): NodeJS.ProcessEnv {
|
||||
return env;
|
||||
}
|
||||
|
||||
export type LocalAgentId = 'claude' | 'codex' | 'hermes';
|
||||
export type LocalAgentId = 'claude' | 'codex' | 'hermes' | 'opencode' | 'omp';
|
||||
|
||||
/** Apply an authoritative bulk selection; single-agent connects remain additive. */
|
||||
export function syncLocalAgentSelection<T>(
|
||||
@@ -164,6 +164,30 @@ const SPECS: AgentSpec[] = [
|
||||
authArtifacts: ['~/.hermes/.env', "~/.hermes/auth.json", localAppData('hermes/.env'), localAppData('hermes/auth.json')],
|
||||
oneShot: (p, m) => ['-z', p, ...(hermesYoloEnabled() ? ['--yolo'] : []), ...(m ? ['-m', m] : [])],
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
label: 'OpenCode',
|
||||
vendor: 'Anomaly',
|
||||
bin: 'opencode',
|
||||
blurb: 'Open-source coding agent CLI',
|
||||
invokeHint: 'opencode run "<prompt>" (internal tools denied by T3MP3ST)',
|
||||
versionArgs: ['--version'],
|
||||
parseVersion: (o) => (o.match(/[\d]+\.[\d]+(\.[\d]+)?/) || ['?'])[0],
|
||||
authArtifacts: ['~/.local/share/opencode/auth.json', '~/.config/opencode/auth.json'],
|
||||
oneShot: (p, m) => ['run', ...(m ? ['--model', m] : []), p],
|
||||
},
|
||||
{
|
||||
id: 'omp',
|
||||
label: 'Oh My Pi',
|
||||
vendor: 'Oh My Pi',
|
||||
bin: 'omp',
|
||||
blurb: 'Oh My Pi coding agent CLI',
|
||||
invokeHint: 'omp --no-tools -p "<prompt>"',
|
||||
versionArgs: ['--version'],
|
||||
parseVersion: (o) => (o.match(/[\d]+\.[\d]+(\.[\d]+)?/) || ['?'])[0],
|
||||
authArtifacts: ['~/.omp/agent/agent.db'],
|
||||
oneShot: (p, m) => ['--no-tools', '-p', ...(m ? ['--model', m] : []), p],
|
||||
},
|
||||
];
|
||||
|
||||
export function getSpec(id: string): AgentSpec | undefined {
|
||||
@@ -181,6 +205,13 @@ export function hermesYoloEnabled(): boolean {
|
||||
return /^(1|true|yes|on)$/i.test((process.env.T3MP3ST_HERMES_YOLO || '').trim());
|
||||
}
|
||||
|
||||
/** OpenCode remains a planning backbone; Arsenal owns every executable tool action. */
|
||||
function agentChildEnv(id: string): NodeJS.ProcessEnv {
|
||||
const env = childEnv();
|
||||
if (id === 'opencode') env.OPENCODE_PERMISSION = JSON.stringify({ '*': 'deny' });
|
||||
return env;
|
||||
}
|
||||
|
||||
export interface AgentDetection {
|
||||
id: LocalAgentId;
|
||||
label: string;
|
||||
@@ -213,6 +244,7 @@ const NEWLINE_RE = new RegExp('\\r?\\n');
|
||||
function wellKnownBinDirs(home: string): string[] {
|
||||
const dirs = [
|
||||
join(home, '.local', 'bin'), // Claude Code native installer, pipx, mise
|
||||
join(home, '.opencode', 'bin'), // OpenCode native installer
|
||||
join(home, '.local', 'share', 'mise', 'shims'), // mise
|
||||
join(home, '.asdf', 'shims'), // asdf
|
||||
'/opt/homebrew/bin', // Homebrew (Apple Silicon)
|
||||
@@ -410,7 +442,7 @@ export function runLocalAgent(
|
||||
const timeoutMs = opts.timeoutMs ?? envTimeoutMs('T3MP3ST_LOCAL_AGENT_TIMEOUT_MS', 600000);
|
||||
const maxChars = opts.maxChars ?? 4000;
|
||||
// child env: provider keys stripped + HOME pinned to the real agent home (see childEnv).
|
||||
const env = childEnv();
|
||||
const env = agentChildEnv(id);
|
||||
const resolvedBin = resolveBin(spec.bin) || spec.bin;
|
||||
return new Promise((resolve) => {
|
||||
// stdin:'ignore' so the agent doesn't stall waiting on piped input (e.g. `claude -p`'s 3s stdin wait).
|
||||
@@ -452,16 +484,16 @@ export function pingLocalAgent(id: string, prompt?: string, timeoutMs?: number):
|
||||
|
||||
/**
|
||||
* Drive a connected agent as the LLM BACKEND for the mission/operator flow — a long-prompt one-shot
|
||||
* that returns ONLY the model's reply text (no CLI banner). Per-agent invocation: claude/codex feed
|
||||
* the prompt via STDIN (robust for long planning prompts), codex uses --output-last-message for a
|
||||
* clean reply, hermes takes the prompt as an arg. Provider keys are stripped so each CLI uses its own
|
||||
* that returns ONLY the model's reply text (no CLI banner). Claude, Codex, OpenCode, and Oh My Pi feed
|
||||
* the prompt via STDIN (robust for long planning prompts); Codex uses --output-last-message for a
|
||||
* clean reply, while Hermes takes the prompt as an arg. Provider keys are stripped so each CLI uses its own
|
||||
* login (no API key needed). Throws on non-zero exit / timeout so the LLMBackbone retry/fallback fires.
|
||||
*/
|
||||
export function localAgentChat(id: string, prompt: string, opts: { model?: string; timeoutMs?: number } = {}): Promise<string> {
|
||||
const spec = getSpec(id);
|
||||
if (!spec) return Promise.reject(new Error(`unknown local agent: ${id}`));
|
||||
// child env: provider keys stripped + HOME pinned to the real agent home (see childEnv).
|
||||
const env = childEnv();
|
||||
const env = agentChildEnv(id);
|
||||
const model = opts.model && opts.model !== 'codex-default' && opts.model !== id ? opts.model : undefined;
|
||||
const timeoutMs = opts.timeoutMs ?? envTimeoutMs('T3MP3ST_LOCAL_AGENT_TIMEOUT_MS', 600000);
|
||||
|
||||
@@ -478,6 +510,12 @@ export function localAgentChat(id: string, prompt: string, opts: { model?: strin
|
||||
workDir = mkdtempSync(join(tmpdir(), 't3mp3st-codexllm-'));
|
||||
outFile = join(workDir, 'reply.txt');
|
||||
args = ['exec', '--ephemeral', '--skip-git-repo-check', '--color', 'never', '--sandbox', 'read-only', '--output-last-message', outFile, ...(model ? ['-m', model] : [])];
|
||||
} else if (id === 'opencode') {
|
||||
args = ['run', ...(model ? ['--model', model] : [])];
|
||||
} else if (id === 'omp') {
|
||||
// Piped stdin selects OMP's one-shot print mode. Its own tools stay disabled so the model may
|
||||
// request actions through T3MP3ST's text contract while Arsenal remains the execution boundary.
|
||||
args = ['--no-tools', ...(model ? ['--model', model] : [])];
|
||||
} else { // hermes — takes the prompt as an arg
|
||||
args = ['-z', prompt, ...(hermesYoloEnabled() ? ['--yolo'] : []), ...(model ? ['-m', model] : [])];
|
||||
viaStdin = false;
|
||||
|
||||
+18
-2
@@ -638,7 +638,7 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
||||
],
|
||||
'local-agent': [
|
||||
// Connected local agent CLIs used AS the LLM backend — no API key (each uses its own login).
|
||||
// The chosen agent id (codex|claude|hermes) travels in the `model` field.
|
||||
// The chosen agent id (codex|claude|hermes|opencode|omp) travels in the `model` field.
|
||||
{
|
||||
id: 'codex',
|
||||
name: 'Codex (local CLI)',
|
||||
@@ -663,6 +663,22 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'agents', 'local-cli'],
|
||||
},
|
||||
{
|
||||
id: 'opencode',
|
||||
name: 'OpenCode (local CLI)',
|
||||
provider: 'LocalAgent',
|
||||
contextWindow: 200000,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'agents', 'local-cli'],
|
||||
},
|
||||
{
|
||||
id: 'omp',
|
||||
name: 'Oh My Pi (local CLI)',
|
||||
provider: 'LocalAgent',
|
||||
contextWindow: 200000,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'agents', 'local-cli'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -975,7 +991,7 @@ class ConfigManager {
|
||||
break;
|
||||
case 'local-agent':
|
||||
// Keyless backbone: the mission is routed through a connected local CLI agent
|
||||
// (Claude Code / Codex / Hermes), each using its own login — no API key or base
|
||||
// (Claude Code / Codex / Hermes / OpenCode / Oh My Pi), each using its own login — no API key or base
|
||||
// URL. The chosen agent id travels in `model`, optionally with the selected
|
||||
// underlying model after "::" (for example "claude::opus").
|
||||
actualModel = model || 'claude';
|
||||
|
||||
+2
-2
@@ -1353,7 +1353,7 @@ function estimateUsage(promptText: string, completionText: string): LLMResponse[
|
||||
return { promptTokens, completionTokens, totalTokens: promptTokens + completionTokens };
|
||||
}
|
||||
|
||||
// Generic adapter that drives a CONNECTED local agent CLI (Claude Code / Codex / Hermes) as the LLM
|
||||
// Generic adapter that drives a connected local agent CLI as the LLM
|
||||
// backend — NO API key needed; each CLI uses its own login. The agent id travels in `config.model`,
|
||||
// optionally with an underlying model after a `::` separator ("claude::opus", "claude::claude-opus-4-8")
|
||||
// so the operator can pick WHICH model the local CLI runs (passed through as the CLI's --model/-m flag).
|
||||
@@ -1371,7 +1371,7 @@ class LocalAgentAdapter implements LLMProviderAdapter {
|
||||
return { agentId, agentModel };
|
||||
}
|
||||
validateConfig(): { valid: boolean; error?: string } {
|
||||
return this.config.model ? { valid: true } : { valid: false, error: 'local-agent requires the agent id in `model` (codex|claude|hermes)' };
|
||||
return this.config.model ? { valid: true } : { valid: false, error: 'local-agent requires the agent id in `model` (codex|claude|hermes|opencode|omp)' };
|
||||
}
|
||||
private formatPrompt(messages: LLMMessage[], options?: ChatOptions): string {
|
||||
const parts = [
|
||||
|
||||
+3
-4
@@ -6290,7 +6290,7 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
|
||||
: resolveGeneralLLMConfig(provider, model, apiKey, baseUrl);
|
||||
const effectiveKey = missionLLMConfig.apiKey;
|
||||
if (providerNeedsApiKey(missionLLMConfig.provider) && !effectiveKey) {
|
||||
res.status(400).json({ error: 'API key required — pass apiKey, configure one on the server, or connect a local agent (Claude Code / Codex / Hermes)' });
|
||||
res.status(400).json({ error: 'API key required — pass apiKey, configure one on the server, or connect a supported local agent' });
|
||||
return;
|
||||
}
|
||||
if (missionLLMConfig.provider === 'local-agent') {
|
||||
@@ -6818,8 +6818,7 @@ function resolveGeneralLLMConfig(provider: string | undefined, model: string | u
|
||||
} {
|
||||
const defaultConfig = config.getLLMConfig();
|
||||
const selectedProvider = provider || defaultConfig.provider;
|
||||
// Local-agent backends (Claude Code / Codex / Hermes via the connector) need NO API key — the
|
||||
// agent uses its own login. The agent id (codex|claude|hermes) travels in `model`.
|
||||
// Local-agent backends use their own CLI login and need no T3MP3ST API key.
|
||||
if (selectedProvider === 'local-agent') {
|
||||
return {
|
||||
provider: 'local-agent' as any,
|
||||
@@ -7805,7 +7804,7 @@ app.get('/api/bounty/credentials', (_req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// LOCAL AGENT CONNECTORS — bring-your-own already-authed CLIs (Claude Code / Codex / Hermes)
|
||||
// LOCAL AGENT CONNECTORS — bring-your-own already-authed CLIs
|
||||
// =============================================================================
|
||||
// Detect agent CLIs that are already installed + logged-in on this machine and enlist them as
|
||||
// operators — no keys are entered or read (auth is detected by artifact PRESENCE only; see
|
||||
|
||||
Reference in new issue
Block a user