- Clamp the summary request's max_tokens to the model's output limit so a
large STRIX_CONTEXT_SUMMARY_TOKENS can't get the request rejected (which
left the overflowing session uncompacted). Applied consistently to the
input-budget reservation and the request itself.
- Replace the tokenizer-unavailable fallback with the UTF-8 byte length, a
guaranteed upper bound on tokens for byte-level BPE, so budget checks can
never under-count dense history.
The chars/4 fallback under-counts dense text (code, base64, CJK), which
could let a summary request be packed past the real context window and get
rejected. Use a conservative ~3-chars/token estimate instead so budget
checks never under-count.
Drop the brittle substring matching for context-overflow detection. LiteLLM
already normalises every provider's overflow error to
ContextWindowExceededError and maintains the provider-specific matching
upstream, so is_context_overflow just checks that type.
When instructions, the reserved summary output, and any prior checkpoint
already consume the model's context window, the summary-input budget is
zero. Previously a doomed summary request was still submitted (and
rejected). Skip summarization in that case and log, rather than issue a
request guaranteed to overflow.
The 1,000-token floor on summary-input room could exceed the space
actually left after instructions, the reserved summary output, and any
existing checkpoint. On a small window the summary request then overflowed
and returned nothing, leaving the oversized session uncompacted. Clamp to
the real room instead so the request always fits.
Head+tail-truncate the serialized history before the summary request so a
very large head cannot itself overflow the model context and abort the
recovery it is meant to perform.
Restore cumulative history compaction (removed in the SDK migration) so a
long scan no longer replays an ever-growing transcript until it overflows
the model's context window and the run fails.
- strix/llm/context_budget.py: resolve the model's real input/output token
limits from LiteLLM metadata (128k gpt-4o, 272k gpt-5, 1M claude-sonnet-4,
131k deepseek), with a large configurable fallback for unmapped models and
a chars/4 token-count fallback.
- strix/llm/compaction.py: provider-agnostic compaction via litellm. Keeps a
security-focused structured summary (objective, findings, credentials,
payloads, URLs/paths, work state, dead ends, next move), keeps the most
recent turns by token budget, and snaps the summary boundary so no tool
call is separated from its result. Both triggers: proactive before each
run and reactive compact-and-retry on a real context-overflow error.
- Wire both triggers into the agent run loop next to the existing image
recovery; add replace_session_items() with restore-on-failure.
Env-tunable via STRIX_CONTEXT_* (auto-compact on by default).
A configured tool_output_max_bytes smaller than the truncation notice
itself can't fit a bounded preview, so a persisted result could exceed the
ceiling. Enforce a config floor (ge=1024) so nonsensical values are
rejected at load time instead of being worked around at runtime.
The head+tail slices could each take half of max_bytes, then the
truncation notice and its separators were appended on top, so the value
persisted to history could exceed the configured maximum. Reserve an
upper bound for the notice (and separators) out of the byte budget before
slicing so the whole joined result stays within max_bytes.
Chat-completions mode converts filesystem CustomTools to FunctionTools
(which bounds their result), but the Responses-API path kept them native
and unbounded, so a large read_file could still exhaust the context
window. Always configure the Filesystem capability to head+tail bound
tool output in both modes.
Treat tool_output_max_tokens as a ceiling so an explicit model-supplied
cap can't exceed it, and derive the truncation notice's dropped-line
count from the lines actually kept after the byte-trim pass. Also cast
the pygments fallback lexer so it satisfies the resolve_lexer return
type under the pre-commit mypy hook.
Cap the size of every tool result so a single verbose command (recursive
find, noisy scanner, full page dump) can't pin the conversation near the
model's context window for the rest of a scan.
- New ContextSettings config group with env-tunable caps.
- Default the SDK shell tools' max_output_tokens so exec_command /
write_stdin truncate head+tail instead of returning unbounded output.
- Bound Strix's own FunctionTool/CustomTool results (line + UTF-8 byte
head+tail preview with a truncation notice) and cap error strings.
The Exit Codes table in the CLI reference only listed 0 and 2, but the
CLI also exits with 1 on fatal errors (missing environment variables,
Docker unavailable, invalid config file, diff-scope resolution failure,
or an unhandled exception). It also implied exit 0 means no
vulnerabilities were found, which is only true in headless mode -
interactive runs always exit 0 regardless of findings.
Document exit code 1 and clarify the two cases for exit 0.
Every `uses:` in build-release.yml was a mutable tag; the `release` job has
`contents: write` and publishes the binaries users install, so a compromised
action could tamper the release. Action tag-hijacking keeps recurring
(aquasecurity/trivy-action, 75 tags, Mar 2026 TeamPCP; tj-actions, 2025;
codfish/semantic-release-action, Jun 2026) and SHA-pinned workflows were immune
each time. Pin all six actions to the commit each @major resolves to today
(concrete version in a trailing comment; setup-uv's annotated tag dereferenced
to its commit, not the tag object, so Dependabot tracks it). Also add a
top-level `permissions: contents: read` (the release job keeps its explicit
write) and `persist-credentials: false` on the build checkout.
actionlint passes. Pairs with #860 (Dependabot github-actions keeps the pins
current).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the post-scan local viewer hosting step so the run exits directly
instead of blocking with 'Hosting the local viewer. Press Ctrl-C to stop.'
Drop the 'View in web' link (and 'Reopen' variant) from the completion
panel, which now always shows 'View strix view <run_name>'.
cryptography 49.x ships arm64-only macOS wheels (no universal2), forcing the
Intel macOS (macos-x86_64) release runner to build from sdist under
`uv sync --frozen`. Pin to 48.0.1, which still clears GHSA-537c-gmf6-5ccf
(fixed in 48.0.1) and provides a macosx_10_9_universal2 wheel.
Resolves GHSA-537c-gmf6-5ccf (vulnerable OpenSSL in cryptography wheels,
fixed in 48.0.1) and CVE-2026-59885 / CVE-2026-59886 (pyasn1 DoS via
OBJECT IDENTIFIER / REAL decoding, fixed in 0.6.4).
* feat(cli): update notifications + self-update (strix --update)
* fix(update): verify release checksum, clean up staged binary, roll back Windows rename on failure
* feat(update): 3-way pre-scan prompt (update now / not now / skip this version) + package-manager upgrade
* fix(update): never show update prompt/notice in non-interactive runs
* docs(reporting): add CVSS calibration guidance to reduce severity inflation
The create_vulnerability_report tool documents the cvss_breakdown format but
gives no guidance on choosing metric values, so findings are frequently
over-rated. Add a concise calibration block covering the most common
inflation mistakes: scoring scenarios that presuppose the attacker already
holds a stolen secret as unauthenticated (PR:N) criticals, using C:H/I:H for
single-user or read-only/enumeration impact, folding a chained worst case
into one vector, and ignoring adversary-in-the-middle or user-interaction
prerequisites.
* docs(reporting): drop 'one weakness per report' calibration bullet
* fix(report): prevent code-fence breakout in vulnerability markdown
render_vulnerability_md wrapped LLM-authored poc_script_code and code
snippet values in a fixed three-backtick fence, so a triple-backtick inside
the value closed the fence early and the rest rendered as live markdown
(headings, tracking-beacon images) in the shareable report deliverable.
Open each such block with a fence one backtick longer than the longest
backtick run in the payload (CommonMark: a block closes only on a fence at
least as long as the opener), so the content always renders verbatim. The
adjacent ```diff block is already safe (its lines are '- '/'+ ' prefixed and
so can never be a bare-backtick closing fence) and is left unchanged.
Fixes#815
* fix(report): indent multiline snippets
---------
Co-authored-by: thejesh23 <thejesh23@users.noreply.github.com>
Co-authored-by: Alex Schapiro <bearsyankees@gmail.com>
build_raw_request kept the Content-Length inherited from the captured
request, so replaying a modified body (repeat_request) emitted a request
whose declared length did not match the body — truncating the payload or
stalling the target. Drop any inherited Content-Length (case-insensitively)
and recompute it from the body actually being sent.
Adds tests covering a lengthened body, an emptied body, and the
no-inherited-header path.
Fixes#814
Co-authored-by: thejesh23 <thejesh23@users.noreply.github.com>
An httpx.Timeout in ModelSettings.extra_args crashes
ModelSettings.to_json_dict() (PydanticSerializationError) on the Chat
Completions and LiteLLM model paths, which serialize settings for their
tracing generation span — failing every model turn on those paths. Pass
the timeout as a plain float, which httpx-based clients apply as the
read (inactivity) timeout.