* test(binary): RED — no rule for memcpy/memmove with a non-constant length
Adds two corpus fixtures + direct assertions for a memcpy/memmove sink:
- memcpy-wire-length (bad): a wire-decoded length into a fixed buffer,
expect B-MEMCPY. Fails on main — the ruleset has no memcpy rule.
- memcpy-bounded-control (clean): two safe memcpys (sizeof-bounded + a
constant length). This forces a DISCRIMINATING rule: a naive /memcpy/
match false-positives here and fails the zero-false-positive gate.
Refs #150
* feat(binary): detect memcpy/memmove with a non-constant length
Adds B-MEMCPY to the decompiled-vuln ruleset — the classic unbounded-copy
overflow. The framework's own reasoning layer treats this as its textbook
target (src/orchestration/prompts.ts:60,80 — 'a wire-controlled length flowing
into an unchecked memcpy'; orchestration/index.ts:11), but the static
pre-filter had no rule for it: strcpy was covered, its memcpy(dst, src, len)
sibling was not.
The rule inspects the length (3rd) argument and fires only when it is
non-constant — a variable, struct field, deref, or computed/decoded value —
staying silent on a sizeof(...) or numeric-literal length. Validated against a
22-case dangerous/safe matrix (perfect discrimination). Corpus 6+2 -> 7+3; the
new sizeof/constant-bounded control keeps the rule honest — a bare /memcpy/
match would false-positive on it.
Closes#150
* fix(binary): stop B-MEMCPY false-positive on nested-call args; disclose limits
Adversarial review found a real false positive: a comma inside a nested call in
arg 1/2 (memcpy(dst, get_src(a,b), sizeof(dst))) mis-split the naive
[^,]+,[^,]+, arg list, so a sizeof-BOUNDED call fired. Restricting args 1-2 to
[^,()]+ (paren-free) removes it — verified the bounded nested-call form is now
silent while every common non-constant length still fires. The trade is a
memcpy whose 1st/2nd arg is itself a call is not matched (an acceptable miss for
a directional pre-filter, now disclosed).
Rule comment rewritten to honestly enumerate the directional misses (leading-
digit computed length, sizeof-plus-term, nested-call args, inline comments,
__memcpy_chk/memcpy_s/wmemcpy variants). Test expanded into an in-tree
discrimination matrix (fires / silent / pinned disclosed-limit cases) so the
scope boundary is auditable and any future tightening surfaces deliberately.
Refs #150
* fix(binary): match pointer casts in B-MEMCPY args; correct disclosed limits
A second adversarial pass on the shipped rule caught that the previous
paren-free tightening ([^,()]+) over-corrected: it also stopped matching
pointer CASTS on args 1-2 — memcpy((void *)dst, (void *)src, len) — which are
the most common memcpy shape in real decompiled output. The rule was silently
missing the main case.
Replace the arg matcher with one level of BALANCED parens,
(?:[^,()]|\([^()]*\))+, so a cast (void *)dst and a comma-bearing call
get(a,b) are each consumed as ONE arg. This fires on casts again, still fires on
a call-bearing arg with a non-constant length (now a true positive, previously a
disclosed miss), and keeps the original nested-call-with-bounded-length false
positive silent. Verified no catastrophic backtracking on pathological input.
Also corrects the disclosed-limits comment: the previous 'arg is itself a call'
miss is now only DOUBLY-nested parens, and the inline-comment note was
backwards (a comment obscuring a sizeof/literal false-fires, it does not cause a
miss) — restated accurately, with decompiled output not emitting inline comments
noted. Tests pin the cast fires and the variant exclusions.
Refs #150
* 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>
* test(ingest): RED — JS/TS arrow and function-expression definitions are not extracted
Pins issue #141 in both directions: every named-value function form must be
extracted with its name, parameters and span, and the shapes carrying no
name-to-function binding (IIFE, destructured, HOC-wrapped) must stay unmatched.
Also covers the end-to-end consequence — an arrow-bound sink ranking
attack_surface, and an arrow-bound handler becoming an entry point.
Fails on main: the .js/.ts/.tsx def-queries bind declarations only.
Refs #141
* feat(ingest): extract JS/TS arrow and function-expression definitions
The .js/.ts/.tsx def-queries bound declarations only, so every definition bound
to a value produced no CodeBlock and its sinks never reached classification or
ranking. Adds the name-to-function-value shapes to all three queries:
const f = (a) => {} / function (a) {} variable_declarator
const f = a => ... un-parenthesized single param
obj.f = ... / module.exports.handler assignment_expression
onmessage = e => ... bare-identifier assignment
{ f: (a) => {} } pair
class K { f = (a) => {} } field_definition / public_field_definition
Query-only — no parser, pipeline or CodeBlock-shape change. nodeToCodeBlock
already handles any @def/@name/@params triple and kindOf maps these to
'function'. Property-assigned and object-literal forms are named by the
property, consistent with how method_definition is named: that is the name
buildCallGraph and the entry-point heuristic match on.
Not matched, by design: HOC-wrapped values (const h = withAuth(fn)) and
anonymous callbacks (app.get('/x', (req,res) => ...)) have no name-to-function
binding, and inventing one is worse than honest absence. whitebox.ts caveat
block updated to match.
Refs #141
* fix(ingest): capture #private class-field arrows; keep same-line block ids distinct
Two defects found by adversarial review of the arrow-extraction change:
- `class C { #handler = (x) => eval(x) }` yielded nothing: a private field's
name is a `private_property_identifier`, not the `property_identifier` the
class-field pattern required. A sink in a private handler is exactly as
dangerous as one in a public handler, so it is captured (name keeps the `#`).
- A block id is `path::name@line`, so two same-named definitions starting on the
same line collide, and buildCallGraph keys on that id — the two blocks silently
share one callers/callees entry even when only one carries a sink. Rare for
declarations, routine once value-bound functions are extracted (minified
bundles pack many `{ handler: … }` onto one line). The later block is now
disambiguated by column; the first keeps the plain id, so no existing id
changes and the Python path is untouched.
Refs #141
* feat(arsenal): add subdomain-takeover classifier + fingerprint table
Pure, I/O-free detection logic split into src/arsenal/takeover.ts so it is fully
unit-testable without DNS or network:
- TAKEOVER_FINGERPRINTS: curated CNAME + unclaimed-resource-body signatures for 15
high-frequency services (S3, GitHub Pages, Heroku, Azure, Fastly, Shopify, Netlify, ...)
- classifySubdomainTakeover(): conservative verdict (confirmed / potential / none) driven by
a dangling CNAME and/or a service body fingerprint; no cross-service false matches
- renderTakeoverReport(): human-readable output block
* feat(arsenal): register subdomain_takeover_check built-in + bump tool count 108->109
- New keyless, self-contained built-in tool 'subdomain_takeover_check' (recon): resolves the
CNAME, checks whether the target still resolves (dangling), fetches the live body via the
scope-gated targetFetch, and classifies via classifySubdomainTakeover. Emits a real finding.
- Added to the Recon operator's default toolkit so the swarm can reach it (operator-toolkits
coverage invariant).
- Bumped the advertised arsenal size 108 -> 109 in lockstep across the count-honesty test,
README, and verify-claims (73 adapters + 36 built-in).
* test(arsenal): cover subdomain_takeover_check (classifier matrix + mocked DNS/fetch e2e)
- classifySubdomainTakeover decision matrix: confirmed via body fingerprint, confirmed via
dangling nxdomain-prone CNAME, potential (known service, no confirmation), potential
(dangling unknown), none (no CNAME / resolves clean), and no cross-service false matching
- renderTakeoverReport output shape
- end-to-end handler test with DNS + fetch mocked, driving the confirmed S3 path
Stage ProjectDiscovery binaries before installing them into /usr/local/bin, run the tools image as the existing Kali operator identity, preserve runtime access to Foundry tools, and cover the non-root Dockerfile and smoke-test contracts.
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>
* fix(config): handle 'local-agent' provider in getLLMConfig
Routing a mission through a connected local CLI agent (Claude Code / Codex /
Hermes) sets the provider to `local-agent`, but getLLMConfig's switch had no
case for it, so it fell through to `default` and threw
`Unknown provider: local-agent`. This aborted every keyless mission with a
REFUSED, even though `local-agent` is a registered provider wired to
LocalAgentAdapter everywhere else.
Add a `case 'local-agent'` mirroring `codex`: keyless (no apiKey/baseUrl), with
the connected agent id (codex|claude|hermes) carried in `model` and a sensible
default. Verified getLLMConfig('local-agent','claude') now resolves to
{provider:'local-agent', model:'claude'} instead of throwing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(config): regression coverage for local-agent provider (#118)
Addresses review feedback: the fix had no test, so removing the
`case 'local-agent'` would leave CI green while re-breaking keyless missions.
Adds focused assertions in src/__tests__/local-agent-provider.test.ts:
- local-agent + model `claude` resolves to provider/model with no apiKey/baseUrl
- any supported agent id (codex|claude|hermes) passed as model is preserved
- omitting the model falls back to the default agent id
- getLLMConfig('local-agent') no longer throws
- AVAILABLE_MODELS['local-agent'] surfaces the connected-agent ids
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: hoptoad-off <mvzmysun@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#111 shipped 8 bad-auto-merge scars into the SPA past a CI that only ever
lints/tests `src/` — the 27k-line docs/index.html had (and still has) no
static gate. The parse test added with the #111 fix catches outright syntax
failures; this adds the *semantic* layer that parsing misses.
It runs ESLint's high-precision "possible problem" rules programmatically
over each inline <script> (no new dependency — `eslint` is already a
devDependency; no eslint.config or CI change — rides the existing `npm test`):
no-redeclare, no-unreachable, no-dupe-keys, no-dupe-args, no-dupe-else-if,
no-duplicate-case, no-const-assign, no-import-assign, no-class-assign — the
exact shapes a conflict-concatenating merge leaves behind.
Tight, always-a-bug rules, each script linted in isolation → zero false
positives on the real file. no-undef / no-unused-vars are excluded (the SPA
relies on cross-<script> and browser globals a per-block lint can't see) and
no-func-assign is excluded (the app legitimately monkey-patches navigateTo).
Verified: 0 findings on the fixed tree, and it rejects the pre-fix main
(catches the duplicate `response`). Full suite green.
Also drops a now-unused eslint-disable directive in the parse test.
docs/index.html is a single classic <script>. Eight scars from bad
auto-merges (duplicated old/new versions of the same LLM-call sites left
side by side) made the script fail to PARSE, so nothing ran: the UI stuck
on "Initializing…", every link/handler dead, only the pre-registered
selfimprove/ledger poller still firing.
A parse error surfaces one at a time (the engine stops at the first), so
these were only visible once each prior one was fixed. Repaired:
- _safeLLMCallOnce: duplicate `response` decl + a leftover pre-refactor
fetch; restored backend-aware endpoint/headers (OpenRouter + Venice,
backend.key) and kept the cancellable AbortController.
- runLLMJudge: two function bodies concatenated (one nested, never closed).
- safeLLMCall: the whole function duplicated (old truncated head + new
loop that referenced an undefined `backend`); reunified into one
function with backend resolution, agent/server routing, model chain and
the fallback loop.
- runBenchmarks / autoApplyRecommendations: old "ANY LLM backend"
guard left un-closed before the new "reachable (key or local mode)"
guard — dropped the dead old guard / restored the missing brace.
- generateAnalysis auto-apply button, config-optimizer, self-improve and
CTF call sites: duplicated option objects / double catch collapsed to
the newer (_noFallback / cancellable) variant.
Verified on macOS: new regression test compiles every classic inline
<script> with node:vm (the RED baseline failed with "Identifier response
has already been declared"); full suite 646 green; headless-Chromium boot
now reaches "Connected", navigateTo is defined, zero uncaught errors.
Compiles every classic inline <script> in docs/index.html with node:vm
(how a browser parses it). Fails on current main with 'Identifier
response has already been declared' — the duplicate declaration in
_safeLLMCallOnce that stops the whole SPA from booting.
Capture the brownfield architecture and its relationship to the project vision.\n\n- add the current-state SAD and five retrospective ADRs\n- classify vision vectors against implementation evidence\n- record requirements, risks, gates, and construction handoff
Add user and developer documentation plus Pagenary docsite publishing support.
Includes Getting Started, Developer Guide, API and MCP references, Pagenary tenant metadata, npm scripts for sync/build/check/serve, and validation coverage for the generated docsite.
Closes#102
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>
* chore(ingest): pin web-tree-sitter@0.25.10 + tree-sitter-wasms@0.1.13; add multilang-ingest design docs
* feat(ingest): multi-language grammar registry (web-tree-sitter, 8 langs, fail-open); 100% cov
* feat(ingest): language-aware param-name splitter (py/go/ts/java/c); 100% cov
* feat(ingest): tree-sitter CodeBlock extractor with wall-clock parse budget + fail-open
* feat(ingest): cross-lang sinks + multilang config + wire tree-sitter into ingestRepository (sync)
* feat(ingest): repoint white-box production callers to multilang config + prod regression test
* server-init-wiring: load multi-language grammars once at bootstrap
initGrammars() runs after LLM init, before app.listen. Internally
fail-open (never rejects): on failure the grammar registry stays empty
and ingest falls back to the Python regex parser.
* adversarial-tests: hostile-input hardening for untrusted target source
Malformed per-language source, wrong-language content, binary/non-UTF8,
empty, unicode identifiers, 20k-func large file (wall-clock budget),
deeply nested input, and a symlink-loop crawl — all must fail-open, never
throw or spin. Drives parseFileMultiLang + ingestRepository/crawl.
* readme-status + docs: flip white-box status to multi-language ingest
README source-code rows now state web-tree-sitter multi-language ingest
(fail-open to Python regex). whitebox.ts scope caveat + no-source error
message updated off the stale Python-only wording.
* coverage-setup: enforce 100% coverage gate on the three new ingest files
vitest.config.ts adds a coverage-only block (v8, perFile thresholds=100)
scoped to ts-grammars.ts, param-split.ts, ts-parse.ts. Test discovery and
env stay on defaults. Edited files gate on new-branch coverage via the
regression suites; the server.ts initGrammars bootstrap line is the
documented smoke-covered carve-out.
* review: fix WASM heap leak, language-scoped fail-open, sink recall/precision
Adversarial security + strict code review (two independent subagents)
returned one blocking bug and several correctness findings; remediated:
BLOCKING #1 — native WASM memory leak: web-tree-sitter Tree/Parser objects
were never .delete()'d. Both reviewers reproduced a process-global wedge:
after ~7 large parses the shared WASM heap exhausts, every subsequent parse
aborts (caught by fail-open), and non-Python ingest silently returns []
forever. Fix: free the tree in a finally; delete the parser in resetParser.
Regression test parses ~8MB through the real shared parser 12x.
regex, which mis-parses brace/class syntax into corrupt blocks (wrong line
spans, lost methods). Now non-.py fail-open returns [] (honest absence); the
Python regex only ever runs on .py. Isolated ts-parse-fallback test.
http(s).request, axios(config), C exec-family); negative lookbehind stops
bare system(/popen(/fetch( from matching Python receivers (os.system etc.
still matched, corpus rankings unchanged). Recall+precision tests added.
operator sees non-Python degradation.
(one-line call-graph edges, no decorators, C++ member methods).
* review round 2: fix sink-regex regressions, real timeout test, deterministic leak guard
Third review round (correctness + security re-attack + test-quality) against
the round-1 fixes surfaced issues in the fixes themselves; remediated:
- Sink-regex regressions I introduced: (1) my fetch lookbehind dropped
window.fetch/this.fetch (top JS SSRF idiom) — reverted to \bfetch\(;
(2) bare .Get/.Post/.Do flagged db.Get/cache.Get/queue.Do as SSRF —
receiver-qualified to [Cc]lient. Recall+precision tests cover both;
regex is ReDoS-free (timed).
- Placebo leak test: the 12x function-dense loop did NOT wedge the parser
on machines with heap headroom (proven: passed with tree.delete removed).
Replaced with a deterministic vi.spyOn(Tree.prototype,'delete') assertion,
verified RED without the fix.
- Real (non-mocked) parse-timeout test: parseFileMultiLang gains an injectable
budgetMs; a 0ms budget drives the real progressCallback cancellation to [].
- runWhiteboxAnalysis swap now has coverage (spy on orchestrator.run) so the
2nd production caller can't silently revert to the Python-only config.
- Un-vacuoused weak tests: unicode now asserts the extracted block; nested/
wrong-language wrapped in invariants() + timing bounds.
- CI now runs npm run test:coverage (the 100% gate was defined but unenforced).
- Security: documented maxFileBytes as the control bounding a single parse's
peak WASM memory below the 2GB heap-abort ceiling.
- Stale 'fail-open to Python regex' docs fixed (ts-parse, whitebox, server,
adversarial); circular-import note added; temp-dir cleanup; honest coverage
wording in spec + config.
* docs(ingest): align fallback-contract text with the shipped language-scoped fail-open
Review feedback (#75, @jmagly): the design docs + one ts-grammars comment
still described the pre-implementation 'fall back to the Python regex'
model for non-Python files. The shipped contract is language-scoped: .py
uses parseFile; any non-.py file with a missing/unsupported/failed grammar
or a parse timeout returns [] (never the Python regex, which would mis-parse
brace/class syntax into corrupt blocks). Updated every stale reference in
multilang-ingest-spec.md, multilang-ingest-plan.md, and ts-grammars.ts so
the docs match the security boundary. Docs/comment only — no code change.
* fix(test): make the ReDoS wall-clock guard robust under coverage instrumentation
Review feedback (#75, @jmagly): the CI step this PR adds (npm run
test:coverage = vitest run --coverage) runs the full suite under V8
coverage, and that overhead pushes the pathological-brace DoS timing guard
in local-agent-tool-calling.test.ts:69 over its <300ms bound (320 < 300
fails on CI). Raise the bound to 1000ms — still 2x below the ~2100ms
quadratic-ReDoS regression it guards against, so it fails hard if the
regression returns, but no longer false-fails under coverage/slow CI.
Chose the reviewer's option (a) (robust bound) over scoping test:coverage,
so the gate stays a simple full-suite run and no test-file enumeration can
rot. Verified: npm run test:coverage is green and the 3 new files stay at
100%.
* fix(ingest): cover pointer-returning C functions
---------
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
* feat: update DeepSeek models to V4 Flash and V4 Pro
- Replace deprecated deepseek-chat / deepseek-reasoner with deepseek-v4-flash / deepseek-v4-pro
- Update base URL from api.deepseek.com/v1 to api.deepseek.com
- Update context window (1M) and max output (384K) per official API docs
- Old models deprecated 2026-07-24
* fix: resolveGeneralLLMConfig missing baseUrl + Admiral timeout 180s
resolveGeneralLLMConfig constructed a new LLMConfig but omitted
baseUrl, causing the adapter to fall back to api.openai.com/v1
regardless of provider — DeepSeek keys hit OpenAI (401).
Also raised Admiral preview timeout to 180s for thinking-mode models.
* fix: createTempestCommandInstance missing baseUrl for operator exec
Operator LLM calls also lacked baseUrl, same 401-for-DeepSeek bug
as resolveGeneralLLMConfig. Reads provider-specific baseUrl from config.
* fix(deepseek): migrate retired persisted defaults
---------
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
* feat(settings): Universal API Config panel with server-side model fetch (#90)
Consolidates provider configuration into one "Universal API Config" panel in
Settings — provider dropdown (OpenAI-compatible / Anthropic), base URL, API key,
model with a live "Fetch models" button, and max context tokens — above the
existing per-provider key sections.
Backward-compatible by design: the panel reads and writes the SAME state.settings
keys the individual sections already use (${provider}Key, selectedModel), so
nothing migrates and nothing is lost; the individual sections keep working and
stay in sync.
- src/config/provider-models.ts (new): listProviderModels queries a provider's
own model endpoint (OpenAI-compatible Bearer /models, Anthropic x-api-key
/v1/models, OpenRouter public list); resolveModels wraps it fail-open.
- POST /api/models (new route): server-side so the browser dodges provider CORS.
Binds key+baseUrl by source — a caller-supplied base URL must bring its own
key, so the server never lends its configured key to a body-chosen URL. Always
responds, failing open to the static AVAILABLE_MODELS list on a missing key,
network error, or unknown provider (incl. local-agent) — never hangs.
- Model options are built via the DOM API (createElement/textContent), never
innerHTML, so untrusted provider-returned model ids cannot inject markup.
Scope notes: augment-not-replace (removing the individual sections is deferred
to maintainer preference); base URL + context cap are stored and used for model
discovery and server/agent flows — in-browser mission routing still uses
OpenRouter/Venice, matching the existing "not wired" hints; the "Model Reasoning
(Eino)" sub-proposal is a separate follow-up pending a mapping.
Verified on macOS: 12 unit tests (RED->GREEN) for the model module, full suite
462 green, and Playwright QA (17 checks) driving the real panel — render,
provider switch, persistence across reload, backward-compat, and both the static
and live Fetch paths.
Refs #90
* fix(settings): harden model list fetch
---------
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Enable authenticated scans across the HTTP arsenal while binding configured credentials to an exact target origin.
Includes case-insensitive per-request overrides, cross-origin redirect containment, protected curl configuration, result redaction, documentation, and regression coverage.
Co-authored-by: Fred Blaise <fred.blaise@protonmail.com>
* feat(ops): add portable War Room preflight command
Add npm run ops:preflight that checks operational readiness before
launching a War Room mission: API health, active mission status,
pending action receipts, local-agent/provider configuration, and
arsenal tool availability. Follows same pattern as scripts/doctor.mjs.
Supports --json for machine-readable output and --strict to fail
on warnings.
Fixes#35
Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
* chore: revert package-lock.json churn to reduce diff noise
Restores package-lock.json to upstream main version.
Removes unintended npm-version lockfile rewrite (peer flips, @emnapi/* entry shifts).
* fix(ops-preflight): correct response-shape reads for mission, approvals, and agent checks
Three bugs identified by jmagly in review:
1. Mission-active: API returns {active: boolean}, not {phase}
2. Pending-approvals: API returns {approvals: [...]}, not a flat array
3. Local-agent: API returns {connected: [...]} (array), not a boolean
All three verified against live endpoints per review feedback.
Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
* fix(ops): harden portable preflight checks
Signed-off-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
---------
Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
Signed-off-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Co-authored-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Two War Room UI fixes (docs/index.html):
1. The floating "guided tour" relaunch button (#t3mpTourRelaunch) was
position:fixed pinned bottom-left, landing on top of the sidebar
"Connected" status pill and hiding its first character. Moved it to
bottom-right (the conventional help-FAB corner) so it collides with
nothing.
2. Added a theme switcher in the sidebar footer with four themes: Storm
(the existing teal/green default, visually unchanged), Ember (amber),
Crimson (red), and Void (violet). The choice persists in localStorage
and is applied before first paint to avoid a flash of the default theme.
To make themes reskin the whole UI rather than only the ~87 var(--accent)
sites, the dominant hardcoded brand colors (#00ff88 / #2fffd2 / #00d4ff
and their rgba() forms) were converted to CSS variables -- but only
inside <style> and inline style="..." attributes, where var() is valid.
Bare JS string literals and SVG fill= attributes are left as-is; most are
semantic status colors (green=ready, red=error) that should stay fixed.
No JS logic changed; the page renders and all scripts run unchanged.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the JSON-scanner parser layer: wire semgrep, gitleaks, trivy and
grype into parseToolOutput so their findings become structured, gate-verifiable
evidence instead of raw text (they were parserStatus:'planned').
- semgrep (scan --json) → one finding per rule match; ERROR/WARNING/INFO
mapped to severity; CWE token pulled from
metadata (e.g. "CWE-78: …" → CWE-78); refs → remediation.
- trivy (fs --format json) → one finding per package vuln; CVE + CweIDs +
"upgrade <pkg> to <fixed>" remediation.
- grype (dir:. -o json) → one finding per match; CVE + package coordinates.
- gitleaks (detect --report json --redact) → one high finding per secret. Defence in
depth: the details are run through redactString()
so a raw secret can NEVER reach a finding / the vault,
even though the adapter already passes --redact.
Broadens the severity map to cover scanner vocabularies (ERROR/WARNING, CRITICAL…
NEGLIGIBLE, moderate) onto the five canonical levels. Flips parserStatus to
'structured' for the four tools. Extends parsers.test.ts + fixtures: field mapping,
the gitleaks redaction guarantee, and the end-to-end gate pass for every new scanner.
Stacked on the parser layer (#42); pairs with the scanner-invocation fix (#40) that
gives these tools the JSON-emitting arg templates. Until #40 lands the parsers simply
degrade to [] (no JSON to parse) — no misbehaviour. Secret scanners beyond gitleaks
and SARIF-style tools remain a follow-up.
Co-authored-by: Pazificateur69 <agagliardi@guardiaschool.fr>
The generic adapter factory returned raw stdout only (`adapter-tools.ts` success
path), so 35 catalogued scanners with `parserStatus: 'planned'` never populated the
structured `ToolResult.findings` channel — their output was dropped on the floor and
the agent could neither verify nor chain on it. The whole downstream pipe already
exists and is honest-by-construction: the agent loop stamps `provenance:'tool'` + the
raw output, the operator materialises each finding with output-evidence, and the live
gate (`evidence/gate.ts`) marks it verified.
Add `src/arsenal/parsers.ts` — `parseToolOutput(toolId, rawStdout) → ToolFinding[]`
for the five scanners that already emit machine-readable JSON via their existing arg
templates (zero template changes): nuclei (-jsonl), httpx (-json), dalfox (--format
json), ffuf (-of json), katana (-jsonl). Parsers are pure, dependency-free, and NEVER
throw: empty/garbled/unknown input yields `[]`, never a fabricated finding (same
discipline as stub-honesty / no-phantom-tools). The factory attaches parsed findings
while ALWAYS keeping the raw stdout as `output` — the evidence of record the gate checks.
- nuclei/dalfox emit CWE, which the tool channel dropped: add optional `cwe?: string[]`
to `ToolFinding` and thread it through the operator's finding materialisation.
- flip `parserStatus: 'planned' → 'structured'` for httpx, dalfox, katana (nuclei/ffuf
were already structured).
Adds `src/__tests__/parsers.test.ts` + fixtures: per-scanner field mapping, the
honesty contract (empty/garbled/adversarial → [], no throw), a factory-wiring check
that a minted adapter returns findings while preserving raw stdout, and an end-to-end
assertion that every parsed finding PASSES `gateLiveFinding` with provenance='tool'.
The minted-adapter path is gated behind T3MP3ST_FULL_ARSENAL, so the benchmarked
default arsenal is untouched. Supply-chain JSON tools (semgrep/gitleaks/trivy/grype)
and secret scanners (need redaction) are a clean follow-up.
* fix(ui): reset War Room to idle when a hunt is declined or aborted
A declined-approval hunt ("hunt not started") — or the red-X abort — left the
War Room stuck advertising an active run: the ENGAGE button stayed on the
"ENGAGING…" spinner (disabled), the LIVE fidelity banner kept claiming "real
tools probing the target", and the narration rail kept showing "Looking at your
target". completeMission() only toggled the button's display, never restoring
its label/disabled/opacity, and never cleared the fidelity banner or narration
rail; abortMission() had the same gap and no-op'd entirely when nothing was
"running" (so the red X did nothing on a declined hunt).
Add resetEngageUI(): restores the ENGAGE button (▶ ENGAGE, enabled) and clears
the fidelity banner (setFidelity('off')) + narration rail. Route both
completeMission() and abortMission() through it, and let the red X clear a
leftover engaging/probing UI even when missionRunning is already false.
Client-side only; no behavior change to an actually-running mission.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ui): clear stale substatus when the red X resets an idle War Room
Review nit: the idle-abort branch set cmdMissionName='STANDBY' but left
cmdMissionStatus unchanged, so after an approval-declined the header could read
STANDBY with a stale red "approval declined" line beneath it. Reset the
substatus to the initial idle state ("Awaiting orders", #666) too.
Verified: abort after a decline now shows STANDBY / "Awaiting orders" (grey).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>