Restore exact upstream file contents

This commit is contained in:
maxi-maxima 2026-08-12 10:26:01 +08:00
parent 02ff1d0a28
commit 46bbdcacb0
7 changed files with 665 additions and 171 deletions

View file

@ -1,17 +1,176 @@
­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëKKKB<4B>]Nˆ<08>ÛÛ™šYÝ\˜][Ûˆ‚™\ØÜš\[ÛŽˆ‘[<5B>š\›Û›Y[<5B>˜\šXX›\ț܈ݚ^‚‹KKB‚<42>ÛÛ™šYÝ\™HÝš^\Ú[™È[<5B>š\›Û›Y[<5B>˜\šXX›\È܈HÛÛ™šYÈš[K‚‚ˆÈÈHÛÛ™šYÝ\˜][Û‚‚<E2809A>\˜[QšY[]H”Õ’VÓHˆ\OHœÝš[™Èˆ™\]Z\™Y‚ˆ[Ù[˜[YH[ˆ]SH›Ü›X]
K™Ë‹Ü[˜ZKÙÜ MK<4D> [<5B>›ÜXËØÛ]YK\ÛÛ›™] M M˜
K‚<EFBFBD> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H“WÐTWÒÑVHˆ\OHœÝš[™È<E284A2>‚ˆTHÙ^H›Üˆ[Ý\ˆH›ÝšY\‹ˆ›Ý™\]Z\™Y›ÜˆØØ[[Ù[È܈ÛÝY›ÝšY\ˆ]]
™\<5C>^RKUÔÈ™Y›ØÚÊK‚<4B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H“WÐTWÐ<57>TÑHˆ\OHœÝš[™È<E284A2>‚ˆÝ\ÝÛHTH˜\ÙHT“ ˆ[ÛÈXØÙ\ÈÔS<>RWÐTWÐ<57>TÑX USWÐ<57>TÑWÕT“ ܈ÓSPWÐTWÐ<57>TÑX ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H“WÑV<14>WÒPQT”Ȉ\OHœÝš[™È<E284A2>‚ˆ^˜HXY\œÈÙ[<5B>Ûˆ]™\žHH™\]Y\Ý \ÈH”Óӈؚ™XÝ
K™Ë‚ˆÈ– Q™X]\™KRÙ^HŽˆ<C5BD>˜[YH‹– U[˜[<5B>Žˆ˜XÛYHŸX
Kˆ\ÙY<C399>[›ÜˆÜ[<5B>RKXÛÛ\]X›BˆØ]]Ø^\È]™\]Z\™H]šX<C5A1>][Ûˆ܈›Ý][™ÈXY\œÈ[ˆY][ÛˆÈH™X\™\‚ˆÚÙ[‹ˆH™X\™\ˆÚÙ[ˆ]Ù[ˆÝ[ÛÛY\Èœ›ÛHWÐTWÒÑVX ˆ\Y\Èț݈H]SH[™˜]]™HÜ[<5B>RH›Ý][™È]Ë‚<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H“WÕSQSÕUˆY˜][HŒÌ ˆ\OHš[<5B>YÙ\ˆ<>‚ˆ™\]Y\Ý[Y[Ý][ˆÙXÛۙț܈HØ[Ë‚<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÓWÓPVÔ‘U’QTȈY˜][H<>Hˆ\OHš[<5B>YÙ\ˆ<>‚ˆX^[][H<1B>[X™\ˆÙˆ™]šY\ț܈HTHØ[ÈÛˆ˜[œÚY[<5B>˜Z[\™\Ë‚<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆY˜][HšYÚˆ\OHœÝš[™È<E284A2>‚ˆÛÛ<C39B>›Û[šÚ[™ÈY™›Ü<E280BA>›Üˆ™X\ÛÛš[™È[Ù[ˈ˜[Y˜[Y\Έ›Û™X Z[š[X[ ÝØ YY][X YÚ YÚ X^ ˆY˜][ÈÈYY][X›Üˆ]ZXÚÈØØ[ˆ[ÙK‚<4B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÓQSSÔ–WÐÓÓT‘TÔÓÔ—ÕSQSÕUˆY˜][HŒÌˆ\OHš[<5B>YÙ\ˆ<>‚ˆ[Y[Ý][ˆÙXÛۙț܈Y[[ÜžHÛÛ\™\ÜÚ[ÛˆÜ\˜][ÛœÈ
ÛÛ<C39B>^Ý[[X\š^˜][ÛŠK‚<4B> Ô\˜[QšY[‚‚ˆÈÈÈYXØ]YY\XØ][Ûˆ[Ù[‚‘š[™[™ÈY\XØ][Ûˆ\ÈHÚX\ Ý<1C>XÝ\™YÛ\ÜÚYšXØ][Ûˆ\ÚˈžHY˜][]œ<>[œÈÛˆHXZ[ˆ[Ù[ <18>][ÝHØ[ˆ›Ý]H]ÈHÛX[\‹ØÚX\\ˆ[Ù[Ú]Ý]˜Y™™XÝ[™ÈHYÙ[<5B>È]ÈHXÝX[\Ý[™Ë‚‚<E2809A>\˜[QšY[]H”Õ’VÑQTWÓSÑSˆ\OHœÝš[™È<E284A2>‚ˆ[Ù[\ÙYÈ<1A>YÙHÚ]\ˆHØ[™Y]Hš[™[™È\XØ]\È[ˆ^\Ý[™È™\Ü<> ‚ˆ˜[ȘXÚÈÈÕ’VÓXÚ[ˆ[œÙ] ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H‘QTWÓWÐTWÒÑVHˆ\OHœÝš[™È<E284A2>‚ˆÜ[Û˜[›ÝšY\ˆÙ^H›ÜˆHY\XØ][Ûˆ[Ù[ ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H‘QTWÓWÐTWÐ<57>TÑHˆ\OHœÝš[™È<E284A2>‚ˆÜ[Û˜[Ý\ÝÛHTH˜\ÙHT“›ÜˆHY\XØ][Ûˆ[Ù[ ˆ\ÙHÚ[ˆHY\Bˆ[Ù[<1C>[œÈÛˆHY™™\™[<5B>[™Ú[<5B>[ˆHXZ[ˆ[Ù[ ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H‘QTWÓWÑV<14>WÒPQT”Ȉ\OHœÝš[™È<E284A2>‚ˆÜ[Û˜[”Óӈؚ™XÝÙˆ^˜HXY\œÈÙ[<5B>Ûˆ]™\žHY\XØ][Û‹[[Ù[ˆ™\]Y\Ý K™ËˆÈ– Q™X]\™KRÙ^HŽˆ<C5BD>˜[YHŸX ˆHYXØ]YY\H[Ù[™]™\‚ˆ[š\š]ÈWÑV<14>WÒPQT”ØÈÙ]\ÈÚ[ˆ]È[™Ú[<5B>™YYÈÝ\ÝÛHXY\œË‚<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÑQTWÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆ\OHœÝš[™È<E284A2>‚ˆ™X\ÛÛš[™ÈY™›Ü<E280BA>›ÜˆHY\XØ][Ûˆ[Ù[ ˆY˜][ÈÈH[Ù[ ÜÈÝÛ‚ˆ˜\Ù[[™HÚ[ˆ[œÙ] ‚<> Ô\˜[QšY[‚‚ˆÈÈÜ[Û˜[™X]\™\‚<C382>\˜[QšY[]H”T”VUWÐTWÒÑVHˆ\OHœÝš[™È<E284A2>‚ˆTHÙ^H›Üˆ\œ^]HRKˆ[˜X›\È™X[ ][YHÙXˆÙX\˜Ú\š[™ÈØØ[œÈ›ÜˆÔÒS•[™<1D>[™\˜Xš[]H™\ÙX\˜Ú ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”ÔÕPS—ÐTWÒÑVHˆ\OHœÝš[™È<E284A2>‚ˆÜÝX[ˆTHÙ^H
PRËx )˜
Kˆ[˜X›\È™]Ú[™ÈÜÝX[ˆÛÛXÝ[ۜȞHY\ÈH\™Ù]
ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY˜
K[™ÜÝX[ˆ[<5B>š\›Û›Y[<5B>È
ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY<0F>Ù[<5B><>O[<5B>š\›Û›Y[<5B> ]ZY˜
HÈ™\ÛÛ™HÛÛXÝ[Ûˆ˜\šXX›\ˈ›Ý™YYYÚ[ˆ\ÜÚ[™ÈHØØ[ÛÛXÝ[Ûˆ^Ü<>š[K‚<4B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÕSSQU–HˆY˜][HŒHˆ\OHœÝš[™È<E284A2>‚ˆ[[Y]žHÙÙÛKˆÙ]È  ˜[ÙX ›Ø ܈Ù™˜È\ØX›H[[Y]žH
ÜÝÙËØØ\™‹ÕS
K‚<EFBFBD> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H•<14>PÑSÓÔÐ<>TÑWÕT“ˆ\OHœÝš[™È<E284A2>‚ˆÕ Õ˜XÙ[Ûܘ\ÙHT“›Üˆ™[[ÝHÜ[•[[Y]žH^Ü<> ˆYˆ[œÙ] Ýš^ÙY\ȘXÙ\ÈØØ[Û›K‚<4B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H•<14>PÑSÓÔÐTWÒÑVHˆ\OHœÝš[™È<E284A2>‚ˆTHÙ^H\ÙY›Üˆ™[[ÝH˜XÙH^Ü<> ˆ™[[ÝH^Ü<>\È[˜X›YÛ›HÚ[ˆ›Ý<14>PÑSÓÔÐ<>TÑWÕT“[™<14>PÑSÓÔÐTWÒÑVX\™HÙ] ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H•<14>PÑSÓÔÒPQT”Ȉ\OHœÝš[™È<E284A2>‚ˆÜ[Û˜[Ý\ÝÛHÕSXY\œÈ
”Óӈؚ™XÝ܈Ù^O]˜[YKÙ^L<>]˜[YL˜
Kˆ\ÙY<C399>[›Üˆ[™Ù<E284A2>\ÙH܈Ý\ÝÛKÜÙ[‹ZÜÝYÕØ]]Ø^\Ë‚<C38B> Ô\˜[QšY[‚‚•Ú[ˆ™[[ÝHÕS˜\œÈ\™H›ÝÙ] Ýš^Ý[Üš]\ÈÛÛ\]H<1C>[ˆ[[Y]žHØØ[H΂‚˜˜\ڜݚ^Ü<>[œËÏ<1C>[—Û˜[YO‹Ù]™[<5B>ËšœÛÛ›˜‚•Ú[ˆ™[[ÝH˜\œÈ\™HÙ] Ýš^X[ ]Üš]\È[[Y]žHÈ›ÝØØ[”ÓÓ“[™H™[[ÝHÕS[™Ú[<5B> ‚‚ˆÈÈØÚÙ\ˆÛÛ™šYÝ\˜][Û‚‚<E2809A>\˜[QšY[]H”Õ’VÒSPQÑHˆY˜][H™ÚÜ‹š[ËÝ\Ù\Ýš^ ÜÝš^ \Ø[™›ÞŒKŒËŒˆ\OHœÝš[™È<E284A2>‚ˆØÚÙ\ˆ[XYÙHÈ\ÙH›ÜˆHØ[™›ÞÛÛ<C39B>Z[™\‹‚<E280B9> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÒSPQÑWÔSÔÓPÖHˆY˜][H˜]]Ȉ\OHœÝš[™È<E284A2>‚ˆÛÛ<C39B>›ÛÈØ[™›Þ[XYÙHXÜ]Z\Ú][Û‹ˆ\ÙH™]™\˜È™\]Z\™HH™[ØYY[XYÙBˆ[™˜Z[Ú]Ý][[™ÈÚ[ˆ]\È[˜]˜Z[X›NÈ]]Ø™\Ù\<5C>™\ÈHY˜][ˆš\œÝ \<5C>[ˆ[™Z]š[Ü‹ˆ\ÈÙ\țݙ\ÝšXÝ™]ÛÜšÈXØÙ\ÜÈœ›ÛHH<1C>[›š[™ÂˆØ[™›Þ ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H‘ÐÒÑT—ÒÔÕˆ\OHœÝš[™È<E284A2>‚ˆØÚÙ\ˆY[[ÛˆÛØÚÙ]] ˆ\ÙH›Üˆ™[[ÝHØÚÙ\ˆÜÝÈ܈Ý\ÝÛHÛÛ™šYÝ\˜][ۜ˂<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÔ•S•SQWÐ<57>PÒÑS‘ˆY˜][H™ØÚÙ\ˆˆ\OHœÝš[™È<E284A2>‚ˆ<14>[<5B>[YH˜XÚÙ[™›ÜˆHØ[™›Þ[<5B>š\›Û›Y[<5B> ‚<> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÐÐRQ×ГÓÕÕÐRUÔȈY˜][HŒÌ ˆ\OHš[<5B>YÙ\ˆ<>‚ˆX^[][H[YH[ˆÙXÛÛ™ÈÈØZ]›ÜˆH[‹\Ø[™›ÞØZYÈ›ÞHÈ™XÛÛYH™XYK‚ˆ[˜Ü™X\ÙH\ÈÛˆÛÝÈ܈X]š[HØYYÛÛ<C39B>Z[™\ˆÜÝË‚<C38B> Ô\˜[QšY[‚‚ˆÈÈØ[™›ÞÛÛ™šYÝ\˜][Û‚‚<E2809A>\˜[QšY[]H”Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUˆY˜][HŒLŒˆ\OHš[<5B>YÙ\ˆ<>‚ˆX^[][H^XÝ][Ûˆ[YH[ˆÙXÛۙț܈Ø[™›ÞÜ\˜][ۜ˂<C38B> Ô\˜[QšY[‚‚<E2809A>\˜[QšY[]H”Õ’VÔÐS‘“ÖÐÓÓ“‘PÕÕSQSÕUˆY˜][HŒLˆ\OHš[<5B>YÙ\ˆ<>‚ˆ[Y[Ý][ˆÙXÛۙț܈ÛÛ›™XÝ[™ÈÈHØ[™›ÞÛÛ<C39B>Z[™\‹‚<E280B9> Ô\˜[QšY[‚‚ˆÈÈÛÛ™šYÈš[B‚”Ýš^ÝÜ™\ÈÛÛ™šYÝ\˜][Ûˆ[ˆ‹ËœÝš^ ØÛKXÛÛ™šYËšœÛÛ˜ ˆ[ÝHØ[ˆ[ÛÈÜXÚYžHHÝ\ÝÛHÛÛ™šYÈš[N‚‚˜˜\ڜݚ^ K]\™Ù] ‹Ø\ KXÛÛ™šYÈ Ü] ÝËØÛÛ™šYËšœÛÛ‚˜‚ŠŠ<C5A0>ÛÛ™šYÈš[H›Ü›X]ŠŠ‚‚˜œÛÛ‚žÂˆ™[<5B>ˆŽˆˆ”Õ’VÓHŽˆ›Ü[˜ZKÙÜ MK<4D>‹ˆ“WÐTWÒÑVHŽˆœÚËK‹‹ˆ‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ŽˆšYÚ‚ˆBŸB˜‚ˆÈÈ^[\HÙ]\‚˜˜\ڈș\]Z\™Y™^Ü<>Õ’VÓOH›Ü[˜ZKÙÜ MK<4D>‚™^Ü<>WÐTWÒÑVOHœÚËK‹‹ˆ‚‚ˆÈÜ[Û˜[ˆ[˜X›HÙXˆÙX\˜Ú™^Ü<>T”VUWÐTWÒÑVOHœ K‹‹ˆ‚‚ˆÈÜ[Û˜[ˆÝ\ÝÛH[Y[Ý]™^Ü<>WÕSQSÕUH<>Œ ‚™^Ü<>Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUHŒÌ ‚‚˜
---
title: "Configuration"
description: "Environment variables for Strix"
---
Configure Strix using environment variables or a config file.
## LLM Configuration
<ParamField path="STRIX_LLM" type="string" required>
Model name in LiteLLM format (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`).
</ParamField>
<ParamField path="LLM_API_KEY" type="string">
API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock).
</ParamField>
<ParamField path="LLM_API_BASE" type="string">
Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`.
</ParamField>
<ParamField path="LLM_EXTRA_HEADERS" type="string">
Extra HTTP headers sent on every LLM request, as a JSON object (e.g.
`{"X-Feature-Key":"value","X-Tenant":"acme"}`). Useful for OpenAI-compatible
gateways that require attribution or routing headers in addition to the bearer
token. The bearer token itself still comes from `LLM_API_KEY`. Applies to both
the LiteLLM and native OpenAI routing paths.
</ParamField>
<ParamField path="LLM_TIMEOUT" default="300" type="integer">
Request timeout in seconds for LLM calls.
</ParamField>
<ParamField path="STRIX_LLM_MAX_RETRIES" default="5" type="integer">
Maximum number of retries for LLM API calls on transient failures.
</ParamField>
<ParamField path="STRIX_REASONING_EFFORT" default="high" type="string">
Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Defaults to `medium` for quick scan mode.
</ParamField>
<ParamField path="STRIX_MEMORY_COMPRESSOR_TIMEOUT" default="30" type="integer">
Timeout in seconds for memory compression operations (context summarization).
</ParamField>
### Dedicated deduplication model
Finding deduplication is a cheap, structured classification task. By default it
runs on the main model, but you can route it to a smaller/cheaper model without
affecting the agents that do the actual testing.
<ParamField path="STRIX_DEDUPE_MODEL" type="string">
Model used to judge whether a candidate finding duplicates an existing report.
Falls back to `STRIX_LLM` when unset.
</ParamField>
<ParamField path="DEDUPE_LLM_API_KEY" type="string">
Optional provider key for the deduplication model.
</ParamField>
<ParamField path="DEDUPE_LLM_API_BASE" type="string">
Optional custom API base URL for the deduplication model. Use when the dedupe
model runs on a different endpoint than the main model.
</ParamField>
<ParamField path="DEDUPE_LLM_EXTRA_HEADERS" type="string">
Optional JSON object of extra HTTP headers sent on every deduplication-model
request, e.g. `{"X-Feature-Key":"value"}`. A dedicated dedupe model never
inherits `LLM_EXTRA_HEADERS`; set this when its endpoint needs custom headers.
</ParamField>
<ParamField path="STRIX_DEDUPE_REASONING_EFFORT" type="string">
Reasoning effort for the deduplication model. Defaults to the model's own
baseline when unset.
</ParamField>
## Optional Features
<ParamField path="PERPLEXITY_API_KEY" type="string">
API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research.
</ParamField>
<ParamField path="POSTMAN_API_KEY" type="string">
Postman API key (`PMAK-…`). Enables fetching Postman collections by id as a target (`postman://<collection-uid>`), and Postman environments (`postman://<collection-uid>?env=<environment-uid>`) to resolve collection variables. Not needed when passing a local collection export file.
</ParamField>
<ParamField path="STRIX_TELEMETRY" default="1" type="string">
Telemetry toggle. Set to `0`, `false`, `no`, or `off` to disable telemetry (PostHog, Scarf, OTEL).
</ParamField>
<ParamField path="TRACELOOP_BASE_URL" type="string">
OTLP/Traceloop base URL for remote OpenTelemetry export. If unset, Strix keeps traces local only.
</ParamField>
<ParamField path="TRACELOOP_API_KEY" type="string">
API key used for remote trace export. Remote export is enabled only when both `TRACELOOP_BASE_URL` and `TRACELOOP_API_KEY` are set.
</ParamField>
<ParamField path="TRACELOOP_HEADERS" type="string">
Optional custom OTEL headers (JSON object or `key=value,key2=value2`). Useful for Langfuse or custom/self-hosted OTLP gateways.
</ParamField>
When remote OTEL vars are not set, Strix still writes complete run telemetry locally to:
```bash
strix_runs/<run_name>/events.jsonl
```
When remote vars are set, Strix dual-writes telemetry to both local JSONL and the remote OTEL endpoint.
## Docker Configuration
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:1.3.0" type="string">
Docker image to use for the sandbox container.
</ParamField>
<ParamField path="STRIX_IMAGE_PULL_POLICY" default="auto" type="string">
Controls sandbox image acquisition. Use `never` to require a preloaded image
and fail without pulling when it is unavailable; `auto` preserves the default
first-run pull behavior. This does not restrict network access from a running
sandbox.
</ParamField>
<ParamField path="DOCKER_HOST" type="string">
Docker daemon socket path. Use for remote Docker hosts or custom configurations.
</ParamField>
<ParamField path="STRIX_RUNTIME_BACKEND" default="docker" type="string">
Runtime backend for the sandbox environment.
</ParamField>
## Sandbox Configuration
<ParamField path="STRIX_SANDBOX_EXECUTION_TIMEOUT" default="120" type="integer">
Maximum execution time in seconds for sandbox operations.
</ParamField>
<ParamField path="STRIX_SANDBOX_CONNECT_TIMEOUT" default="10" type="integer">
Timeout in seconds for connecting to the sandbox container.
</ParamField>
## Config File
Strix stores configuration in `~/.strix/cli-config.json`. You can also specify a custom config file:
```bash
strix --target ./app --config /path/to/config.json
```
**Config file format:**
```json
{
"env": {
"STRIX_LLM": "openai/gpt-5.4",
"LLM_API_KEY": "sk-...",
"STRIX_REASONING_EFFORT": "high"
}
}
```
## Example Setup
```bash
# Required
export STRIX_LLM="openai/gpt-5.4"
export LLM_API_KEY="sk-..."
# Optional: Enable web search
export PERPLEXITY_API_KEY="pplx-..."
# Optional: Custom timeouts
export LLM_TIMEOUT="600"
export STRIX_SANDBOX_EXECUTION_TIMEOUT="300"
```

View file

@ -1,60 +1,160 @@
­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ýš^\XØ][ÛˆÙ][™ÜÈ8 %Y[<EFBFBD>XË\Ù][™ÜÈÝÙ\™Y ˆˆˆ‚‚™œ›ÛH×Ù<EFBFBD>]\™W×È[\Ü<EFBFBD>[››Ý][ۜ‚™œ›ÛH\[™È[\Ü<EFBFBD>]\˜[‚™œ›ÛHY[<EFBFBD>XÈ[\Ü<EFBFBD>[X\ÐÚÚXÙ\ËšY[™œ›ÛHY[<EFBFBD>X×ÜÙ][™ÜÈ[\Ü<EFBFBD>˜\ÙTÙ][™ÜËÙ][™ÜÐÛÛ™šYÑXÝ‚‚”™X\ÛÛš[™ÑY™›Ü<EFBFBD>H]\˜[È››Û™H‹›Z[š[X[‹›ÝÈ‹›YY][H‹šYÚ‹žYÚ‹›X^—B‚‘Q<EFBFBD>USÓPVÕT“”ÈH L ‚—Ð<EFBFBD>TÑWÐÓÓ‘’QÈHÙ][™ÜÐÛÛ™šYÑXÝ
ˆØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKˆÜ[]WØžWÛ˜[YOU<EFBFBD>YKˆ^˜OHšYۛܙH‹ŠB‚‚˜Û\ÜÈTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ[Ù[ˆÝˆ›Û™HHšY[
Y˜][S›Û™K[X\ÏH”Õ’VÓHŠBˆ\WÚÙ^NˆÝˆ›Û™HHšY[
ˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\Ê“WÐTWÒÑVH‹“ÔS<EFBFBD>RWÐTWÒÑVHŠKˆ™\<EFBFBD>Q˜[ÙKˆ
Bˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[
ˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\ʈ“WÐTWÐ<EFBFBD>TÑH‹ˆ“ÔS<EFBFBD>RWÐTWÐ<EFBFBD>TÑH‹ˆ“ÔS<EFBFBD>RWÐ<EFBFBD>TÑWÕT“‹ˆ“USWÐ<EFBFBD>TÑWÕT“‹ˆ“ÓSPWÐTWÐ<EFBFBD>TÑH‹ˆ
Kˆ
Bˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[
ˆY˜][S›Û™Kˆ[X\ÏH“WÑV<EFBFBD>WÒPQT”È‹ˆ™\<EFBFBD>Q˜[ÙKˆ
Bˆ™X\ÛÛš[™×ÙY™›Ü<EFBFBD>ˆ™X\ÛÛš[™ÑY™›Ü<EFBFBD>HšY[
Y˜][HšYÚ‹[X\ÏH”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ŠBˆ›Ü˜ÙWÜ™\]Z\™YÝÛÛØÚÚXÙNˆ›ÛÛHšY[
ˆY˜][Q˜[ÙKˆ[X\ÏH”Õ’VÑ“Ô<EFBFBD>ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ
Bˆ›Û\ØØXÚNˆ›ÛÛHšY[
ˆY˜][U<EFBFBD>YKˆ[X\ÏH”Õ’VÔ“ÓTÐÐPÒH‹ˆ
Bˆ\ØX›WÜÝ™X[Z[™Îˆ›ÛÛHšY[
ˆY˜][Q˜[ÙKˆ[X\ÏH“WÑTÐP“WÔÕ‘PSRS‘È‹ˆ
Bˆ[Y[Ý]ˆ[<EFBFBD>HšY[
Y˜][LÌ [X\ÏH“WÕSQSÕUŠBˆÝ™X[WÚYWÝ[Y[Ý]ˆ[<EFBFBD>HšY[
Y˜][LÌ ÙOL [X\ÏH“WÔÕ‘PSWÒQWÕSQSÕUŠBˆX^ÝÛÛØØ[×Ü\—Ý\›Žˆ[<EFBFBD>HšY[
ˆY˜][LÌ‹ˆÙOL ˆ[X\ÏH“WÓPVÕÓÓÐÐS×ÔT—ÕT“ˆ‹ˆ
B‚‚˜Û\ÜÈY\TÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ[Ù[ˆÝˆ›Û™HHšY[
Y˜][S›Û™K[X\ÏH”Õ’VÑQTWÓSÑSŠBˆ™X\ÛÛš[™×ÙY™›Ü<EFBFBD>ˆ™X\ÛÛš[™ÑY™›Ü<EFBFBD>›Û™HHšY[
ˆY˜][S›Û™Kˆ[X\ÏH”Õ’VÑQTWÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹ˆ
Bˆ\WÚÙ^NˆÝˆ›Û™HHšY[
Y˜][S›Û™K[X\ÏH‘QTWÓWÐTWÒÑVH‹™\<EFBFBD>Q˜[ÙJBˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[
Y˜][S›Û™K[X\ÏH‘QTWÓWÐTWÐ<EFBFBD>TÑHŠBˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[
ˆY˜][S›Û™Kˆ[X\ÏH‘QTWÓWÑV<EFBFBD>WÒPQT”È‹ˆ™\<EFBFBD>Q˜[ÙKˆ
B‚‚˜Û\ÜÈÛÛ<EFBFBD>^Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆˆˆ<EFBFBD>ÛÛ<EFBFBD>^ ]Ú[™ÝÈX[˜YÙ[Y[<EFBFBD>ˆ\‹]ÛÛ [Ý]]Ø\È[™\ÝÜžHÛÛ\XÝ[Û‹ˆˆˆ‚‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ]]רÛÛ\X݈›ÛÛHšY[
Y˜][U<EFBFBD>YK[X\ÏH”Õ’VÐÓÓ•VÐUU×ÐÓÓTPÕŠBˆÛÛ\XÝØ<EFBFBD>Y™™\—ÝÚÙ[œÎˆ[<EFBFBD>HšY[
Y˜][LŒÌ ÝL [X\ÏH”Õ’VÐÓÓ•VЕQ‘‘T—ÕÒÑS”ÈŠBˆÙY\ÝÚÙ[œÎˆ[<EFBFBD>HšY[
Y˜][NÌ ÝL [X\ÏH”Õ’VÐÓÓ•VÒÑQTÕÒÑS”ÈŠBˆ˜[˜XÚרÛÛ<EFBFBD>^ÝÚÙ[œÎˆ[<EFBFBD>HšY[
ˆY˜][LŒ Ì ÝL [X\ÏH”Õ’VÐÓÓ•VÑ<EFBFBD>S<EFBFBD>PÒ×ÕÒÑS”È‚ˆ
BˆÝ[[X\žWÛX^ÝÚÙ[œÎˆ[<EFBFBD>HšY[
Y˜][MÌM‹ÝL [X\ÏH”Õ’VÐÓÓ•VÔÕSSPT–WÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^ÝÚÙ[œÎˆ[<EFBFBD>HšY[
Y˜][NÌ ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^Û[™\Έ[<EFBFBD>HšY[
Y˜][L—Ì ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÓS‘TÈŠBˆÈ›Û܈X›Ý™HH<EFBFBD>[˜Ø][Û‹[›ÝXÙHÚ^™HÛÈH™]šY]È[Ø^\Èš]Ë‚ˆÛÛÛÝ]]ÛX^Øž]\Έ[<EFBFBD>HšY[
ˆY˜][ML
ˆ L <EFBFBD> ÙOLL <EFBFBD> [X\ÏH”Õ’VÕÓÓÓÕUUÓPVЖUTÈ‚ˆ
B‚‚˜Û\ÜÈ<EFBFBD>[<EFBFBD>[YTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ[XYÙNˆÝˆHšY[
ˆY˜][H™ÚÜ‹š[ËÝ\Ù\Ýš^ ÜÝš^ \Ø[™›ÞŒKŒËŒ‹ˆ[X\ÏH”Õ’VÒSPQÑH‹ˆ
Bˆ[XYÙWÜ[ÜÛXÞNˆ]\˜[Ș]]È‹›™]™\ˆ—HHšY[
ˆY˜][H˜]]È‹ˆ[X\ÏH”Õ’VÒSPQÑWÔSÔÓPÖH‹ˆ
Bˆ˜XÚÙ[™ˆÝˆHšY[
Y˜][H™ØÚÙ\ˆ‹[X\ÏH”Õ’VÔ•S•SQWÐ<EFBFBD>PÒÑS‘ŠBˆØZYר›ÛÝÝØZ]ÜΈ[<EFBFBD>HšY[
Y˜][LÌ ÝL [X\ÏH”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈŠBˆÈX^ØÜ™Y[œÚÝ Ú[XYÙHÛÛÝ]]ÈÙ\]™H\ˆYÙ[<EFBFBD>ÛÛ<EFBFBD>^
H›Û™JK‚ˆX^ØÛÛ<EFBFBD>^Ú[XYÙ\Έ[<EFBFBD>HšY[
Y˜][LËÙOL [X\ÏH”Õ’VÓPVÐÓÓ•VÒSPQÑTÈŠB‚‚˜Û\ÜÈ[[Y]žTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ[˜X›Yˆ›ÛÛHšY[
Y˜][U<EFBFBD>YK[X\ÏH”Õ’VÕSSQU–HŠB‚‚˜Û\ÜÈ[<EFBFBD>Yܘ][Û”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆ\œ^]WØ\WÚÙ^NˆÝˆ›Û™HHšY[
ˆY˜][S›Û™Kˆ[X\ÏH”T”VUWÐTWÒÑVH‹ˆ™\<EFBFBD>Q˜[ÙKˆ
BˆÜÝX[—Ø\WÚÙ^NˆÝˆ›Û™HHšY[
ˆY˜][S›Û™Kˆ[X\ÏH”ÔÕPS—ÐTWÒÑVH‹ˆ™\<EFBFBD>Q˜[ÙKˆ
B‚‚˜Û\ÜÈšY]Ù\”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆÈ˜\ÙHT“ÙˆHÝš^™[^HHØØ[šY]Ù\ˆ›ÞY\Èț܈[XZ[ˆÈ™\šYšXØ][Ûˆ[™[˜Üž\Y™\Ü<EFBFBD>[]™\žKˆHœ›ÝÜÙ\ˆ™]™\ˆ[ÜȈÈH™[^H\™XÝNÈHØØ[Ù\<EFBFBD>™\ˆ\ÈHÛ›HØ[\‹‚ˆ\Ý\›ˆÝˆHšY[
Y˜][HšÎ‹ËØ\ œÝš^ ˜ZH‹[X\ÏH”Õ’VÐTÕT“ŠB‚‚˜Û\ÜÈÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ<EFBFBD>TÑWÐÓÓ‘’Q‚ˆNˆTÙ][™ÜÈHšY[
Y˜][Ù˜XÝÜžOSTÙ][™ÜÊBˆY\NˆY\TÙ][™ÜÈHšY[
Y˜][Ù˜XÝÜžOQY\TÙ][™ÜÊBˆ<EFBFBD>[<EFBFBD>[YNˆ<EFBFBD>[<EFBFBD>[YTÙ][™ÜÈHšY[
Y˜][Ù˜XÝÜžOT<EFBFBD>[<EFBFBD>[YTÙ][™ÜÊBˆÛÛ<EFBFBD>^ˆÛÛ<EFBFBD>^Ù][™ÜÈHšY[
Y˜][Ù˜XÝÜžOPÛÛ<EFBFBD>^Ù][™ÜÊBˆ[[Y]žNˆ[[Y]žTÙ][™ÜÈHšY[
Y˜][Ù˜XÝÜžOU[[Y]žTÙ][™ÜÊBˆ[<EFBFBD>Yܘ][ۜΈ[<EFBFBD>Yܘ][Û”Ù][™ÜÈHšY[
Y˜][Ù˜XÝÜžOR[<EFBFBD>Yܘ][Û”Ù][™ÜÊBˆšY]Ù\ŽˆšY]Ù\”Ù][™ÜÈHšY[
Y˜][Ù˜XÝÜžOUšY]Ù\”Ù][™ÜÊB
"""Strix application settings — pydantic-settings powered."""
from __future__ import annotations
from typing import Literal
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
DEFAULT_MAX_TURNS = 500
_BASE_CONFIG = SettingsConfigDict(
case_sensitive=False,
populate_by_name=True,
extra="ignore",
)
class LlmSettings(BaseSettings):
model_config = _BASE_CONFIG
model: str | None = Field(default=None, alias="STRIX_LLM")
api_key: str | None = Field(
default=None,
validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"),
repr=False,
)
api_base: str | None = Field(
default=None,
validation_alias=AliasChoices(
"LLM_API_BASE",
"OPENAI_API_BASE",
"OPENAI_BASE_URL",
"LITELLM_BASE_URL",
"OLLAMA_API_BASE",
),
)
extra_headers: dict[str, str] | None = Field(
default=None,
alias="LLM_EXTRA_HEADERS",
repr=False,
)
reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT")
force_required_tool_choice: bool = Field(
default=False,
alias="STRIX_FORCE_REQUIRED_TOOL_CHOICE",
)
prompt_cache: bool = Field(
default=True,
alias="STRIX_PROMPT_CACHE",
)
disable_streaming: bool = Field(
default=False,
alias="LLM_DISABLE_STREAMING",
)
timeout: int = Field(default=300, alias="LLM_TIMEOUT")
stream_idle_timeout: int = Field(default=300, ge=0, alias="LLM_STREAM_IDLE_TIMEOUT")
max_tool_calls_per_turn: int = Field(
default=32,
ge=0,
alias="LLM_MAX_TOOL_CALLS_PER_TURN",
)
class DedupeSettings(BaseSettings):
model_config = _BASE_CONFIG
model: str | None = Field(default=None, alias="STRIX_DEDUPE_MODEL")
reasoning_effort: ReasoningEffort | None = Field(
default=None,
alias="STRIX_DEDUPE_REASONING_EFFORT",
)
api_key: str | None = Field(default=None, alias="DEDUPE_LLM_API_KEY", repr=False)
api_base: str | None = Field(default=None, alias="DEDUPE_LLM_API_BASE")
extra_headers: dict[str, str] | None = Field(
default=None,
alias="DEDUPE_LLM_EXTRA_HEADERS",
repr=False,
)
class ContextSettings(BaseSettings):
"""Context-window management: per-tool-output caps and history compaction."""
model_config = _BASE_CONFIG
auto_compact: bool = Field(default=True, alias="STRIX_CONTEXT_AUTO_COMPACT")
compact_buffer_tokens: int = Field(default=20_000, gt=0, alias="STRIX_CONTEXT_BUFFER_TOKENS")
keep_tokens: int = Field(default=8_000, gt=0, alias="STRIX_CONTEXT_KEEP_TOKENS")
fallback_context_tokens: int = Field(
default=200_000, gt=0, alias="STRIX_CONTEXT_FALLBACK_TOKENS"
)
summary_max_tokens: int = Field(default=4_096, gt=0, alias="STRIX_CONTEXT_SUMMARY_TOKENS")
tool_output_max_tokens: int = Field(default=8_000, gt=0, alias="STRIX_TOOL_OUTPUT_MAX_TOKENS")
tool_output_max_lines: int = Field(default=2_000, gt=0, alias="STRIX_TOOL_OUTPUT_MAX_LINES")
# Floor above the truncation-notice size so a preview always fits.
tool_output_max_bytes: int = Field(
default=50 * 1024, ge=1024, alias="STRIX_TOOL_OUTPUT_MAX_BYTES"
)
class RuntimeSettings(BaseSettings):
model_config = _BASE_CONFIG
image: str = Field(
default="ghcr.io/usestrix/strix-sandbox:1.3.0",
alias="STRIX_IMAGE",
)
image_pull_policy: Literal["auto", "never"] = Field(
default="auto",
alias="STRIX_IMAGE_PULL_POLICY",
)
backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND")
# 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):
model_config = _BASE_CONFIG
enabled: bool = Field(default=True, alias="STRIX_TELEMETRY")
class IntegrationSettings(BaseSettings):
model_config = _BASE_CONFIG
perplexity_api_key: str | None = Field(
default=None,
alias="PERPLEXITY_API_KEY",
repr=False,
)
postman_api_key: str | None = Field(
default=None,
alias="POSTMAN_API_KEY",
repr=False,
)
class ViewerSettings(BaseSettings):
model_config = _BASE_CONFIG
# Base URL of the Strix relay the local viewer proxies to for email
# verification and encrypted report delivery. The browser never talks to
# the relay directly; the local server is the only caller.
app_url: str = Field(default="https://app.strix.ai", alias="STRIX_APP_URL")
class Settings(BaseSettings):
model_config = _BASE_CONFIG
llm: LlmSettings = Field(default_factory=LlmSettings)
dedupe: DedupeSettings = Field(default_factory=DedupeSettings)
runtime: RuntimeSettings = Field(default_factory=RuntimeSettings)
context: ContextSettings = Field(default_factory=ContextSettings)
telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings)
integrations: IntegrationSettings = Field(default_factory=IntegrationSettings)
viewer: ViewerSettings = Field(default_factory=ViewerSettings)

View file

@ -1,4 +1,4 @@
­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Startup environment validation and Docker image management."""
"""Startup environment validation and Docker image management."""
import logging
import shutil

View file

@ -1,31 +1,107 @@
­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ø[™›Þ˜XÚÙ[™™YÚ\ÝžH8 %Ù[XÝYšXHÕ’VÔ•S•SQWÐ<EFBFBD>PÒÑS‘
Y˜][ˆØÚÙ\ŠKˆˆˆ‚‚™œ›ÛH×Ù<EFBFBD>]\™W×È[\Ü<EFBFBD>[››Ý][ۜ‚š[\Ü<EFBFBD>ÙÙÚ[™Â™œ›ÛHÛÛXÝ[ۜ˘X˜È[\Ü<EFBFBD>]ØZ]X›KØ[X›B™œ›ÛH\[™È[\Ü<EFBFBD>TWÐÒPÒÒS‘Ë[žB‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛHYÙ[<EFBFBD>ËœØ[™›Þ ›X[šY™\Ý[\Ü<EFBFBD>X[šY™\Ý‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚”Ø[™›Þ˜XÚÙ[™HØ[X›VË‹‹‹]ØZ]X›VÝ\VÐ[žK[žWWWB‚‚˜\Þ[˜ÈYˆÙØÚÙ\—ؘXÚÙ[™
ˆ
‹ˆ[XYÙNˆÝ‹ˆX[šY™\݈X[šY™\Ý ˆ^ÜÙYÜÜ<EFBFBD>Έ\VÚ[<EFBFBD>  ‹‹—Kˆš[™Û[Ý[<EFBFBD>Έ\ÝÙXÝÜÝ‹[žWWH›Û™HH›Û™KŠH Oˆ\VÐ[žK[žWN‚ˆˆˆ<EFBFBD>œš[™È\HÙ\ÜÚ[Ûˆ˜XÚÙYžHHØØ[ØÚÙ\ˆY[[Û‹‚‚ˆ\Ù\ȘÛ\ÜΘÝš^ØÚÙ\”Ø[™›ÞÛY[<EFBFBD>È[š™XÝ‘UÐQRSˆ ˆ‘UÔ<EFBFBD>UÈØ\È
ÈÜÝ ™ØÚÙ\‹š[<EFBFBD>\›˜[ÜÝ YØ]]Ø^Kˆ[\Ü<EFBFBD>ˆØÚÙ\˜^š[HÛÈ\Þ[Y[<EFBFBD>È]\™Ù]H›Û‹QØÚÙ\‚ˆ˜XÚÙ[™Û‰Ý™YYHØÚÙ\‹\HXœ˜\žH[œÝ[Y ‚‚ˆÙ\ÜÚ[Û‹œÝ\<EFBFBD>
"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker)."""
X\ÈÚ]X]\šX[^™\ÈHX[šY™\Ý[<EFBFBD>ÈH<EFBFBD>[›š[™ÂˆÛÛ<EFBFBD>Z[™\ˆ8 %HÑÉÜÈÛY[<EFBFBD> ˜Ü™X]J
XÛ›H<EFBFBD>Z[ÈH[›™\ˆÙ\ÜÚ[Û‚ˆØš™XÝÚ]Ý]\Z[™È] ˆ\Þ[˜ÈÚ]Ù\ÜÚ[ÛŽ˜ÛÝ[Ø[]ÛË<EFBFBD>]ˆÝš^X[˜YÙ\ÈÙ\ÜÚ[ÛˆY™][YH^XÚ]HšXHÛY[<EFBFBD> ™[]J
XÛÈÙBˆšYÙÙ\ˆÝ\<EFBFBD>
from __future__ import annotations
XÝ\œÙ[™\Ë‚ˆˆˆ‚ˆ[\Ü<EFBFBD>ØÚÙ\‚ˆœ›ÛHYÙ[<EFBFBD>ËœØ[™›Þ œØ[™›Þ\Ë™ØÚÙ\ˆ[\Ü<EFBFBD>ØÚÙ\”Ø[™›ÞÛY[<EFBFBD>Ü[ۜ‚ˆœ›ÛHÝš^ ˜ÛÛ™šYË›ØY\ˆ[\Ü<EFBFBD>ØYÜÙ][™Üˆœ›ÛHÝš^ œ<EFBFBD>[<EFBFBD>[YK™ØÚÙ\—ØÛY[<EFBFBD>[\Ü<EFBFBD>Ýš^ØÚÙ\”Ø[™›ÞÛY[<EFBFBD>‚ˆÛY[<EFBFBD>HÝš^ØÚÙ\”Ø[™›ÞÛY[<EFBFBD>
ØÚÙ\‹™œ›ÛWÙ[<EFBFBD>Š
JBˆÛY[<EFBFBD> œÝš^Øš[™Û[Ý[<EFBFBD>ÈHš[™Û[Ý[<EFBFBD>È܈×BˆÛY[<EFBFBD> š[XYÙWÜ[ÜÛXÞHHØYÜÙ][™ÜÊ
Kœ<EFBFBD>[<EFBFBD>[YKš[XYÙWÜ[ÜÛXÞBˆÜ[ÛœÈHØÚÙ\”Ø[™›ÞÛY[<EFBFBD>Ü[ÛœÊ[XYÙOZ[XYÙK^ÜÙYÜÜ<EFBFBD>ÏY^ÜÙYÜÜ<EFBFBD>ÊBˆÙ\ÜÚ[ÛˆH]ØZ]ÛY[<EFBFBD> ˜Ü™X]JÜ[ÛœÏ[Ü[ÛœËX[šY™\Ý[X[šY™\Ý
Bˆ]ØZ]Ù\ÜÚ[Û‹œÝ\<EFBFBD>
import logging
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any
Bˆ™]\›ˆÛY[<EFBFBD> Ù\ÜÚ[Û‚‚‚—Ð<EFBFBD>PÒÑS‘ΈXÝÜÝ‹Ø[™›Þ˜XÚÙ[™HHˆ™ØÚÙ\ˆŽˆÙØÚÙ\—ؘXÚÙ[™ ŸB‚—Ð’S‘ÓSÕS•Ð<EFBFBD>PÒÑS‘ΈÙ]ÜÝ—HHÈ™ØÚÙ\ˆŸB‚‚™YˆÙ]ؘXÚÙ[™
˜[YNˆÝŠH OˆØ[™›Þ˜XÚÙ[™‚ˆˆˆ”™]\›ˆH˜XÚÙ[™˜XÝÜžH›Üˆ˜[YX܈˜Z\ÙK‚‚ˆ\™Ü΂ˆ˜[YNˆ˜XÚÙ[™Y[<EFBFBD>YšY\ˆ
K™Ëˆ™ØÚÙ\ˆ˜
KˆX]Ú\È^X݈›È˜[˜XÚˈ[šÛ›ÝÛˆ˜[Y\ȘZ\ÙHÛÈÛÛ™šYÈ\ÜÈÝ\™˜XÙBˆ[[YYX][H[œÝXYÙˆÚ[[<EFBFBD>HXÚÚ[™ÈHY˜][ ‚ˆˆˆ‚ˆ˜XÚÙ[™HÐ<EFBFBD>PÒÑS‘Ë™Ù]
˜[YJBˆYˆ˜XÚÙ[™\țۙN‚ˆÝ\Ü<EFBFBD>YH‹‹š›Ú[ŠÛÜ<EFBFBD>Y
Ð<EFBFBD>PÒÑS‘ÊJBˆ˜Z\ÙH˜[YQ\œ›ÜŠˆˆ•[šÛ›ÝÛˆÕ’VÔ•S•SQWÐ<EFBFBD>PÒÑS‘ˆÛ˜[YH\ŸH
Ý\Ü<EFBFBD>YˆÜÝ\Ü<EFBFBD>YJH‹ˆ
BˆÙÙÙ\‹™X<EFBFBD>YÊ”Ù[XÝYØ[™›Þ˜XÚÙ[™ˆ \È‹˜[YJBˆ™]\›ˆ˜XÚÙ[™‚‚™Yˆ™YÚ\Ý\—ؘXÚÙ[™
ˆ˜[YNˆÝ‹ˆ˜XÚÙ[™ˆØ[™›Þ˜XÚÙ[™ ˆ
‹ˆÝ\Ü<EFBFBD>רš[™Û[Ý[<EFBFBD>Έ›ÛÛH˜[ÙKŠH Oˆ›Û™N‚ˆˆˆ”™YÚ\Ý\ˆHÝ\ÝÛH˜XÚÙ[™[™\ˆ˜[YX ‚‚ˆ[<EFBFBD>[™Y›ÜˆÝۜݙX[H\Ù\œÈÚÈÚ\Z\ˆÝÛˆ<EFBFBD>[<EFBFBD>[YH8 %™YÚ\Ý\‚ˆ™Y›Ü™H[žHÙ\ÜÚ[Û—ÛX[˜YÙ\‹˜Ü™X]WÛÜ—Ü™]\ÙXØ[ ˆ™K\™YÚ\Ý\š[™Âˆ[ˆ^\Ý[™È˜[YHÝ™\<EFBFBD>Üš]\ÈHš[܈[<EFBFBD>žKˆÝ\Ü<EFBFBD>רš[™Û[Ý[<EFBFBD>؈Y˜][ÈȘ[ÙNˆH™[[ÝH<EFBFBD>[<EFBFBD>[YHØ[››ÝÙYHHØ[\‰ÜÈš[\Þ\Ý[KÛˆ]\È[™YØØ[ÛÝ\˜Ù\È\ÈX[šY™\Ý[<EFBFBD>šY\ÈÈ\ØY[œÝXY ‚ˆˆˆ‚ˆÐ<EFBFBD>PÒÑS‘ÖÛ˜[YWHH˜XÚÙ[™ˆYˆÝ\Ü<EFBFBD>רš[™Û[Ý[<EFBFBD>΂ˆÐ’S‘ÓSÕS•Ð<EFBFBD>PÒÑS‘˘Y
˜[YJBˆ[ÙN‚ˆÐ’S‘ÓSÕS•Ð<EFBFBD>PÒÑS‘Ë™\ØØ\™
˜[YJBˆÙÙÙ\‹š[™›Ê”™YÚ\Ý\™YØ[™›Þ˜XÚÙ[™ˆ \È
š[™[Ý[<EFBFBD>Έ \ÊH‹˜[YKÝ\Ü<EFBFBD>רš[™Û[Ý[<EFBFBD>ÊB‚‚™Yˆ˜XÚÙ[™ÜÝ\Ü<EFBFBD>רš[™Û[Ý[<EFBFBD>ʘ[YNˆÝŠH Oˆ›ÛÛ‚ˆ™]\›ˆ˜[YH[ˆÐ’S‘ÓSÕS•Ð<EFBFBD>PÒÑS‘‚‚™YˆÝ\Ü<EFBFBD>YؘXÚÙ[™Ê
H Oˆ\ÝÜÝ—N‚ˆ™]\›ˆÛÜ<EFBFBD>Y
Ð<EFBFBD>PÒÑS‘ÊB
if TYPE_CHECKING:
from agents.sandbox.manifest import Manifest
logger = logging.getLogger(__name__)
SandboxBackend = Callable[..., Awaitable[tuple[Any, Any]]]
async def _docker_backend(
*,
image: str,
manifest: Manifest,
exposed_ports: tuple[int, ...],
bind_mounts: list[dict[str, Any]] | None = None,
) -> tuple[Any, Any]:
"""Bring up a session backed by the local Docker daemon.
Uses :class:`StrixDockerSandboxClient` to inject NET_ADMIN /
NET_RAW caps + ``host.docker.internal`` host-gateway. Imports
``docker`` lazily so deployments that target a non-Docker
backend don't need the docker-py library installed.
``session.start()`` is what materializes the manifest into the running
container — the SDK's ``client.create()`` only builds the inner session
object without applying it. ``async with session:`` would call it too, but
Strix manages session lifetime explicitly via ``client.delete()`` so we
trigger ``start()`` ourselves.
"""
import docker
from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions
from strix.config.loader import load_settings
from strix.runtime.docker_client import StrixDockerSandboxClient
client = StrixDockerSandboxClient(docker.from_env())
client.strix_bind_mounts = bind_mounts or []
client.image_pull_policy = load_settings().runtime.image_pull_policy
options = DockerSandboxClientOptions(image=image, exposed_ports=exposed_ports)
session = await client.create(options=options, manifest=manifest)
await session.start()
return client, session
_BACKENDS: dict[str, SandboxBackend] = {
"docker": _docker_backend,
}
_BIND_MOUNT_BACKENDS: set[str] = {"docker"}
def get_backend(name: str) -> SandboxBackend:
"""Return the backend factory for ``name`` or raise.
Args:
name: Backend identifier (e.g. ``"docker"``). Match is exact;
no fallback. Unknown values raise so config typos surface
immediately instead of silently picking a default.
"""
backend = _BACKENDS.get(name)
if backend is None:
supported = ", ".join(sorted(_BACKENDS))
raise ValueError(
f"Unknown STRIX_RUNTIME_BACKEND: {name!r} (supported: {supported})",
)
logger.debug("Selected sandbox backend: %s", name)
return backend
def register_backend(
name: str,
backend: SandboxBackend,
*,
supports_bind_mounts: bool = False,
) -> None:
"""Register a custom backend under ``name``.
Intended for downstream users who ship their own runtime — register
before any ``session_manager.create_or_reuse`` call. Re-registering
an existing name overwrites the prior entry. ``supports_bind_mounts``
defaults to False: a remote runtime cannot see the caller's filesystem, so
it is handed local sources as manifest entries to upload instead.
"""
_BACKENDS[name] = backend
if supports_bind_mounts:
_BIND_MOUNT_BACKENDS.add(name)
else:
_BIND_MOUNT_BACKENDS.discard(name)
logger.info("Registered sandbox backend: %s (bind mounts: %s)", name, supports_bind_mounts)
def backend_supports_bind_mounts(name: str) -> bool:
return name in _BIND_MOUNT_BACKENDS
def supported_backends() -> list[str]:
return sorted(_BACKENDS)

View file

@ -1,4 +1,4 @@
­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""StrixDockerSandboxClient — preserves the image's ENTRYPOINT and adds
"""StrixDockerSandboxClient — preserves the image's ENTRYPOINT and adds
NET_ADMIN/NET_RAW capabilities + host-gateway.
The SDK's ``DockerSandboxClient._create_container`` does not expose a hook for

View file

@ -1,66 +1,225 @@
­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ•\Ýț܈ݚ^ ˜ÛÛ™šYË›ØY\Žˆ”Óӈݙ\œšY\Ë[X\È™\ÛÛ][Û‹\œÚ\Ý[˜ÙKˆˆˆ‚‚™œ›ÛH×Ù<EFBFBD>]\™W×È[\Ü<EFBFBD>[››Ý][ۜ‚š[\Ü<EFBFBD>œÛÛ‚™œ›ÛH\[™È[\Ü<EFBFBD>TWÐÒPÒÒS‘‚š[\Ü<EFBFBD>]\Ý™œ›ÛHY[<EFBFBD>XÈ[\Ü<EFBFBD>[X\ÐÚÚXÙ\ËšY[ ˜[Y][Û‘\œ›Ü‚™œ›ÛHY[<EFBFBD>XË™šY[È[\Ü<EFBFBD>šY[[™›Â‚™œ›ÛHÝš^ ˜ÛÛ™šYÈ[\Ü<EFBFBD>ØY\‚™œ›ÛHÝš^ ˜ÛÛ™šYËœÙ][™ÜÈ[\Ü<EFBFBD>ÛÛ<EFBFBD>^Ù][™Ü‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛH]Xˆ[\Ü<EFBFBD>]‚‚—ÓWÑS•—ÒÑVTÈHˆ”Õ’VÓH‹ˆ“WÐTWÒÑVH‹ˆ“ÔS<EFBFBD>RWÐTWÒÑVH‹ˆ“WÐTWÐ<EFBFBD>TÑH‹ˆ“ÔS<EFBFBD>RWÐTWÐ<EFBFBD>TÑH‹ˆ“ÔS<EFBFBD>RWÐ<EFBFBD>TÑWÕT“‹ˆ“USWÐ<EFBFBD>TÑWÕT“‹ˆ“ÓSPWÐTWÐ<EFBFBD>TÑH‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹ˆ”Õ’VÑ“Ô<EFBFBD>ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ“WÕSQSÕU‹ˆ”T”VUWÐTWÒÑVH‹ˆÈ<EFBFBD>[<EFBFBD>[YTÙ][™Üˆ”Õ’VÒSPQÑH‹ˆ”Õ’VÒSPQÑWÔSÔÓPÖH‹ˆ”Õ’VÔ•S•SQWÐ<EFBFBD>PÒÑS‘‹ˆ”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈ‹ˆÈ[[Y]žTÙ][™Üˆ”Õ’VÕSSQU–H‹—B‚‚<EFBFBD>]\Ý ™š^\™J]]Ý\ÙOU<EFBFBD>YJB™YˆÜ™\Ù]ÛØY\—ÜÝ]J[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú
H Oˆ›Û™N‚ˆˆˆ”™\Ù][Ù[HÛؘ[È[™ÛX\ˆÛ›ÝÛˆ[<EFBFBD>ˆ˜\œÈ›Üˆ]\›Z[š\ÝXÈ<EFBFBD>[œËˆˆˆ‚ˆ›ÜˆÙ^H[ˆÓWÑS•—ÒÑVT΂ˆ[ÛšÙ^\]Ú ™[[<EFBFBD>ŠÙ^K˜Z\Ú[™ÏQ˜[ÙJBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ØØXÚY‹›Û™JBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ÛÝ™\œšYH‹›Û™JB‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈÜ™XYÚœÛÛ—ÛÝ™\œšY\ÂˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛZ\ÜÚ[™×Ùš[J\Ü]ˆ]
H Oˆ›Û™N‚ˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê\Ü] È››ÜKšœÛÛˆŠHOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\רÛÜœ<EFBFBD>\ÚœÛÛŠ\Ü]ˆ]
H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
žÛ›Ý˜[YœÛÛˆ‹[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Û›Û—ÙXÝÙ[<EFBFBD>Š\Ü]ˆ]
H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ››Ý‹˜H‹™XÝ—_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛX\×Ý×Û™\ÝYÜÙ][™ÜÊ\Ü]ˆ]
H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
ˆœÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆ›^K[[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH<EFBFBD>]‹N‹ˆ
Bˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHˆ›HŽˆÈ›[Ù[Žˆ›^K[[Ù[ŸKˆš[<EFBFBD>Yܘ][ۜȎˆÈœ\œ^]WØ\WÚÙ^HŽˆœÈŸKˆB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÜÚÚ\×ÚÙ^\ר[™XYWÚ[—Ù[<EFBFBD>š\›ÛŠˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ДՒVÓH‹™œ›ÛKY[<EFBFBD>ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆÈ[<EFBFBD>ˆÚ[œÈ OˆH”ÓÓˆ˜[YH\È›ÝÝ\™˜XÙY\È[ˆ[š]ÝØ\™Ë‚ˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[<EFBFBD>—ÝÚ[œ×ØXÜ›ÜÜ×ÙšY[Ø[X\Ù\ʈ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈ\WÚÙ^H™\ÛÛ™\Èœ›ÛH[X\ÐÚÚXÙ\Ê“WÐTWÒÑVH‹“ÔS<EFBFBD>RWÐTWÒÑVHŠKˆH[<EFBFBD>‚ˆÈÙ]ÈÛ™H[X\ÈÚ[HH\œÚ\ÝYš[HÛÈ[›Ý\‹ˆ[<EFBFBD>ˆ]\ÝÝ[Ú[‹ÛˆÈHÝ[Hš[H˜[YH]\ݛݙHÝ\™˜XÙY\È[ˆ[š]ÝØ\™È
ÚXÚÝ]˜[šÜÈ[<EFBFBD>ŠK‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>Š“ÔS<EFBFBD>RWÐTWÒÑVH‹œÚËY[<EFBFBD>ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ“WÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[<EFBFBD>—ÝÚ[œ×ØØ\ÙWÚ[œÙ[œÚ]]™[Jˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈÙ][™ÜÈ\ÙHØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKÛÈHÝÙ\˜Ø\ÙH[<EFBFBD>ˆ˜\ˆ[ÛÈÛÝ[<EFBFBD>È\ÈÙ] ‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ŠœÝš^ÛH‹™œ›ÛKY[<EFBFBD>ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ý\Ù\×ÚœÛÛ—ÝÚ[—Û›×Ø[X\×Ú[—Ù[<EFBFBD>š\›ÛŠ\Ü]ˆ]
H Oˆ›Û™N‚ˆÈ›È[X\ÈÙˆ\WÚÙ^H\ÈÙ][ˆH[<EFBFBD>š\›Û›Y[<EFBFBD> OˆHš[H˜[YH\È\ÙY ]™[‚ˆÈÚ[ˆ]\ÈÝÜ™Y[™\ˆH›Û‹Yš\œÝ[X\Ë‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ“ÔS<EFBFBD>RWÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê]
HOHÈ›HŽˆȘ\WÚÙ^HŽˆœÚËYš[HŸ_B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\×Ü™Z™XÝ×ÜÝX—Û›ÝXÙWݘ[Y\Ê
H Oˆ›Û™N‚ˆÚ]]\Ý œ˜Z\Ù\ʘ[Y][Û‘\œ›ÜŠN‚ˆÛÛ<EFBFBD>^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏM<EFBFBD>
B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\רXØÙ\×Ù›ÛÜŠ
H Oˆ›Û™N‚ˆ\ÜÙ\<EFBFBD>ÛÛ<EFBFBD>^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏLL <EFBFBD>
K<EFBFBD>ÛÛÛÝ]]ÛX^Øž]\ÈOH L <EFBFBD>‚‚™Yˆ\ÝÜ<EFBFBD>[<EFBFBD>[YWØØZYר›ÛÝÝØZ]Ùœ›ÛWÙ[<EFBFBD>Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú
H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ДՒVÐÐRQ×ГÓÕÕÐRUÔÈ‹ŽL ŠB‚ˆ\ÜÙ\<EFBFBD>ØY\‹›ØYÜÙ][™ÜÊ
Kœ<EFBFBD>[<EFBFBD>[YK˜ØZYר›ÛÝÝØZ]ÜÈOHL ‚‚™Yˆ\ÝÜ<EFBFBD>[<EFBFBD>[YWÚ[XYÙWÜ[ÜÛXÞWÙœ›ÛWÙ[<EFBFBD>Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú
H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ДՒVÒSPQÑWÔSÔÓPÖH‹›™]™\ˆŠB‚ˆ\ÜÙ\<EFBFBD>ØY\‹›ØYÜÙ][™ÜÊ
Kœ<EFBFBD>[<EFBFBD>[YKš[XYÙWÜ[ÜÛXÞHOH›™]™\ˆ‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈØ[X\Ù\×ٛ܂ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ[X\Ù\×ٛܗÜÚ[\WØ[X\Ê
H Oˆ›Û™N‚ˆš[™›ÈHšY[[™›Ê[X\ÏH”ÒSTWÐSPTÈŠBˆ\ÜÙ\<EFBFBD>ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHÈ”ÒSTWÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×ٛܗØ[X\רÚÚXÙ\Ê
H Oˆ›Û™N‚ˆš[™›ÎˆšY[[™›ÈHšY[
È\NˆYۛܙVØ\ÜÚYÛ›Y[<EFBFBD>BˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\Ê‘’T”Õ‹”ÑPÓÓ‘ŠKˆ
Bˆ\ÜÙ\<EFBFBD>ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHÈ‘’T”Õ‹”ÑPÓÓ‘—B‚‚™Yˆ\ÝØ[X\Ù\×ٛܗÜÝš[™×ݘ[Y][Û—Ø[X\Ê
H Oˆ›Û™N‚ˆš[™›ÎˆšY[[™›ÈHšY[
Y˜][S›Û™K˜[Y][Û—Ø[X\ÏH”Õ—ÐSPTÈŠHÈ\NˆYۛܙVØ\ÜÚYÛ›Y[<EFBFBD>Bˆ\ÜÙ\<EFBFBD>ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHȔ՗ÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×Ù›Ü—Û›×Ø[X\Ê
H Oˆ›Û™N‚ˆ\ÜÙ\<EFBFBD>ØY\‹—Ø[X\Ù\×ٛ܊šY[[™›Ê
JHOH×B‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\WØÛÛ™šY×ÛÝ™\œšYH
ÈØYÜÙ][™ÜÈ›Ý[™ ]š\ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ\WÛÝ™\œšYWØ[™ÛØYÜÙ][™Ü×Ü›Ý[™Ýš\
\Ü]ˆ]
H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] <EFBFBD>Üš]WÝ^
ˆœÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆœ›Ý[™ ]š\ [[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH<EFBFBD>]‹N‹ˆ
B‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ]
BˆÙ][™ÜÈHØY\‹›ØYÜÙ][™ÜÊ
B‚ˆ\ÜÙ\<EFBFBD>Ù][™ÜË›K›[Ù[OHœ›Ý[™ ]š\ [[Ù[‚ˆ\ÜÙ\<EFBFBD>Ù][™ÜËš[<EFBFBD>Yܘ][ۜ˜\œ^]WØ\WÚÙ^HOHœÈ‚ˆÈÙXÛÛ™Ø[\ÈY[[Ú^™Y OˆØ[YHØš™XÝ ‚ˆ\ÜÙ\<EFBFBD>ØY\‹›ØYÜÙ][™ÜÊ
H\ÈÙ][™Ü‚‚™Yˆ\ÝØ\WØÛÛ™šY×ÛÝ™\œšYWÚ[<EFBFBD>˜[Y]\רØXÚJ\Ü]ˆ]
H Oˆ›Û™N‚ˆš\œÝH\Ü] È™š\œÝ šœÛÛˆ‚ˆš\œÝ <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆ™š\œÝ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠBˆÙXÛÛ™H\Ü] ÈœÙXÛÛ™ šœÛÛˆ‚ˆÙXÛÛ™ <EFBFBD>Üš]WÝ^
œÛÛ‹™[\ÊÈ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆœÙXÛÛ™ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH<EFBFBD>]‹NŠB‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJš\œÝ
Bˆ\ÜÙ\<EFBFBD>ØY\‹›ØYÜÙ][™ÜÊ
K›K›[Ù[OH™š\œÝ [[Ù[‚‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJÙXÛÛ™
Bˆ\ÜÙ\<EFBFBD>ØY\‹›ØYÜÙ][™ÜÊ
K›K›[Ù[OHœÙXÛÛ™ [[Ù[‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\œÚ\ÝØÝ\œ™[<EFBFBD>ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[<EFBFBD>ÝÜš]\×Ù[<EFBFBD>—Ø›ØÚÊ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú
H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ÈœÝXˆˆ ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù]
B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[<EFBFBD>
"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence."""
B‚ˆ\ÜÙ\<EFBFBD>\™Ù] ™^\ÝÊ
Bˆ\ÜÙ\<EFBFBD>œÛÛ‹›ØYÊ\™Ù] œ™XYÝ^
[˜ÛÙ[™ÏH<EFBFBD>]‹NŠJHOHˆ™[<EFBFBD>ˆŽˆÈ”Õ’VÓHŽˆœ\œÚ\ÝY [[Ù[ŸBˆB‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[<EFBFBD>ÜÙ]×Ì Œ Û[ÙJ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú
H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][<EFBFBD>ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù]
B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[<EFBFBD>
from __future__ import annotations
B‚ˆ\ÜÙ\<EFBFBD>\™Ù] œÝ]
import json
from typing import TYPE_CHECKING
KœÝÛ[ÙH ˆ ÍÍÍÈOH ÍŒ 
import pytest
from pydantic import AliasChoices, Field, ValidationError
from pydantic.fields import FieldInfo
from strix.config import loader
from strix.config.settings import ContextSettings
if TYPE_CHECKING:
from pathlib import Path
_LLM_ENV_KEYS = [
"STRIX_LLM",
"LLM_API_KEY",
"OPENAI_API_KEY",
"LLM_API_BASE",
"OPENAI_API_BASE",
"OPENAI_BASE_URL",
"LITELLM_BASE_URL",
"OLLAMA_API_BASE",
"STRIX_REASONING_EFFORT",
"STRIX_FORCE_REQUIRED_TOOL_CHOICE",
"LLM_TIMEOUT",
"PERPLEXITY_API_KEY",
# RuntimeSettings
"STRIX_IMAGE",
"STRIX_IMAGE_PULL_POLICY",
"STRIX_RUNTIME_BACKEND",
# TelemetrySettings
"STRIX_TELEMETRY",
]
@pytest.fixture(autouse=True)
def _reset_loader_state(monkeypatch: pytest.MonkeyPatch) -> None:
"""Reset module globals and clear known env vars for deterministic runs."""
for key in _LLM_ENV_KEYS:
monkeypatch.delenv(key, raising=False)
monkeypatch.setattr(loader, "_cached", None)
monkeypatch.setattr(loader, "_override", None)
# --------------------------------------------------------------------------- #
# _read_json_overrides
# --------------------------------------------------------------------------- #
def test_read_json_overrides_missing_file(tmp_path: Path) -> None:
assert loader._read_json_overrides(tmp_path / "nope.json") == {}
def test_read_json_overrides_corrupt_json(tmp_path: Path) -> None:
path = tmp_path / "cli-config.json"
path.write_text("{not valid json", encoding="utf-8")
assert loader._read_json_overrides(path) == {}
def test_read_json_overrides_non_dict_env(tmp_path: Path) -> None:
path = tmp_path / "cli-config.json"
path.write_text(json.dumps({"env": ["not", "a", "dict"]}), encoding="utf-8")
assert loader._read_json_overrides(path) == {}
def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None:
path = tmp_path / "cli-config.json"
path.write_text(
json.dumps({"env": {"STRIX_LLM": "my-model", "PERPLEXITY_API_KEY": "pk"}}),
encoding="utf-8",
)
assert loader._read_json_overrides(path) == {
"llm": {"model": "my-model"},
"integrations": {"perplexity_api_key": "pk"},
}
def test_read_json_overrides_skips_keys_already_in_environ(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("STRIX_LLM", "from-env")
path = tmp_path / "cli-config.json"
path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8")
# env wins -> the JSON value is not surfaced as an init kwarg.
assert loader._read_json_overrides(path) == {}
def test_read_json_overrides_env_wins_across_field_aliases(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# api_key resolves from AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"). The env
# sets one alias while the persisted file holds another. Env must still win, so
# the stale file value must not be surfaced as an init kwarg (which outranks env).
monkeypatch.setenv("OPENAI_API_KEY", "sk-env")
path = tmp_path / "cli-config.json"
path.write_text(json.dumps({"env": {"LLM_API_KEY": "sk-file"}}), encoding="utf-8")
assert loader._read_json_overrides(path) == {}
def test_read_json_overrides_env_wins_case_insensitively(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# Settings use case_sensitive=False, so a lowercase env var also counts as set.
monkeypatch.setenv("strix_llm", "from-env")
path = tmp_path / "cli-config.json"
path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8")
assert loader._read_json_overrides(path) == {}
def test_read_json_overrides_uses_json_when_no_alias_in_environ(tmp_path: Path) -> None:
# No alias of api_key is set in the environment -> the file value is used, even
# when it is stored under a non-first alias.
path = tmp_path / "cli-config.json"
path.write_text(json.dumps({"env": {"OPENAI_API_KEY": "sk-file"}}), encoding="utf-8")
assert loader._read_json_overrides(path) == {"llm": {"api_key": "sk-file"}}
def test_tool_output_max_bytes_rejects_sub_notice_values() -> None:
with pytest.raises(ValidationError):
ContextSettings(STRIX_TOOL_OUTPUT_MAX_BYTES=64)
def test_tool_output_max_bytes_accepts_floor() -> None:
assert ContextSettings(STRIX_TOOL_OUTPUT_MAX_BYTES=1024).tool_output_max_bytes == 1024
def test_runtime_image_pull_policy_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("STRIX_IMAGE_PULL_POLICY", "never")
assert loader.load_settings().runtime.image_pull_policy == "never"
# --------------------------------------------------------------------------- #
# _aliases_for
# --------------------------------------------------------------------------- #
def test_aliases_for_simple_alias() -> None:
finfo = FieldInfo(alias="SIMPLE_ALIAS")
assert loader._aliases_for(finfo) == ["SIMPLE_ALIAS"]
def test_aliases_for_alias_choices() -> None:
finfo: FieldInfo = Field( # type: ignore[assignment]
default=None,
validation_alias=AliasChoices("FIRST", "SECOND"),
)
assert loader._aliases_for(finfo) == ["FIRST", "SECOND"]
def test_aliases_for_string_validation_alias() -> None:
finfo: FieldInfo = Field(default=None, validation_alias="STR_ALIAS") # type: ignore[assignment]
assert loader._aliases_for(finfo) == ["STR_ALIAS"]
def test_aliases_for_no_alias() -> None:
assert loader._aliases_for(FieldInfo()) == []
# --------------------------------------------------------------------------- #
# apply_config_override + load_settings round-trip
# --------------------------------------------------------------------------- #
def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None:
path = tmp_path / "cli-config.json"
path.write_text(
json.dumps({"env": {"STRIX_LLM": "round-trip-model", "PERPLEXITY_API_KEY": "pk"}}),
encoding="utf-8",
)
loader.apply_config_override(path)
settings = loader.load_settings()
assert settings.llm.model == "round-trip-model"
assert settings.integrations.perplexity_api_key == "pk"
# Second call is memoized -> same object.
assert loader.load_settings() is settings
def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
first = tmp_path / "first.json"
first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8")
second = tmp_path / "second.json"
second.write_text(json.dumps({"env": {"STRIX_LLM": "second-model"}}), encoding="utf-8")
loader.apply_config_override(first)
assert loader.load_settings().llm.model == "first-model"
loader.apply_config_override(second)
assert loader.load_settings().llm.model == "second-model"
# --------------------------------------------------------------------------- #
# persist_current
# --------------------------------------------------------------------------- #
def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("STRIX_LLM", "persisted-model")
target = tmp_path / "sub" / "cli-config.json"
loader.apply_config_override(target)
loader.persist_current()
assert target.exists()
assert json.loads(target.read_text(encoding="utf-8")) == {
"env": {"STRIX_LLM": "persisted-model"}
}
def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("STRIX_LLM", "persisted-model")
target = tmp_path / "cli-config.json"
loader.apply_config_override(target)
loader.persist_current()
assert target.stat().st_mode & 0o777 == 0o600

View file

@ -1,4 +1,4 @@
­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Regression tests for sandbox image acquisition policy."""
"""Regression tests for sandbox image acquisition policy."""
from __future__ import annotations