mirror of
https://github.com/usestrix/strix.git
synced 2026-08-20 10:33:34 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cc9fa9faa | ||
|
|
174c16fa26 | ||
|
|
94a2586aaa | ||
|
|
372e27fa17 | ||
|
|
ad727edd66 | ||
|
|
7b3c8f9b74 | ||
|
|
ae07af6159 | ||
|
|
649a2e2140 |
@@ -117,6 +117,21 @@ ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
|
|||||||
ENV AGENT_BROWSER_USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
ENV AGENT_BROWSER_USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||||
ENV AGENT_BROWSER_ARGS="--disable-blink-features=AutomationControlled,--no-first-run,--no-default-browser-check,--lang=en-US"
|
ENV AGENT_BROWSER_ARGS="--disable-blink-features=AutomationControlled,--no-first-run,--no-default-browser-check,--lang=en-US"
|
||||||
ENV AGENT_BROWSER_SCREENSHOT_DIR=/workspace/.agent-browser-screenshots
|
ENV AGENT_BROWSER_SCREENSHOT_DIR=/workspace/.agent-browser-screenshots
|
||||||
|
ENV AGENT_BROWSER_IDLE_TIMEOUT_MS=180000
|
||||||
|
USER root
|
||||||
|
RUN set -eu; \
|
||||||
|
{ \
|
||||||
|
for var in AGENT_BROWSER_EXECUTABLE_PATH AGENT_BROWSER_USER_AGENT \
|
||||||
|
AGENT_BROWSER_ARGS AGENT_BROWSER_SCREENSHOT_DIR \
|
||||||
|
AGENT_BROWSER_IDLE_TIMEOUT_MS; do \
|
||||||
|
eval "value=\${$var}"; \
|
||||||
|
printf 'export %s="${%s:-%s}"\n' "$var" "$var" "$value"; \
|
||||||
|
done; \
|
||||||
|
} > /tmp/agent-browser.sh; \
|
||||||
|
install -m 0644 /tmp/agent-browser.sh /etc/profile.d/agent-browser.sh; \
|
||||||
|
rm /tmp/agent-browser.sh; \
|
||||||
|
env -i bash -lc 'test "${AGENT_BROWSER_IDLE_TIMEOUT_MS}" = "180000"'
|
||||||
|
USER pentester
|
||||||
RUN /home/pentester/.npm-global/bin/agent-browser doctor --offline --quick
|
RUN /home/pentester/.npm-global/bin/agent-browser doctor --offline --quick
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "1.5.2"
|
version = "1.5.3"
|
||||||
description = "Open-source AI Hackers for your apps"
|
description = "Open-source AI Hackers for your apps"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -263,7 +263,13 @@ Remember: A single well-validated high-impact vulnerability is worth more than d
|
|||||||
<multi_agent_system>
|
<multi_agent_system>
|
||||||
AGENT ISOLATION & SANDBOXING:
|
AGENT ISOLATION & SANDBOXING:
|
||||||
- All agents run in the same shared Docker container for efficiency
|
- All agents run in the same shared Docker container for efficiency
|
||||||
- Each agent has its own: browser sessions, terminal sessions
|
- Each agent has its own terminal sessions
|
||||||
|
- Browsers are NOT per-agent by default: `agent-browser` with no `--session` is one
|
||||||
|
shared browser, so a concurrent agent's navigation invalidates your page and refs.
|
||||||
|
Pass `--session <your-agent-name>` for any browser work of your own — then it is
|
||||||
|
yours alone. Each session is a full Chromium (~340 MB) on this shared box, so keep
|
||||||
|
one, not several, and `agent-browser --session <name> close` when you're done with
|
||||||
|
the target; an idle browser is reclaimed automatically after 3 minutes
|
||||||
- All agents share the same /workspace directory and proxy history
|
- All agents share the same /workspace directory and proxy history
|
||||||
- Agents can see each other's files and proxy traffic for better collaboration
|
- Agents can see each other's files and proxy traffic for better collaboration
|
||||||
|
|
||||||
|
|||||||
@@ -652,27 +652,31 @@ def _install_openrouter_stream_cost_capture() -> None:
|
|||||||
litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc]
|
litellm.OpenrouterConfig = _StrixOpenrouterConfig # type: ignore[misc]
|
||||||
|
|
||||||
|
|
||||||
_OPENROUTER_ATTRIBUTION_HEADERS = {
|
OPENROUTER_ATTRIBUTION_HEADERS = {
|
||||||
"HTTP-Referer": "https://strix.ai",
|
"HTTP-Referer": "https://strix.ai",
|
||||||
"X-Title": "Strix",
|
"X-Title": "Strix",
|
||||||
"X-OpenRouter-Categories": "cli-agent",
|
"X-OpenRouter-Categories": "cli-agent",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_openrouter_model(model_name: str | None) -> bool:
|
||||||
|
return bool(model_name) and "openrouter/" in (model_name or "").strip().lower()
|
||||||
|
|
||||||
|
|
||||||
def _configure_openrouter_attribution(model_name: str | None) -> None:
|
def _configure_openrouter_attribution(model_name: str | None) -> None:
|
||||||
import litellm
|
import litellm
|
||||||
|
|
||||||
current: object = litellm.headers
|
current: object = litellm.headers
|
||||||
existing: dict[str, str] = current if isinstance(current, dict) else {}
|
existing: dict[str, str] = current if isinstance(current, dict) else {}
|
||||||
if not model_name or "openrouter/" not in model_name.strip().lower():
|
if not is_openrouter_model(model_name):
|
||||||
if any(key in existing for key in _OPENROUTER_ATTRIBUTION_HEADERS):
|
if any(key in existing for key in OPENROUTER_ATTRIBUTION_HEADERS):
|
||||||
remaining = {
|
remaining = {
|
||||||
k: v for k, v in existing.items() if k not in _OPENROUTER_ATTRIBUTION_HEADERS
|
k: v for k, v in existing.items() if k not in OPENROUTER_ATTRIBUTION_HEADERS
|
||||||
}
|
}
|
||||||
litellm.headers = remaining or None # type: ignore[assignment]
|
litellm.headers = remaining or None # type: ignore[assignment]
|
||||||
return
|
return
|
||||||
|
|
||||||
litellm.headers = {**existing, **_OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
|
litellm.headers = {**existing, **OPENROUTER_ATTRIBUTION_HEADERS} # type: ignore[assignment]
|
||||||
|
|
||||||
|
|
||||||
def _configure_extra_headers(llm: LlmSettings) -> None:
|
def _configure_extra_headers(llm: LlmSettings) -> None:
|
||||||
|
|||||||
+17
-2
@@ -10,10 +10,12 @@ from openai.types.shared import Reasoning
|
|||||||
|
|
||||||
from strix.config.models import (
|
from strix.config.models import (
|
||||||
DEFAULT_MODEL_RETRY,
|
DEFAULT_MODEL_RETRY,
|
||||||
|
OPENROUTER_ATTRIBUTION_HEADERS,
|
||||||
bedrock_route_supports_prompt_caching,
|
bedrock_route_supports_prompt_caching,
|
||||||
is_bedrock_route,
|
is_bedrock_route,
|
||||||
is_claude_model,
|
is_claude_model,
|
||||||
is_known_openai_bare_model,
|
is_known_openai_bare_model,
|
||||||
|
is_openrouter_model,
|
||||||
model_supports_reasoning,
|
model_supports_reasoning,
|
||||||
request_timeout_extra_args,
|
request_timeout_extra_args,
|
||||||
)
|
)
|
||||||
@@ -201,13 +203,15 @@ def make_model_settings(
|
|||||||
request_timeout: float | None = None,
|
request_timeout: float | None = None,
|
||||||
prompt_cache: bool = True,
|
prompt_cache: bool = True,
|
||||||
extra_headers: dict[str, str] | None = None,
|
extra_headers: dict[str, str] | None = None,
|
||||||
|
has_tools: bool = True,
|
||||||
) -> ModelSettings:
|
) -> ModelSettings:
|
||||||
|
headers = _request_headers(model_name, extra_headers)
|
||||||
model_settings = ModelSettings(
|
model_settings = ModelSettings(
|
||||||
parallel_tool_calls=False,
|
parallel_tool_calls=False if has_tools else None,
|
||||||
retry=DEFAULT_MODEL_RETRY,
|
retry=DEFAULT_MODEL_RETRY,
|
||||||
include_usage=True,
|
include_usage=True,
|
||||||
extra_args=request_timeout_extra_args(request_timeout),
|
extra_args=request_timeout_extra_args(request_timeout),
|
||||||
extra_headers=dict(extra_headers) if extra_headers else None,
|
extra_headers=headers,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
reasoning_effort is not None
|
reasoning_effort is not None
|
||||||
@@ -230,6 +234,17 @@ def make_model_settings(
|
|||||||
return model_settings
|
return model_settings
|
||||||
|
|
||||||
|
|
||||||
|
def _request_headers(
|
||||||
|
model_name: str, extra_headers: dict[str, str] | None
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
headers: dict[str, str] = {}
|
||||||
|
if is_openrouter_model(model_name):
|
||||||
|
headers.update(OPENROUTER_ATTRIBUTION_HEADERS)
|
||||||
|
if extra_headers:
|
||||||
|
headers.update(extra_headers)
|
||||||
|
return headers or None
|
||||||
|
|
||||||
|
|
||||||
def _reasoning_settings(
|
def _reasoning_settings(
|
||||||
effort: ReasoningEffort,
|
effort: ReasoningEffort,
|
||||||
extra_args: dict[str, Any] | None,
|
extra_args: dict[str, Any] | None,
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ async def warm_up_llm(show_model_warning: bool = True) -> None:
|
|||||||
request_timeout=llm.timeout,
|
request_timeout=llm.timeout,
|
||||||
prompt_cache=False,
|
prompt_cache=False,
|
||||||
extra_headers=settings.dedupe.extra_headers,
|
extra_headers=settings.dedupe.extra_headers,
|
||||||
|
has_tools=False,
|
||||||
)
|
)
|
||||||
if deduper_extra:
|
if deduper_extra:
|
||||||
merged = {**(deduper_settings.extra_args or {}), **deduper_extra}
|
merged = {**(deduper_settings.extra_args or {}), **deduper_extra}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ async def preflight_model_connection(
|
|||||||
request_timeout=resolved_settings.llm.timeout,
|
request_timeout=resolved_settings.llm.timeout,
|
||||||
prompt_cache=False,
|
prompt_cache=False,
|
||||||
extra_headers=resolved_settings.llm.extra_headers,
|
extra_headers=resolved_settings.llm.extra_headers,
|
||||||
|
has_tools=False,
|
||||||
)
|
)
|
||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
model.get_response(
|
model.get_response(
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ async def _summarize(model: str, prompt: str, max_tokens: int) -> str | None:
|
|||||||
request_timeout=llm.timeout,
|
request_timeout=llm.timeout,
|
||||||
prompt_cache=False,
|
prompt_cache=False,
|
||||||
extra_headers=llm.extra_headers,
|
extra_headers=llm.extra_headers,
|
||||||
|
has_tools=False,
|
||||||
).resolve(ModelSettings(max_tokens=max_tokens))
|
).resolve(ModelSettings(max_tokens=max_tokens))
|
||||||
try:
|
try:
|
||||||
response = (
|
response = (
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ def _dedupe_model_settings(
|
|||||||
# must never receive the main endpoint's credentials. A dedicated model
|
# must never receive the main endpoint's credentials. A dedicated model
|
||||||
# gets its own DEDUPE_LLM_EXTRA_HEADERS instead.
|
# gets its own DEDUPE_LLM_EXTRA_HEADERS instead.
|
||||||
extra_headers=dedupe.extra_headers if dedupe.model else llm.extra_headers,
|
extra_headers=dedupe.extra_headers if dedupe.model else llm.extra_headers,
|
||||||
|
has_tools=False,
|
||||||
)
|
)
|
||||||
extra = _dedupe_extra_args(dedupe)
|
extra = _dedupe_extra_args(dedupe)
|
||||||
if extra:
|
if extra:
|
||||||
|
|||||||
@@ -58,6 +58,26 @@ agent-browser screenshot
|
|||||||
The browser stays running across commands so these feel like a single
|
The browser stays running across commands so these feel like a single
|
||||||
session. Use `agent-browser close` (or `close --all`) when you're done.
|
session. Use `agent-browser close` (or `close --all`) when you're done.
|
||||||
|
|
||||||
|
The default session is **shared with every other agent in the sandbox** — if
|
||||||
|
another agent navigates it, your page and your refs are gone from under you. So
|
||||||
|
claim your own by passing `--session <your-agent-name>` on **every** command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser --session recon-3 open https://example.com
|
||||||
|
agent-browser --session recon-3 snapshot -i
|
||||||
|
agent-browser --session recon-3 close # when done with the target
|
||||||
|
```
|
||||||
|
|
||||||
|
The examples in the rest of this skill omit `--session` to keep them readable;
|
||||||
|
keep passing yours. Each session is a separate Chromium (~340 MB) on a shared
|
||||||
|
box, so hold one rather than several, and close it when you're finished.
|
||||||
|
|
||||||
|
A browser left idle for 3 minutes is reclaimed automatically to free memory for
|
||||||
|
the other agents; the next command relaunches it, but the page, tabs, refs and
|
||||||
|
cookies are gone. If you're authenticated and about to go do something else for a
|
||||||
|
while, save the state first (see
|
||||||
|
[Persist session across runs](#persist-session-across-runs)).
|
||||||
|
|
||||||
## Reading a page
|
## Reading a page
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -307,6 +327,16 @@ agent-browser --session b fill @e1 "bob@test.com"
|
|||||||
`AGENT_BROWSER_SESSION=myapp` sets the default session for the current
|
`AGENT_BROWSER_SESSION=myapp` sets the default session for the current
|
||||||
shell.
|
shell.
|
||||||
|
|
||||||
|
Use a session named after yourself for your own work — that's what keeps a
|
||||||
|
concurrent agent from navigating the page out from under you. Every session is a
|
||||||
|
separate Chromium though, so hold one at a time rather than a collection, and
|
||||||
|
close each one when its flow is finished:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
agent-browser --session a close
|
||||||
|
agent-browser --session b close
|
||||||
|
```
|
||||||
|
|
||||||
### Mock network requests
|
### Mock network requests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -368,8 +398,11 @@ agent-browser dialog dismiss # cancel
|
|||||||
## Readiness & recovery
|
## Readiness & recovery
|
||||||
|
|
||||||
The first `agent-browser open` in a session launches the headless-Chrome
|
The first `agent-browser open` in a session launches the headless-Chrome
|
||||||
daemon; later commands reuse it. Distinguish the two failure modes and react
|
daemon; later commands reuse it. A daemon left idle for 3 minutes shuts itself
|
||||||
differently — do **not** blindly re-run the same failing command in a loop:
|
down to free memory for the other agents, so an `open` after a long gap is a
|
||||||
|
fresh browser rather than a resumed one — expect to re-navigate, and re-`state
|
||||||
|
load` if you were logged in. Distinguish the 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`,
|
- **Daemon / connection failure** (`Failed to connect`, `connection refused`,
|
||||||
socket missing, `browser not running`): the daemon isn't up or has died. Run
|
socket missing, `browser not running`): the daemon isn't up or has died. Run
|
||||||
|
|||||||
@@ -299,6 +299,16 @@ def test_make_model_settings_forces_required_for_anyllm_routed_openai_model() ->
|
|||||||
assert settings.tool_choice == "required"
|
assert settings.tool_choice == "required"
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_model_settings_disables_parallel_tool_calls_by_default() -> None:
|
||||||
|
assert make_model_settings("none", model_name="gpt-4o").parallel_tool_calls is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_model_settings_omits_parallel_tool_calls_without_tools() -> None:
|
||||||
|
settings = make_model_settings("none", model_name="gpt-4o", has_tools=False)
|
||||||
|
|
||||||
|
assert settings.parallel_tool_calls is None
|
||||||
|
|
||||||
|
|
||||||
def test_make_model_settings_sets_request_timeout() -> None:
|
def test_make_model_settings_sets_request_timeout() -> None:
|
||||||
settings = make_model_settings(
|
settings = make_model_settings(
|
||||||
"none",
|
"none",
|
||||||
@@ -351,3 +361,32 @@ def test_make_model_settings_timeout_survives_reasoning_resolve() -> None:
|
|||||||
|
|
||||||
assert settings.extra_args is not None
|
assert settings.extra_args is not None
|
||||||
assert settings.extra_args["timeout"] == 120.0
|
assert settings.extra_args["timeout"] == 120.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrouter_attribution_rides_on_the_request_headers() -> None:
|
||||||
|
# litellm.headers is ignored once a request carries any header of its own,
|
||||||
|
# so the attribution must be part of the per-request headers.
|
||||||
|
headers = make_model_settings(
|
||||||
|
None, model_name="openrouter/anthropic/claude-sonnet-4-5"
|
||||||
|
).extra_headers
|
||||||
|
assert headers == {
|
||||||
|
"HTTP-Referer": "https://strix.ai",
|
||||||
|
"X-Title": "Strix",
|
||||||
|
"X-OpenRouter-Categories": "cli-agent",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrouter_attribution_absent_for_other_providers() -> None:
|
||||||
|
assert make_model_settings(None, model_name="anthropic/claude-sonnet-4-5").extra_headers is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_headers_override_openrouter_attribution() -> None:
|
||||||
|
headers = make_model_settings(
|
||||||
|
None,
|
||||||
|
model_name="openrouter/anthropic/claude-sonnet-4-5",
|
||||||
|
extra_headers={"X-Title": "Custom", "X-Tenant": "acme"},
|
||||||
|
).extra_headers
|
||||||
|
assert headers is not None
|
||||||
|
assert headers["X-Title"] == "Custom"
|
||||||
|
assert headers["X-Tenant"] == "acme"
|
||||||
|
assert headers["HTTP-Referer"] == "https://strix.ai"
|
||||||
|
|||||||
@@ -2378,7 +2378,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strix-agent"
|
name = "strix-agent"
|
||||||
version = "1.5.2"
|
version = "1.5.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "caido-sdk-client" },
|
{ name = "caido-sdk-client" },
|
||||||
|
|||||||
Reference in New Issue
Block a user