The client sent MCP-Protocol-Version on the initialize request itself.
The spec requires the header on requests after initialization, and the
offered version already travels in the initialize body. A server that
validates the header rejected the handshake unless the client happened
to offer a version it supports, so connecting depended on matching the
error body and only worked for the two versions the client offers.
- Send the header only after initialize succeeds, with the version
the server negotiated
- Keep the fallback loop, newest-first order and negotiated-version
persistence for servers that still reject the initialize body version
- Rewrite both version tests around this, including a 2025-03-26-only
server, and assert that initialize carries no version header
Tags: #mcp
Co-Authored-By: htjulia <htjulia1@gmail.com>
The Streamable HTTP client always offered 2025-06-18 and kept sending
that MCP-Protocol-Version header after initialize, ignoring the
version the server answered with. A server that only supports
2025-11-25 negotiates its own version and then rejects every later
request with 400, so none of its tools load. Servers that validate
the header on initialize itself reject the handshake outright, and
the client never retried with another version.
- Offer 2025-11-25 first and fall back to 2025-06-18 when the server
rejects the version: HTTP 400 "Unsupported protocol version",
JSON-RPC -32602, or an UNSUPPORTED_PROTOCOL_VERSION code
- Send the protocolVersion from the initialize result on later
requests
- Keep the legacy HTTP+SSE transport on 2024-11-05
Closes#131
Tags: #mcp
Co-Authored-By: htjulia <htjulia1@gmail.com>
Two review fixes on top of the Ollama provider:
FromEnv no longer inherits OPENAI_API_KEY for the ollama format. Ollama
ignores the bearer token, so inheriting the cloud key could only ship it to
the local or LAN endpoint, contradicting the comment in NewProvider and the
documented behavior.
ConfigFrom now normalizes the base URL to the /v1 root, so the natural
Ollama URL (host:11434 without /v1) works for chat as well. Previously the
provider appended /chat/completions to it and the request landed on a path
Ollama does not serve, while pgListModels already appended /v1.
Tests: bare-host normalization assertion, the /v1/chat/completions path
assertion, and a new test that the env config carries no inherited key.
First tagged release of this fork. Adds the 0.4.0 changelog section covering the work since upstream 0.3.15, guards the Docker Hub publishing job behind a repository variable (no credentials are configured for that namespace), and stops auto-generated release notes in favour of the changelog body.
The update source is hard-coded to a repository that no longer exists and the release checksums are unsigned. Removes the selfupdate package and the /api/update/* endpoints, the top-bar check that queried GitHub on every page load, the Version & Updates card, and the exit-75 swap protocol in the start scripts.
Syncs BaaRai/Norma to upstream Autumn-27/Norma v0.4.4 (fast-forward, our
earlier changes were already merged upstream) and repins the replace from
the pre-merge pseudo-version to the tag.
What v0.4.4 brings for us: per-task process-tree RSS watchdog (kills the
process group over a memory cap, matters now that workers spawn nuclei,
ffuf, sqlmap and bbot), UTF-8-aligned output truncation, non-interactive
default env for subprocesses (no more tools hanging on hidden stdin reads),
input usage recovery from message_delta for accurate token accounting, and
streaming / thought_signature fixes. Full test suite green against a
scratch postgres.
Toolbox layer pins bbot v3.0.2 (AGPL-3.0, same license as ARTEX; standalone
CLI dependency, not linked code) in its own venv with module dependencies
preinstalled via --install-all-deps, so scans never need pip under a program
egress allowlist. Build-time gate dry-runs a dep-heavy module sample twice:
the first primes the dependency check, the second must be free of Installing
and ERRR lines or the build fails.
The bbot-recon skill gates usage on program policy: passive zero-touch mode
via seeds-only scans (-s, no -t) when scanners are banned, active-safe and
loud module sets when the program allows them, scope discipline with
blacklist and strict-scope, --no-deps mandatory on every run, JSONL ingestion
into facts and finding candidates with bbot findings treated as leads that
still need independent verification.
Closes#117
The dalfox musl tarball does not carry the binary at archive root, so a
rooted tar extract failed the image build; extract to a temp dir and
install by name (same shape as the projectdiscovery zips). Also dalfox
has no version subcommand - it treated it as a scan target; use
--version. Locally verified: image builds clean, nuclei v3.11.1 and
dalfox 3.2.4 execute inside the built image.
Seam fix after merging #111 (escalation) with #114 (ctx-threaded
hooks.current): the escalation test predates the signature change and kept
compiling on its own branch, so the merge left main red.
- resetLLMFailStreaksForTest helper: the package-global streak is in-memory
state, so the no-switch-on-ordinary-error subtests now reset it before each
case (accumulation across subtests sharing task/profile keys made the third
case escalate, breaking the contract the test guards)
- core lifecycle and credentials fixtures: goals now name the lab target, per
the ambiguity gate (these tests exercise lifecycle/credentials, not the gate)
- canonical reset helper moved into task_llm.go next to the counter it resets
Implements the per-intent rotation shape from #85: a task created with
llm_rotate=true distributes worker intents round-robin over its LLM chain
heads, so lane A can hunt on one model while lane B hunts on another
(different lenses per intent, no extra tokens). Default stays the classic
ordered failover from the active head; planner/goals/mainagent keep the
active head always.
Mechanics: the engine pins the next chain head into the run context when a
worker claims an intent; the pin travels through the provider resolver
(hooks.current now takes ctx). A pin is only a dispatch preference - it must
belong to the task chain and never bypasses failover state (an exhausted
chain stays exhausted). Schema: tasks.llm_rotate boolean, default false,
idempotent ALTER; exposed on the create payload and the task DTO.
Closes#85 (the ensemble/P4 shape stays parked as before)
Implements #109. Skills ship the chain-1 workflow (session file injection
through upload_progress filename + app LFI -> RCE, CVE-2026-109618/109619)
with hard gates: authorized targets only, program zero-day policy checked
before any live attempt (patches landed 2026-10-21 for 8.2-8.5), passive
detection before exploitation, lab-first practice via the vendored docker
lab, chains 2/3 (UAF, serializer mismatch) marked lab-only, one-shot
verification with a marker. Upstream PoCs vendored under scripts/poc/php2shell
with attribution and the MIT notice (upstream declares MIT in its README).
Installs the drost-community take-1 subset (#100) directly into the runtime
image as pinned release binaries (plus a sqlmap checkout symlinked into
PATH), so recon-phase workers get modern tooling through their existing
Bash access with no new trust surface (no docker, no MCP wrapper). Versions
are ARGs for deliberate upgrades; each tool is executed once at build time
so a broken download fails the build instead of a hunt.
Closes#100 (takes 2 and 3 stay design notes: docker-exec integration
remains blocked per #90; catalog/execute MCP gating is future work).
Three orchestration mechanisms from the Q00/ouroboros evaluation (#99):
- Goal ambiguity gate at task creation: a goal must name a target (URL,
domain, IPv4, localhost:port or absolute path) and reach a length floor,
else the API returns 400 before any token is spent. Deliberately
target-less goals opt out with the literal marker [targetless]. Guards
against the live failure where a goal naming no target sent every worker
to an unrelated asset.
- Consecutive-failure escalation on the task LLM chain: quota exhaustion
already advances the chain; now 3 consecutive non-quota call failures on
the same chain profile advance it too, with the true reason recorded.
Escalation-only by design (no auto-downgrade: mid-task model swaps risk
behavior drift; the operator keeps the manual chain reset).
- Stagnation surfacing in the planner overview: finished intents are
fingerprinted (case-folded, digits collapsed); a direction dispatched 3+
times with zero findings since its first attempt is flagged in a
self-describing stagnation_warning payload that instructs the planner to
diversify instead of grinding. Absent when nothing stagnates.
Closes#99
Sessions keep state=running in the store during a task-level pause, so the
sessions tab treated them as live and the run duration kept ticking with
Date.now(), visibly advancing time while the task (and its deadline budget)
was frozen. Pass taskPaused down from the detail page and pin the duration
at the last recorded step while paused.
Closes#84
Extracts candidate lessons from terminal tasks (confirmed, negative,
retest caution), gates them on cue + action + evidence, and makes
extraction idempotent through a per-source evidence key.
ARTEX_RSI_LESSONS selects off / shadow / inject. Shadow logs what would
have been shown; inject appends a code-owned, data-not-instructions block
after the planner and worker system prompts. No prompt mutation and no
adoption: status stays candidate.
Credit: the Library admission rule comes from harness2 (Google Research,
Apache-2.0, arXiv 2610.14224); the gain taxonomy from awesome-rsi
(lobehub).
Writes down the rules any future harness-optimization work must meet:
live tasks propose and benchmarks adopt, owner approval per adoption,
immutable gates and scope checks, one-step rollback through prompt
versions, flag off by default, bounded edits, and paired attribution.
Credits harness2 (Google Research, Apache-2.0, arXiv 2610.14224) for the
Library admission rule and propose/probe/compose shape, awesome-rsi
(lobehub) for the gain taxonomy, and RRSI, ModularRSI, SEABench, Agentic
Harness Engineering and the self-evolving-agent safety survey for the
specific guardrails. No code is copied.
GET /api/skill-gaps aggregates skill_usage rows with found=false by
skill, agent, and task category. Hits are returned for the same key so a
miss rate needs no second query; hit-only skills are excluded. The source
rows already existed: a miss is written when the model names a skill the
platform does not provide.
This is the evidence-backed capability backlog, not an auto-fixer: it
creates no skill and changes no prompt. Motivated by the harness and
skill-evolution sections of awesome-rsi (lobehub).
Adds GET /api/tasks/{id}/outcome: finding disposition and severity,
retest verdicts, result-row token totals, tool calls, skill misses, and
wall time in one row. This is the measurement layer an RSI loop needs
before claiming a change helped; it alters no agent behavior.
Metric selection follows harness2 (Google Research, Apache-2.0,
arXiv 2610.14224) and the task-gain / retained-gain / improver-gain
taxonomy in the awesome-rsi list.
The suite does not use one container port: most challenges publish 8080
but some (051, 091, 100) publish 5000, and the adapter had 8080
hardcoded, so deploy failed on those and the failure happened before
the teardown block, leaving the challenge running.
- the host port now comes from the app container port mapping (any
container port, via docker inspect), never from an assumed 8080
- the teardown finally now wraps the deploy as well: a failure halfway
through deploy no longer leaves containers behind (--keep and
--deploy-only still opt out)
The first live drost run died right after the task completed: the
findings detail route keys on finding_id (the findings-table row), not
on the exploration node id the list also carries, so every detail fetch
404ed; fetch_findings then re-raised the SystemExit from the API
wrapper, aborting scoring, the flag scan, the report and the teardown.
- details are fetched with finding_id (node id as fallback) and a
missing detail no longer kills the run: the list fields remain and the
finding stays gradeable
- the stored finding carries finding_id so the deep flag scan can
re-fetch untruncated text
- the deep scan tolerates the same failures
- teardown runs in a finally around the whole task phase, so a crash in
scoring or reporting can no longer leave the challenge running
(--keep still opts out deliberately)
The drost task goal template never included the target URL, so the
created task named no target: the first live run's planner matched the
challenge description against the global asset store and sent every
worker to an unrelated WordPress lab from past tasks. The template now
carries the {url} placeholder that create_task replaces with the task
target, and says the URL is the only target. The unit test locks the
placeholder in.
--llm-profiles passes llm_profile_ids to the created task (head first)
so a benchmark run exercises the model chain it claims to measure, and
the chain is stamped into results.json and the report.
--target-host rewrites the host in the task goal and company scope when
ARTEX runs in a container and the corpus runs on the host: the task must
reach the challenge via the host gateway (172.17.0.1), the same pattern
as the MCP server URLs. Local reachability checks still use 127.0.0.1.
DrostBench v1 ships 100 docker-compose web challenges, each with one
flag-bearing vulnerability plus decoys that do not reach the flag, so
flag capture is an objective win condition that no judge decides.
scripts/benchmark/drost.py drives one challenge: compose build with an
explicit per-build random flag (the suite's deterministic sha256 default
is never used, so a memorized flag cannot be replayed), up --wait, host
port discovery, down --rmi local. Ground truth is derived from the
challenge benchmark.json; the flag is scanned over the fetched findings
first and over full details, facts and the report only before declaring
a miss.
run_bench.py gains --corpus-type drost (one challenge per invocation),
flag capture in every drost report, a flag-only mode when no judge is
configured (gates reported as not measured, never as zero), and
--deploy-only for install validation. Image mode is unchanged.
test_drost.py covers the pure functions (flag formats, challenge id
validation, port parsing, ground-truth derivation incl. its refusals);
the lifecycle was exercised end to end on DBEN-v1-001: build with a
random flag, reachable app, compiled flag matching the override,
teardown without leftovers.
Closes#94
GET /api/tasks/{id} returned the metrics block zeroed (tokens, goal
progress, in_flight, findings, last_activity) because only listTasks
populated those DTO fields. Extract the fill into applyTaskMetrics and
call it from both handlers so the two endpoints report identical
numbers from the same TaskListMetricsAll query. The swallowed query
error in listTasks is now logged instead of silently zeroing the table.
Test: TestTaskDetailReturnsListMetrics seeds result-activity tokens and
goal nodes, asserts the detail endpoint reports them and matches the
list endpoint; fails on main, passes with the fix.
Fixes#91