Commit Graph
174 Commits
Author SHA1 Message Date
Joseph Magly f2eec3c48c ci: split PR and release certification gates (#161)
T3MP3ST CI / Pull request gate (push) Canceled after 0s
T3MP3ST CI / Release certification and ZIP archive (push) Canceled after 0s
2026-08-23 21:27:31 -04:00
N3thunt3r69andJoseph Magly 52ff30de91 feat(llm): resume the Claude Code session across a kill-chain phase (#149)
* feat(llm): resume the Claude Code session across a kill-chain phase

Every ReAct iteration flattens the growing transcript into a brand-new
prompt and spawns a fresh `claude -p` process. Within one task that is
a quadratic resend; across a mission it also means each spawn re-pays
Claude Code's own CLAUDE.md/skills/MCP bootstrap tax from scratch,
since T3MP3ST never told the CLI these calls belong to one session.

LocalAgentAdapter now tracks the Claude Code session id from the JSON
envelope and passes it back in with --resume on every later call, so
the CLI carries the accumulated transcript itself and the repeated
part is billed at cache-read pricing instead of resent. The session
is scoped to one kill-chain phase: TempestCommand drops it for every
operator right after advancePhase(), since local-agent operators are
spawned once for the whole mission and would otherwise carry one
session across all 7 phases. A stale or expired session id fails fast
(confirmed against the real CLI: nonzero exit, no JSON on stdout) and
localAgentChat retries once without --resume rather than failing the
task over it.

Verified against the real CLI: a resumed call recalled prior context
correctly and dropped from $0.151 to $0.008 (cache_read_input_tokens
absorbing the prior turn instead of cache_creation), and an invalid
--resume id triggered the fallback and still succeeded. Ran live
against an authorized target: the same session id held across all 4
recon tasks in one phase, then a newly spawned operator's first call
in the next phase carried no --resume, confirming both the resume and
the reset.

Full suite: 764/764, no regressions.

* fix(llm): send only the delta on a resumed Claude Code call

jmagly's PR #149 review caught it: LocalAgentAdapter.chat() built its
prompt from the full messages array unconditionally, so a resumed
call sent the whole growing transcript again on top of a session that
already had it. The resend the feature exists to remove was still
happening, and the resumed session's own history now grew on top of
it too. Existing tests only asserted that sessionId reached the CLI,
never what was actually in the prompt, so this passed green.

LocalAgentAdapter now tracks the exact messages array object and how
many of its entries were already sent. A later call against the SAME
array (AgentLoop growing one task's transcript via push) sends only
the new tail. A DIFFERENT array (a new task) still sends everything,
since the resumed session has never seen that task's content even
though the CLI session itself carries over. The stale-session
fallback in localAgentChat now takes a separate fallbackPrompt: a
fresh session has no history, so its retry gets the full transcript,
never the delta sized for the failed resumed attempt.

Verified against the real CLI with a synthetic multi-round exchange
(~750 tokens of padding added between each call): promptTokens went
19127 -> 19625 -> 20135 -> 20645, a flat ~510 tokens per round
regardless of the accumulating transcript, instead of resending or
compounding. Added the regression coverage the review asked for: a
same-array resumed call excludes the prior turn's content while
retaining the tool contract, a new-array call still gets the full
prompt, and the fallback retry receives fallbackPrompt rather than
the delta, checked both at the LocalAgentAdapter call-shape level and
against a real spawned CLI script.

Full suite: 769/769, no regressions.

* fix(test): import LLMMessage in local-agent-tool-calling.test.ts

Hosted CI caught it, not my local run: the previous commit used
LLMMessage[] type annotations in two new tests without importing the
type, so tsc failed with TS2304 while vitest itself stayed green
(esbuild strips type annotations without checking them). I ran
typecheck once before writing those tests and never reran it after,
so this went out uncaught.

Full suite: 769/769. npx tsc --noEmit: clean.

* fix(llm): isolate per-operator Claude sessions and narrow the fallback

A self-review after the last round turned up a more serious version of
what jmagly caught: TempestCommand built exactly one LLMBackbone and
handed the SAME instance to every operator's AgentLoop (and to each
OperatorAgent's own decompose-on-failure path). LocalAgentAdapter's
new session/delta state lived on that shared instance, so one
operator's Claude Code session could get resumed by a completely
different operator, with the second operator's unrelated task content
sent into the first operator's conversation. Every test for the
resume feature so far constructed independent LLMBackbone instances
per test, which is not how spawnOperator() actually wires things, so
nothing caught it.

spawnOperator() now builds a fresh LLMBackbone per operator from the
same config, used for both that operator's AgentLoop and its own
decompose fallback, so state stays consistent within one operator and
isolated from every other one. OperatorCell.spawnOperator() takes an
optional llm override to carry that through; existing callers that
don't pass one keep the cell's shared default.

Two narrower issues from the same review:

localAgentChat's stale-session fallback retried on ANY failure of a
resumed call, not just a confirmed stale session. A real network
error or auth failure would silently double latency on a retry that
could not fix it, and compounded with LLMBackbone's own retry ladder
into up to 6 CLI spawns for one failing turn. The fallback now only
fires for the exact empirically-confirmed stale-session error text
("No conversation found with session ID").

The character-estimate usage fallback (used only when the JSON
envelope fails to parse) estimated from the delta prompt instead of
the full transcript. A resumed call's real promptTokens reflects the
whole cached conversation, confirmed empirically in the last round
(19127 -> 19625 -> 20135 -> 20645), so estimating from the small delta
text instead undercounted the budget check by an order of magnitude
in that one edge case.

One finding from the review was deliberately left alone: resending
the tool contract on every delta call is real overhead, but jmagly's
review explicitly required the contract stay on every call ("retaining
... the tool contract") and the existing test already pins that. Cutting
it would contradict that requirement for a small, bounded savings.

New coverage: an isolation test that spawns two operators through a
real TempestCommand and confirms neither's LocalAgentAdapter nor its
session leaks into the other, a test proving a non-stale failure
propagates without a fallback retry, and a test proving the usage
estimate reflects the full transcript on a mid-session parse failure.

Full suite: 772/772, no regressions.

* fix(llm): reset the Claude session at every task boundary

jmagly's third review: switching from delta to full-prompt sending on
a new messages array wasn't enough, since LocalAgentAdapter.chat()
still passed the OLD claudeSessionId either way. A second task on the
same operator inherited the first task's persisted Claude session,
carrying its system prompt, target data, and tool output (including
anything attacker-controlled) across a boundary T3MP3ST already has a
single sanctioned channel for: PackBoard.

chat() now clears claudeSessionId whenever the incoming messages
array is not the one it last saw, before building the request. A
different array means AgentLoop.run() started a new task; the call
after that reset carries no --resume, so it opens a genuinely fresh
session instead of resuming the old one under a bigger prompt. Within
one task the array is the same object across iterations (AgentLoop
grows it via push), so resumption and the delta-only send from the
last round are untouched.

Rewrote the tests that asserted continuity across separate array
literals, since that shape doesn't happen in AgentLoop and was masking
this exact gap: they now grow one array via push to represent a real
task, matching the pattern already used for the delta tests. Added
the test jmagly asked for directly: one operator, two sequential
tasks, task 2 has no session id, both at the LocalAgentAdapter level
and through a real TempestCommand.spawnOperator().

Full suite: 773/773, no regressions.

---------

Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
2026-08-23 17:12:27 -04:00
e05bdc4311 feat(llm): add native Novita AI provider via OpenAI-compatible endpoint (#155)
* feat(llm): add native Novita AI provider via OpenAI-compatible endpoint

Wire Novita AI (https://api.novita.ai/openai/v1) as a first-class LLM
provider, following the same pattern used for DeepSeek/HuggingFace/NanoGPT:
dispatched through the existing OpenAIAdapter, resolved via NOVITA_API_KEY,
with a default model, model catalog entries, config/setup wiring, and
regression tests.

* fixup(llm): use deepseek/deepseek-v4-flash-0731 as the second Novita model

Swap the curated Novita model catalog's second entry from
qwen/qwen3-coder-30b-a3b-instruct to deepseek/deepseek-v4-flash-0731 per
requested model selection. Verified live against api.novita.ai/openai/v1/models.

* fixup(llm): replace superseded DeepSeek Flash id with current Pro flagship

QA flagged that deepseek/deepseek-v4-flash-0731 has been superseded in the
live Novita catalog; deepseek/deepseek-v4-pro-0813 is now the current
serving DeepSeek flagship. Swap the second Novita model catalog entry
accordingly.

---------

Co-authored-by: jax-novita <jax-novita@users.noreply.github.com>
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
2026-08-23 16:29:46 -04:00
Lyubomir Bozhinov 41f0c14039 fix(obsidivm): actionable error when the range service is unreachable (#156) (#158)
* test(obsidivm): RED — bridge should give an actionable error when the service is down

The obsidivm:* benchmarks fetch from the OBSIDIVM range service; when it is not
running the bridge throws a bare 'fetch failed' (surfaced as 'FATAL: fetch
failed'), naming neither the service, the URL, nor how to start it. Pins that a
connection failure becomes an actionable, self-service error.

Fails on main: the message is 'fetch failed'.

Refs #156

* fix(obsidivm): actionable error when the range service is unreachable

The obsidivm:* benchmarks fetch from the OBSIDIVM range service; when it is not
running the bridge's shared call() let the raw 'fetch failed' propagate, which
surfaced as an opaque 'FATAL: fetch failed' (#156) naming neither the service,
the URL, nor how to start it — even in --hunter stub, whose grading step still
needs it. Wrap the fetch in call() (the single choke point for every OBSIDIVM
request) so a connection failure becomes: 'OBSIDIVM service unreachable at <url>
... Start it per docs/OBSIDIVM.md (python3 range.py, listens on :4200), or set
OBSIDIVM_URL', flagged err.unreachable to distinguish it from an HTTP-status
error; a timeout is reported distinctly. Clarify in the bench --help/usage that
every hunter mode (stub included) needs the OBSIDIVM service, live/t3mp3st also
a key. Adds scripts/obsidivm-bridge.d.mts so the new test types cleanly.

Closes #156

* fix(obsidivm): distinguish invalid-URL from unreachable; test timeout path

Adversarial review of the diff found two gaps:
- A malformed OBSIDIVM_URL (ERR_INVALID_URL) was folded into the 'service
  unreachable — is it running?' path, misdirecting the user to start a service
  when the real fix is the URL. Now branched to a distinct 'Invalid OBSIDIVM
  base URL … set OBSIDIVM_URL' error, no err.unreachable flag.
- The timeout (AbortError) branch had no coverage. Added a test using a
  never-responding TCP server + short timeoutMs asserting 'timed out after Nms'
  and NOT unreachable; and a test for the invalid-URL branch.

Also completed the .d.mts stub with the cloudgoat* methods it was missing.

Refs #156
2026-08-23 16:24:20 -04:00
Joseph Magly c352f34843 chore(delivery): align BT6 build and release gates (#160)
Make the contributor, CI, package, and tagged-release contracts agree with the shared BT6 baseline while preserving T3MP3ST's project-specific integrity checks. Exclude workspace-only material from published packages and keep local-agent detection tests host-independent.\n\nCloses #159
2026-08-23 16:22:24 -04:00
Lyubomir Bozhinov 6d4e01756a feat(binary): detect memcpy/memmove with a non-constant length (#151)
* 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
2026-08-12 13:28:55 -04:00
Joseph Magly afc9dad1b2 chore(aiwg): adopt shared BT6 maintainer workflow (#148)
* docs(policy): allow low-impact contributor courtesy cleanup

* chore(aiwg): migrate to shared BT6 maintainer plugin

* chore: reconcile delivery validation
2026-08-02 17:17:28 -04:00
Joseph Magly 8f658891f2 feat(local-agent): support OpenCode and Oh My Pi (#144) 2026-08-01 20:15:42 -04:00
N3thunt3r69andJoseph Magly d471da6603 fix(llm): populate usage for the Claude Code local-agent path (#140)
* 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>
2026-08-01 19:58:30 -04:00
Lyubomir Bozhinov 9020e5fc08 feat(ingest): extract JS/TS arrow and function-expression definitions (#142)
* 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
2026-08-01 16:05:29 -04:00
Joseph Magly a606adf038 feat(ui): add local-agent disconnect control (#143) 2026-08-01 16:03:57 -04:00
Niel Teng Hu 85af936583 feat(arsenal): wire garak structured report capture (#128)
Parse garak JSONL into provenance-backed findings, capture file-based reports in private per-run workspaces, remove sensitive transcript artifacts on every exit path, and cover parser honesty, evidence wiring, permissions, and cleanup failures.
2026-07-30 23:56:46 -04:00
2alf 44e7c210a2 feat(arsenal): subdomain takeover detection built-in (#134)
* 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
2026-07-30 23:50:39 -04:00
monkeygold 571a7743ae fix(tools): run tools image as non-root operator (#114)
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.
2026-07-30 23:38:03 -04:00
RKS 178a64e9c4 fix(deps): restore production dependency safety (#135)
Refresh patched production dependency resolutions and align the declared Node.js minimum with the dependency graph’s Node 22.19 runtime floor.
2026-07-30 23:18:55 -04:00
robbee184-spec 8df967dd39 feat(llm): add native NanoGPT provider support (#131)
Align NanoGPT integration with the published OpenAI-compatible API contract, including model discovery, configuration/setup surfaces, and secret-redaction safeguards.\n\nFixes #129.
2026-07-30 22:58:45 -04:00
2alfandJoseph Magly 8ccea7c82a feat(llm): add Hugging Face provider + selectable local Claude Code models (#127)
* 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>
2026-07-27 15:47:06 -04:00
e52c2fb2b7 fix(config): handle 'local-agent' provider in getLLMConfig (#118)
* 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>
2026-07-27 15:25:02 -04:00
Lyubomir Bozhinov 32b81708ea test(ui): add semantic defect gate for inline scripts (#111 hardening) (#113)
#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.
2026-07-26 14:53:12 -04:00
pliny 7824041c26 Merge pull request #112 from lyubomir-bozhinov/fix/ui-init-duplicate-response-111
fix(ui): repair botched-merge syntax errors that break app boot (#111)
2026-07-24 08:16:15 -04:00
lyubomir-bozhinov 58f35276c5 fix(ui): repair botched-merge syntax errors that break app boot (#111)
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.
2026-07-21 12:38:25 +03:00
lyubomir-bozhinov 23169db7ad test(ui): failing regression for #111 — inline script must parse
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.
2026-07-21 12:16:17 +03:00
Joseph Magly b192577b54 docs(architecture): baseline current design (#109)
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
2026-07-20 18:15:06 -04:00
Joseph Magly 937c6e9837 add reproducible cross-model benchmark matrix (#107) 2026-07-20 14:52:32 -04:00
Joseph Magly 58193d688d fix Op Admiral local-agent and approval flow (#106) 2026-07-20 14:30:45 -04:00
Joseph Magly 0c35a833b9 docs: add usage and developer documentation
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
2026-07-20 12:59:53 -04:00
SiNaPsEr0x 7b1d12a07c feat: SOCKS5 egress proxy + harden local-model path (#79)
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.
2026-07-20 11:54:29 -04:00
df08627cb9 feat: enhance function calling with richer schemas, validation, parallel execution, and Ollama native support (#51)
* 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>
2026-07-20 11:45:52 -04:00
Lyubomir BozhinovandJoseph Magly 151d3555c6 feat(ingest): multi-language white-box ingest (web-tree-sitter) (#75)
* 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>
2026-07-20 11:41:21 -04:00
Krzysztof MarciniakandJoseph Magly 440dedd9c1 feat: update DeepSeek models to V4 Flash and V4 Pro (#103)
* 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>
2026-07-20 11:36:50 -04:00
Lyubomir BozhinovandJoseph Magly 9cce68e804 feat(settings): Universal API Config panel with server-side model fetch (#90) (#101)
* 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>
2026-07-20 11:34:26 -04:00
Joseph Magly 010be32377 Merge pull request #70 from jmagly/fix/60-62-auth-github-deepseek
Add authenticated repo scans and DeepSeek provider
2026-07-17 01:42:08 -04:00
Joseph Magly be3eb7cb44 Merge remote-tracking branch 'upstream/main' into pr-70
# Conflicts:
#	src/config/index.ts
#	src/llm/index.ts
#	src/server.ts
#	src/setup.ts
#	src/types/index.ts
2026-07-17 01:39:59 -04:00
Joseph Magly cc9516a980 Merge pull request #44 from mahdi-salmanzade/feat/arsenal-recon-adapters
feat(arsenal): add six keyless recon/OSINT tool adapters
2026-07-17 01:31:14 -04:00
Joseph Magly 6e6fbbda76 Merge pull request #45 from RheagalFire/feat/add-litellm-provider
feat: add LiteLLM as AI gateway provider
2026-07-16 12:11:44 -04:00
Joseph Magly 5a12593baa Merge remote-tracking branch 'upstream/main' into pr-45
# Conflicts:
#	src/config/index.ts
#	src/types/index.ts
2026-07-16 12:11:16 -04:00
Joseph Magly fa968fb739 Merge pull request #47 from opastorello/feat/docker-deployment
feat: Docker deployment support
2026-07-16 11:55:56 -04:00
Joseph Magly 95868c3d37 Merge remote-tracking branch 'upstream/main' into pr-47
# Conflicts:
#	README.md
2026-07-16 11:55:20 -04:00
Joseph Magly 95fedfff1a Merge pull request #100 from jmagly/pr80-delivery
Deliver repaired PR #80 approval receipts
2026-07-16 01:19:20 -04:00
Joseph Magly db48d881f7 Merge branch 'pr-80' into pr80-delivery
# Conflicts:
#	docs/index.html
#	src/__tests__/local-api-hardening-static.test.ts
2026-07-16 01:18:03 -04:00
Joseph Magly 065fbb555f Merge pull request #99 from jmagly/pr-delivery-74-94-18-39-41
Deliver repaired PR batch (#74 #94 #18 #39 #41)
2026-07-16 00:55:19 -04:00
Joseph Magly 29c477d246 Merge branch 'pr-18' into pr-delivery-74-94-18-39-41
# Conflicts:
#	.gitignore
#	src/agent/local-agents.ts
2026-07-16 00:52:31 -04:00
Joseph Magly 5921221c9b Merge branch 'pr-41' into pr-delivery-74-94-18-39-41 2026-07-16 00:49:26 -04:00
Joseph Magly b76853893a Merge branch 'pr-39' into pr-delivery-74-94-18-39-41 2026-07-16 00:49:26 -04:00
Joseph Magly 9775038507 Merge branch 'pr-94' into pr-delivery-74-94-18-39-41 2026-07-16 00:49:26 -04:00
Joseph Magly 8e82e1c2ba Merge branch 'pr-74' into pr-delivery-74-94-18-39-41 2026-07-16 00:49:26 -04:00
Fred BlaiseandFred Blaise 26fe8495b9 feat: allow origin-scoped target headers from env (#65)
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>
2026-07-15 23:07:08 -04:00
3e19454bd4 feat(ops): add portable strict War Room preflight (#56)
* 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>
2026-07-15 21:59:27 -04:00
Joseph MaglyandWibias 916c260fd8 feat: add safe cross-platform upstream updater (#98)
* feat: add safe upstream update scripts

* fix(updater): use portable PowerShell temp path

---------

Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com>
2026-07-15 19:56:25 -04:00
Junaid Y. SheikhandClaude Opus 4.8 266655abcf fix(ui): separate tour control and add theme switcher (#22)
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>
2026-07-15 19:51:58 -04:00