Commit Graph
112 Commits
Author SHA1 Message Date
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
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
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 58193d688d fix Op Admiral local-agent and approval flow (#106) 2026-07-20 14:30:45 -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 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 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 95868c3d37 Merge remote-tracking branch 'upstream/main' into pr-47
# Conflicts:
#	README.md
2026-07-16 11:55: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 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
Joseph MaglyandPazificateur69 cf0090adf4 feat(arsenal): add source and supply-chain parsers (#97)
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>
2026-07-15 19:45:58 -04:00
Pazificateur69 f8c283377c feat(arsenal): parse scanner JSON into structured findings (#42)
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.
2026-07-15 19:43:51 -04:00
adrianwandCodex 089591b18f feat(scan): add live progress and recon bootstrap (#81)
* Add live scan progress view

* Make local-agent recon report real tool progress

* Feed recon bootstrap into local agent ReAct loop

* Remove stale local-agent JSON tool parser

* Label backend-seeded scan progress

---------

Co-authored-by: Codex <codex@openai.com>
2026-07-15 19:42:20 -04:00
Lyubomir Bozhinov 5b4ac700ad fix(agents): detect local CLIs via well-known install dirs (#83)
A T3MP3ST server launched from Finder, the desktop app, or any non-interactive
shell inherits launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin). The
local-agent detector resolved `claude`/`codex`/`hermes` against that PATH via
execFile, so an installed, authed CLI in ~/.local/bin (Claude Code's native
installer), Homebrew, npm-global, or a version-manager shim (mise/nvm/fnm) threw
ENOENT and was reported installed:false — the Settings "Connect Local Agents"
checkboxes went dead and it read like a UI bug.

Add resolveBin(): pure-fs (no shell) resolution over PATH plus a curated set of
well-known install dirs under the real agent home (agentHome(), so a HOME
redirect can't hide them), with an executable-file access check and fail-open to
the bare name — an unresolved name yields the same ENOENT → installed:false as
before, so true-absence detection is unchanged. Route the detector and both spawn
sites (runLocalAgent, localAgentChat) through it, so a CLI detected under a
minimal PATH also actually spawns. Bound the keychain auth-probe with a 2s
timeout so a locked/headless keychain (the same non-interactive launch context)
can't hang detection.

POSIX/macOS-scoped: resolveBin short-circuits on win32 — Windows resolution
(where.exe / .cmd shims) is handled by the complementary open PR #18.

Verified on macOS: RED->GREEN unit + integration tests (resolver, detector
wiring, and both spawn sites), and manual QA driving the real Web UI under a
reproduced minimal PATH (Claude Code flips from "Not installed" to detected).

Refs #78 — fixes the macOS/POSIX variant. The original report was a Windows
Sandbox environment (PR #18's domain), so this intentionally does not auto-close
that issue.
2026-07-15 19:39:59 -04:00
Joseph Magly db2d270af5 fix(server): avoid caller cwd dotenv loading (#96) 2026-07-15 19:39:12 -04:00
Joseph Magly 51f610f3a5 fix(local-agents): support current Codex exec CLI (#95) 2026-07-15 19:09:34 -04:00
Joseph Magly 4b81bf6436 fix(local): honor server LLM for keyless missions 2026-07-15 18:13:27 -04:00
Joseph Magly 600f43b8b1 fix(local): merge system prompts for vLLM 2026-07-15 18:13:00 -04:00
Eddy 25f86b0c66 fix: reject curl destination override flags
* fix: reject curl destination override flags

* fix: block additional curl target bypass forms
2026-07-15 18:12:35 -04:00
Joseph Magly 84fcef5428 fix: clarify local LLM setup 2026-07-15 18:12:12 -04:00
psighoandClaude Opus 4.8 7127fe8220 fix(server): accept same-origin requests from a non-loopback UI bind (#84)
When the operator binds the server to a non-loopback address
(T3MP3ST_HOST=0.0.0.0 or a LAN IP) they've explicitly opted into network
access — the same decision that already stands the anti-rebinding Host guard
down. But the CSRF origin guard and the SSE guard were NOT updated to match:
they still only trust loopback origins, so the operator's own LAN-served UI
(e.g. http://192.168.x.x:3333/ui) had every state-changing request and the
event stream rejected with "Cross-origin request rejected". That makes hunts
fail on any target from a LAN-accessed War Room (issue #84).

Fix: add isSameOriginAsHost() and, ONLY on a non-loopback bind, also trust a
request whose Origin/Referer host:port equals this server's own Host header —
i.e. a genuine same-origin request from the operator's own UI. A foreign origin
(evil.com) still mismatches the Host and is rejected, so the CSRF/drive-by
protection is preserved. On a loopback bind the branch is skipped entirely:
the strict loopback-only gate plus the Host guard remain fully in force, so
there is no new CSRF or DNS-rebinding surface for the default configuration.

Verified against a 0.0.0.0-bound instance:
  - same-origin LAN request  (Origin==Host, non-loopback) → passes the guard ✅
  - foreign origin (evil.com)                             → still 403 ✅
  - no Origin (curl/CLI)                                  → allowed ✅
Typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:57:09 +05:00
Codex 6fef62aa8e Narrow authorize-target to pending approvals 2026-07-12 09:50:43 -04:00
Codex 3d6dc62957 Tighten approval receipt scoping 2026-07-12 09:50:43 -04:00
Codex 964fe5b3ba Add scope receipts approval UI 2026-07-12 09:50:43 -04:00
Codex d500a29186 Ask approval for Admiral expanded scope 2026-07-12 09:50:43 -04:00