* fix(llm): populate usage for the Claude Code local-agent path
LocalAgentAdapter.chat() never set `usage` on its LLMResponse, so
AgentLoop's token budget check (tokensUsed >= maxTokens) silently
never fired for Claude Code missions. maxIterations was the only
brake, and it bounds turn count, not actual spend.
Claude Code's --output-format json (instead of text) returns exact
per-call token counts, including prompt-cache creation/read tokens
that a text-length estimate has no way to see. localAgentChat now
requests that format for the claude agent id only. LocalAgentAdapter
parses the envelope into usage.promptTokens/completionTokens, with a
character-based estimate as a fallback if parsing fails, so usage
never goes back to undefined.
Verified directly against the CLI: a live run now reports
promptTokens 24458, completionTokens 1662 instead of undefined/0.
Full suite: 675/675, no regressions.
Fixes#139
* test(llm): harden Claude usage fallback
---------
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Align NanoGPT integration with the published OpenAI-compatible API contract, including model discovery, configuration/setup surfaces, and secret-redaction safeguards.\n\nFixes #129.
* 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>
Integrate SOCKS5 attack-traffic proxying, egress leak visibility, and local-model hardening. Preserve direct local/LAN model traffic while the proxy is active and reconcile the branch with current upstream.
* feat: add richer JSON Schema types and AJV validation utility
- Extended ToolParameter with enum, items, properties, additionalProperties (max depth 3)
- Added ToolError class with ToolErrorCategory enum
- Added ToolValidationError interface
- Created AJV-based validateToolArgs() with lazy loading and schema depth enforcement
- 57 new tests covering types and validation
* feat: add validation gate, parallel execution, and structured error feedback
- Arsenal.execute() validates args against JSON Schema before handler execution
- Tool execution errors categorized (scope_denied, tool_not_found, timeout, execution_error, validation_error)
- ToolError import fix in agent (was missing, causing silent ReferenceError)
- Semaphore-based parallel tool execution with configurable concurrency (default 5)
- Results maintain original call order regardless of completion order
- Structured error feedback to LLM with category only (no raw error messages)
- Scope/approval gates checked per-call before execution
- 35 new tests covering validation, parallel execution, and error feedback
* feat: add Ollama native function calling with probe + cache + fallback
- Added nativeTools config option to LLMConfig
- LocalAdapter probes model support for native tools on first call
- Probe result cached per model name to avoid re-probing
- Native tools formatted as Ollama /api/chat tools parameter
- Tool calls parsed from Ollama's tool_calls response format
- Falls back to text-based renderToolContract when native not supported
- OpenAI-compatible wire format also supported (for vLLM, LM Studio)
- 10 new tests covering probe, cache, fallback, and config overrides
* fix(tools): harden validation and native fallback
---------
Co-authored-by: sronaal <sronaal@users.noreply.github.com>
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Add a first-class `gemini` provider so operators can drive missions with a
direct GEMINI_API_KEY, instead of only reaching Gemini through OpenRouter.
Gemini is wired exactly like xAI: dispatched through OpenAIAdapter against
Google's OpenAI-compatible surface. The base URL is
https://generativelanguage.googleapis.com/v1beta/openai — the `/openai`
segment is required because OpenAIAdapter posts to `${baseUrl}/chat/completions`;
without it requests hit `/v1beta/chat/completions`, which Google does not serve.
Wires the provider through the type union, adapter factory, config
(interface, defaults, model catalog, key env map, getLLMConfig, fallback
chain, redaction, .env template), and adds regression tests pinning the
`/openai` base-URL segment.
Two issues blocked OpenAI-compatible servers that aren't Ollama-on-/v1:
1. LocalAdapter.isOpenAIWire matched only a literal /v1 path, so versioned
endpoints like Zhipu/z.ai's /api/paas/v4 silently fell through to Ollama's
native /api/chat wire format and failed. Match any /vN instead of literally /v1.
2. The `local` provider was treated as always-keyless, so servers that require
a bearer (Zhipu/z.ai, Together, ...) could never authenticate. Read an
optional key from TEMPEST_LOCAL_API_KEY (ZAI_API_KEY / ZHIPUAI_API_KEY as
aliases), wire it through getApiKey/hasApiKey so `tempest test` and the
mission engine accept a configured local provider, and document the options
in the env template.
The key is read from env in-memory and never persisted into the store, matching
the existing env-key hardening invariant (api-key-env-static).
Adds src/__tests__/local-openai-wire-static.test.ts pinning both fixes.
Verified end-to-end against z.ai's GLM models (glm-4.5-air / 4.6 / 5.2).