From c42887410815915b0c64ac70a0120db3e282cb02 Mon Sep 17 00:00:00 2001 From: maxi-maxima <240684166@qq.com> Date: Wed, 12 Aug 2026 10:18:51 +0800 Subject: [PATCH 1/3] feat(runtime): support pull-never sandbox images --- docs/advanced/configuration.mdx | 186 ++-------------- strix/config/settings.py | 7 +- strix/interface/environment.py | 308 +++++++++---------------- strix/runtime/backends.py | 4 +- strix/runtime/docker_client.py | 382 +++++++++----------------------- tests/test_config_loader.py | 278 ++++++----------------- tests/test_image_pull_policy.py | 85 +++++++ 7 files changed, 379 insertions(+), 871 deletions(-) create mode 100644 tests/test_image_pull_policy.py diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index f1542b75..5148d616 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -1,169 +1,17 @@ ---- -title: "Configuration" -description: "Environment variables for Strix" ---- - -Configure Strix using environment variables or a config file. - -## LLM Configuration - - - Model name in LiteLLM format (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`). - - - - API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock). - - - - Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`. - - - - 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. - - - - Request timeout in seconds for LLM calls. - - - - Maximum number of retries for LLM API calls on transient failures. - - - - Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Defaults to `medium` for quick scan mode. - - - - Timeout in seconds for memory compression operations (context summarization). - - -### 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. - - - Model used to judge whether a candidate finding duplicates an existing report. - Falls back to `STRIX_LLM` when unset. - - - - Optional provider key for the deduplication model. - - - - Optional custom API base URL for the deduplication model. Use when the dedupe - model runs on a different endpoint than the main model. - - - - 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. - - - - Reasoning effort for the deduplication model. Defaults to the model's own - baseline when unset. - - -## Optional Features - - - API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research. - - - - Postman API key (`PMAK-…`). Enables fetching Postman collections by id as a target (`postman://`), and Postman environments (`postman://?env=`) to resolve collection variables. Not needed when passing a local collection export file. - - - - Telemetry toggle. Set to `0`, `false`, `no`, or `off` to disable telemetry (PostHog, Scarf, OTEL). - - - - OTLP/Traceloop base URL for remote OpenTelemetry export. If unset, Strix keeps traces local only. - - - - API key used for remote trace export. Remote export is enabled only when both `TRACELOOP_BASE_URL` and `TRACELOOP_API_KEY` are set. - - - - Optional custom OTEL headers (JSON object or `key=value,key2=value2`). Useful for Langfuse or custom/self-hosted OTLP gateways. - - -When remote OTEL vars are not set, Strix still writes complete run telemetry locally to: - -```bash -strix_runs//events.jsonl -``` - -When remote vars are set, Strix dual-writes telemetry to both local JSONL and the remote OTEL endpoint. - -## Docker Configuration - - - Docker image to use for the sandbox container. - - - - Docker daemon socket path. Use for remote Docker hosts or custom configurations. - - - - Runtime backend for the sandbox environment. - - -## Sandbox Configuration - - - Maximum execution time in seconds for sandbox operations. - - - - Timeout in seconds for connecting to the sandbox container. - - -## 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" - -``` +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëKKKB�]Nˆ�ÛÛ™šYÝ\˜][Ûˆ‚™\ØÜš\[ÛŽˆ‘[�š\›Û›Y[�˜\šXX›\ț܈ݚ^‚‹KKB‚�ÛÛ™šYÝ\™HÝš^\Ú[™È[�š\›Û›Y[�˜\šXX›\È܈HÛÛ™šYÈš[K‚‚ˆÈÈHÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÓHˆ\OHœÝš[™Èˆ™\]Z\™Y‚ˆ[Ù[˜[YH[ˆ]SH›Ü›X] +K™Ë‹Ü[˜ZKÙÜ MK� [�›ÜXËØÛ]YK\ÛÛ›™] M M˜ +K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H›Üˆ[Ý\ˆH›ÝšY\‹ˆ›Ý™\]Z\™Y›ÜˆØØ[[Ù[È܈ÛÝY›ÝšY\ˆ]] +™\�^RKUÔÈ™Y›ØÚÊK‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÐTWÐ�TÑHˆ\OHœÝš[™È�‚ˆÝ\ÝÛHTH˜\ÙHT“ ˆ[ÛÈXØÙ\ÈÔS�RWÐTWÐ�TÑX USWÐ�TÑWÕT“ ܈ÓSPWÐTWÐ�TÑX ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÑV�WÒPQT”Ȉ\OHœÝš[™È�‚ˆ^˜HXY\œÈÙ[�Ûˆ]™\žHH™\]Y\Ý \ÈH”Óӈؚ™XÝ +K™Ë‚ˆÈ– Q™X]\™KRÙ^HŽˆ�˜[YH‹– U[˜[�Žˆ˜XÛYHŸX +Kˆ\ÙY�[›ÜˆÜ[�RKXÛÛ\]X›BˆØ]]Ø^\È]™\]Z\™H]šX�][Ûˆ܈›Ý][™ÈXY\œÈ[ˆY][ÛˆÈH™X\™\‚ˆÚÙ[‹ˆH™X\™\ˆÚÙ[ˆ]Ù[ˆÝ[ÛÛY\Èœ›ÛHWÐTWÒÑVX ˆ\Y\Èț݈H]SH[™˜]]™HÜ[�RH›Ý][™È]Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÕSQSÕUˆY˜][HŒÌ ˆ\OHš[�YÙ\ˆ�‚ˆ™\]Y\Ý[Y[Ý][ˆÙXÛۙț܈HØ[Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÓWÓPVÔ‘U’QTȈY˜][H�Hˆ\OHš[�YÙ\ˆ�‚ˆX^[][H�[X™\ˆÙˆ™]šY\ț܈HTHØ[ÈÛˆ˜[œÚY[�˜Z[\™\Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆY˜][HšYÚˆ\OHœÝš[™È�‚ˆÛÛ�›Û[šÚ[™ÈY™›Ü�›Üˆ™X\ÛÛš[™È[Ù[ˈ˜[Y˜[Y\Έ›Û™X Z[š[X[ ÝØ YY][X YÚ YÚ X^ ˆY˜][ÈÈYY][X›Üˆ]ZXÚÈØØ[ˆ[ÙK‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÓQSSÔ–WÐÓÓT‘TÔÓÔ—ÕSQSÕUˆY˜][HŒÌˆ\OHš[�YÙ\ˆ�‚ˆ[Y[Ý][ˆÙXÛۙț܈Y[[ÜžHÛÛ\™\ÜÚ[ÛˆÜ\˜][ÛœÈ +ÛÛ�^Ý[[X\š^˜][ÛŠK‚� Ô\˜[QšY[‚‚ˆÈÈÈYXØ]YY\XØ][Ûˆ[Ù[‚‘š[™[™ÈY\XØ][Ûˆ\ÈHÚX\ Ý�XÝ\™YÛ\ÜÚYšXØ][Ûˆ\ÚˈžHY˜][]œ�[œÈÛˆHXZ[ˆ[Ù[ �][ÝHØ[ˆ›Ý]H]ÈHÛX[\‹ØÚX\\ˆ[Ù[Ú]Ý]˜Y™™XÝ[™ÈHYÙ[�È]ÈHXÝX[\Ý[™Ë‚‚�\˜[QšY[]H”Õ’VÑQTWÓSÑSˆ\OHœÝš[™È�‚ˆ[Ù[\ÙYÈ�YÙHÚ]\ˆHØ[™Y]Hš[™[™È\XØ]\È[ˆ^\Ý[™È™\Ü� ‚ˆ˜[ȘXÚÈÈÕ’VÓXÚ[ˆ[œÙ] ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÐTWÒÑVHˆ\OHœÝš[™È�‚ˆÜ[Û˜[›ÝšY\ˆÙ^H›ÜˆHY\XØ][Ûˆ[Ù[ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÐTWÐ�TÑHˆ\OHœÝš[™È�‚ˆÜ[Û˜[Ý\ÝÛHTH˜\ÙHT“›ÜˆHY\XØ][Ûˆ[Ù[ ˆ\ÙHÚ[ˆHY\Bˆ[Ù[�[œÈÛˆHY™™\™[�[™Ú[�[ˆHXZ[ˆ[Ù[ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÑV�WÒPQT”Ȉ\OHœÝš[™È�‚ˆÜ[Û˜[”Óӈؚ™XÝÙˆ^˜HXY\œÈÙ[�Ûˆ]™\žHY\XØ][Û‹[[Ù[ˆ™\]Y\Ý K™ËˆÈ– Q™X]\™KRÙ^HŽˆ�˜[YHŸX ˆHYXØ]YY\H[Ù[™]™\‚ˆ[š\š]ÈWÑV�WÒPQT”ØÈÙ]\ÈÚ[ˆ]È[™Ú[�™YYÈÝ\ÝÛHXY\œË‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÑQTWÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆ\OHœÝš[™È�‚ˆ™X\ÛÛš[™ÈY™›Ü�›ÜˆHY\XØ][Ûˆ[Ù[ ˆY˜][ÈÈH[Ù[ ÜÈÝÛ‚ˆ˜\Ù[[™HÚ[ˆ[œÙ] ‚� Ô\˜[QšY[‚‚ˆÈÈÜ[Û˜[™X]\™\‚�\˜[QšY[]H”T”VUWÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H›Üˆ\œ^]HRKˆ[˜X›\È™X[ ][YHÙXˆÙX\˜Ú\š[™ÈØØ[œÈ›ÜˆÔÒS•[™�[™\˜Xš[]H™\ÙX\˜Ú ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”ÔÕPS—ÐTWÒÑVHˆ\OHœÝš[™È�‚ˆÜÝX[ˆTHÙ^H +PRËx )˜ +Kˆ[˜X›\È™]Ú[™ÈÜÝX[ˆÛÛXÝ[ۜȞHY\ÈH\™Ù] +ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY˜ +K[™ÜÝX[ˆ[�š\›Û›Y[�È +ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY�Ù[��O[�š\›Û›Y[� ]ZY˜ +HÈ™\ÛÛ™HÛÛXÝ[Ûˆ˜\šXX›\ˈ›Ý™YYYÚ[ˆ\ÜÚ[™ÈHØØ[ÛÛXÝ[Ûˆ^Ü�š[K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÕSSQU–HˆY˜][HŒHˆ\OHœÝš[™È�‚ˆ[[Y]žHÙÙÛKˆÙ]È  ˜[ÙX ›Ø ܈Ù™˜È\ØX›H[[Y]žH +ÜÝÙËØØ\™‹ÕS +K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÐ�TÑWÕT“ˆ\OHœÝš[™È�‚ˆÕ Õ˜XÙ[Ûܘ\ÙHT“›Üˆ™[[ÝHÜ[•[[Y]žH^Ü� ˆYˆ[œÙ] Ýš^ÙY\ȘXÙ\ÈØØ[Û›K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H\ÙY›Üˆ™[[ÝH˜XÙH^Ü� ˆ™[[ÝH^Ü�\È[˜X›YÛ›HÚ[ˆ›Ý�PÑSÓÔÐ�TÑWÕT“[™�PÑSÓÔÐTWÒÑVX\™HÙ] ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÒPQT”Ȉ\OHœÝš[™È�‚ˆÜ[Û˜[Ý\ÝÛHÕSXY\œÈ +”Óӈؚ™XÝ܈Ù^O]˜[YKÙ^L�]˜[YL˜ +Kˆ\ÙY�[›Üˆ[™Ù�\ÙH܈Ý\ÝÛKÜÙ[‹ZÜÝYÕØ]]Ø^\Ë‚� Ô\˜[QšY[‚‚•Ú[ˆ™[[ÝHÕS˜\œÈ\™H›ÝÙ] Ýš^Ý[Üš]\ÈÛÛ\]H�[ˆ[[Y]žHØØ[H΂‚˜˜\ڜݚ^Ü�[œËÏ�[—Û˜[YO‹Ù]™[�ËšœÛÛ›˜‚•Ú[ˆ™[[ÝH˜\œÈ\™HÙ] Ýš^X[ ]Üš]\È[[Y]žHÈ›ÝØØ[”ÓÓ“[™H™[[ÝHÕS[™Ú[� ‚‚ˆÈÈØÚÙ\ˆÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÒSPQÑHˆY˜][H™ÚÜ‹š[ËÝ\Ù\Ýš^ ÜÝš^ \Ø[™›ÞŒKŒËŒˆ\OHœÝš[™È�‚ˆØÚÙ\ˆ[XYÙHÈ\ÙH›ÜˆHØ[™›ÞÛÛ�Z[™\‹‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÒSPQÑWÔSÔÓPÖHˆY˜][H˜]]Ȉ\OHœÝš[™È�‚ˆÛÛ�›ÛÈØ[™›Þ[XYÙHXÜ]Z\Ú][Û‹ˆ\ÙH™]™\˜È™\]Z\™HH™[ØYY[XYÙBˆ[™˜Z[Ú]Ý][[™ÈÚ[ˆ]\È[˜]˜Z[X›NÈ]]Ø™\Ù\�™\ÈHY˜][ˆš\œÝ \�[ˆ[™Z]š[Ü‹ˆ\ÈÙ\țݙ\ÝšXÝ™]ÛÜšÈXØÙ\ÜÈœ›ÛHH�[›š[™ÂˆØ[™›Þ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘ÐÒÑT—ÒÔÕˆ\OHœÝš[™È�‚ˆØÚÙ\ˆY[[ÛˆÛØÚÙ]] ˆ\ÙH›Üˆ™[[ÝHØÚÙ\ˆÜÝÈ܈Ý\ÝÛHÛÛ™šYÝ\˜][ۜ˂� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔ•S•SQWÐ�PÒÑS‘ˆY˜][H™ØÚÙ\ˆˆ\OHœÝš[™È�‚ˆ�[�[YH˜XÚÙ[™›ÜˆHØ[™›Þ[�š\›Û›Y[� ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÐÐRQ×ГÓÕÕÐRUÔȈY˜][HŒÌ ˆ\OHš[�YÙ\ˆ�‚ˆX^[][H[YH[ˆÙXÛÛ™ÈÈØZ]›ÜˆH[‹\Ø[™›ÞØZYÈ›ÞHÈ™XÛÛYH™XYK‚ˆ[˜Ü™X\ÙH\ÈÛˆÛÝÈ܈X]š[HØYYÛÛ�Z[™\ˆÜÝË‚� Ô\˜[QšY[‚‚ˆÈÈØ[™›ÞÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUˆY˜][HŒLŒˆ\OHš[�YÙ\ˆ�‚ˆX^[][H^XÝ][Ûˆ[YH[ˆÙXÛۙț܈Ø[™›ÞÜ\˜][ۜ˂� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔÐS‘“ÖÐÓÓ“‘PÕÕSQSÕUˆY˜][HŒLˆ\OHš[�YÙ\ˆ�‚ˆ[Y[Ý][ˆÙXÛۙț܈ÛÛ›™XÝ[™ÈÈHØ[™›ÞÛÛ�Z[™\‹‚� Ô\˜[QšY[‚‚ˆÈÈÛÛ™šYÈš[B‚”Ýš^ÝÜ™\ÈÛÛ™šYÝ\˜][Ûˆ[ˆ‹ËœÝš^ ØÛKXÛÛ™šYËšœÛÛ˜ ˆ[ÝHØ[ˆ[ÛÈÜXÚYžHHÝ\ÝÛHÛÛ™šYÈš[N‚‚˜˜\ڜݚ^ K]\™Ù] ‹Ø\ KXÛÛ™šYÈ Ü] ÝËØÛÛ™šYËšœÛÛ‚˜‚ŠŠ�ÛÛ™šYÈš[H›Ü›X]ŠŠ‚‚˜œÛÛ‚žÂˆ™[�ˆŽˆˆ”Õ’VÓHŽˆ›Ü[˜ZKÙÜ MK�‹ˆ“WÐTWÒÑVHŽˆœÚËK‹‹ˆ‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ŽˆšYÚ‚ˆBŸB˜‚ˆÈÈ^[\HÙ]\‚˜˜\ڈș\]Z\™Y™^Ü�Õ’VÓOH›Ü[˜ZKÙÜ MK�‚™^Ü�WÐTWÒÑVOHœÚËK‹‹ˆ‚‚ˆÈÜ[Û˜[ˆ[˜X›HÙXˆÙX\˜Ú™^Ü�T”VUWÐTWÒÑVOHœ K‹‹ˆ‚‚ˆÈÜ[Û˜[ˆÝ\ÝÛH[Y[Ý]™^Ü�WÕSQSÕUH�Œ ‚™^Ü�Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUHŒÌ ‚‚˜ \ No newline at end of file diff --git a/strix/config/settings.py b/strix/config/settings.py index 42a2c97e..c6f6572f 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -1,4 +1,4 @@ -"""Strix application settings — pydantic-settings powered.""" +­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Strix application settings — pydantic-settings powered.""" from __future__ import annotations @@ -109,7 +109,12 @@ class RuntimeSettings(BaseSettings): 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") + caido_boot_wait_s: int = Field(default=300, gt=0, alias="STRIX_CAIDO_BOOT_WAIT_S") # 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") diff --git a/strix/interface/environment.py b/strix/interface/environment.py index 5589a1fe..4e6379d5 100644 --- a/strix/interface/environment.py +++ b/strix/interface/environment.py @@ -1,217 +1,115 @@ -"""Startup environment validation and Docker image management.""" +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ý\�\[�š\›Û›Y[�˜[Y][Ûˆ[™ØÚÙ\ˆ[XYÙHX[˜YÙ[Y[� ˆˆˆ‚‚š[\Ü�ÙÙÚ[™Âš[\Ü�Ú][š[\Ü�Þ\‚™œ›ÛHšXÚ ˜ÛÛœÛÛH[\Ü�ÛÛœÛÛB™œ›ÛHšXÚ œ[™[[\Ü�[™[™œ›ÛHšXÚ �^[\Ü�^‚™œ›ÛHÝš^ ˜ÛÛ™šYÈ[\Ü�ÛÙ^ ØYÜÙ][™Ü™œ›ÛHÝš^ š[�\™˜XÙK�][È[\Ü� +ˆÚXÚ×ÙØÚÙ\—ØÛÛ›™XÝ[Û‹ˆ[XYÙWÙ^\Ýˈ›ØÙ\Ü×Ü[Û[™KŠB‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚™Yˆ˜[Y]WÙ[�š\›Û›Y[� -import logging -import shutil -import sys +H Oˆ›Û™N‚ˆÙÙÙ\‹š[™›Ê•˜[Y][™È[�š\›Û›Y[�ŠBˆÛÛœÛÛHHÛÛœÛÛJ +BˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÈH×BˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÈH×B‚ˆÙ][™ÜÈHØYÜÙ][™ÜÊ +B‚ˆYˆÛÙ^ œÝXœØÜš\[Û—Û[Ù[ +Ù][™ÜË›K›[Ù[ +N‚ˆYˆ›ÝÛÙ^ š\ר]][�XØ]Y -from rich.console import Console -from rich.panel import Panel -from rich.text import Text +N‚ˆÛÛœÛÛKœš[� +ˆˆ–Ü™YTÕ’VÓO^ÜÙ][™ÜË›K›[Ù[H\Ù\È[Ý\ˆÚ]ÔÝXœØÜš\[Û‹‚ˆ˜�][ÝIÜ™H›ÝÚYÛ™Y[‹–Ë×H�[ˆØÞX[—\Ýš^]]ÙÚ[ˆÚ]ÜË×Hš\œÝ ˆ‚ˆ +BˆÞ\Ë™^] + JBˆÙÙÙ\‹š[™›Ê‘[�š\›Û›Y[�ÒÈ +Ú]ÔÝXœØÜš\[ÛŠHŠBˆ™]\›‚‚ˆYˆ›ÝÙ][™ÜË›K›[Ù[‚ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œË˜\[™ +”Õ’VÓHŠB‚ˆYˆ›ÝÙ][™ÜË›K˜\WÚÙ^N‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ +“WÐTWÒÑVHŠB‚ˆYˆ›ÝÙ][™ÜË›K˜\Wؘ\ÙN‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ +“WÐTWÐ�TÑHŠB‚ˆYˆ›ÝÙ][™ÜËš[�Yܘ][ۜ˜\œ^]WØ\WÚÙ^N‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ +”T”VUWÐTWÒÑVHŠB‚ˆYˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆ\œ›Ü—Ý^H^ -from strix.config import codex, load_settings -from strix.interface.utils import ( - check_docker_connection, - image_exists, - process_pull_line, -) +Bˆ\œ›Ü—Ý^ ˜\[™ +“RTÔÒS‘È‘TURT‘QS•’T“Ó“QS•�T’PP“TÈ‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ +——ˆ‹Ý[OH�Ú]HŠB‚ˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ +ˆ¸ (ˆݘ\ŸH‹Ý[OH˜›ÛY[ÝÈŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆ\È›ÝÙ]ˆ‹Ý[OH�Ú]HŠB‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ +—“Ü[Û˜[[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH™[HÚ]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ +ˆ¸ (ˆݘ\ŸH‹Ý[OH™[HY[ÝÈŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆ\È›ÝÙ]ˆ‹Ý[OH™[HÚ]HŠB‚ˆ\œ›Ü—Ý^ ˜\[™ +—”™\]Z\™Y[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH�Ú]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆYˆ˜\ˆOH”Õ’VÓHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ +¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +”Õ’VÓH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆˆ H[Ù[˜[YHÈ\ÙH +K™Ë‹ ÛÜ[˜ZKÙÜ MK� È܈‚ˆ‰Ø[�›ÜXËØÛ]YK[Ü\ËM MÉÊWˆ‹ˆÝ[OH�Ú]H‹ˆ +B‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ +—“Ü[Û˜[[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH�Ú]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆYˆ˜\ˆOH“WÐTWÐ�TÑHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ +¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +“WÐTWÐ�TÑH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆˆ HÝ\ÝÛHTH˜\ÙHT“Yˆ\Ú[™ÈØØ[[Ù[È +K™Ë‹Û[XKTÝY[ÊWˆ‹ˆÝ[OH�Ú]H‹ˆ +Bˆ[Yˆ˜\ˆOH”T”VUWÐTWÒÑVHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ +¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +”T”VUWÐTWÒÑVH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆˆ HTHÙ^H›Üˆ\œ^]HRHÙXˆÙX\˜Ú +[˜X›\È™X[ ][YH™\ÙX\˜Ú +Wˆ‹ˆÝ[OH�Ú]H‹ˆ +Bˆ[Yˆ˜\ˆOH”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•Ž‚ˆ\œ›Ü—Ý^ ˜\[™ +¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆˆ H™X\ÛÛš[™ÈY™›Ü�]™[ˆ›Û™KZ[š[X[ ÝËYY][KYÚ YÚ ‚ˆ›X^ +Y˜][ˆYÚ +Wˆ‹ˆÝ[OH�Ú]H‹ˆ +B‚ˆ\œ›Ü—Ý^ ˜\[™ +—‘^[\HÙ]\—ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +™^Ü�Õ’VÓOIÛÜ[˜ZKÙÜ MK� ׈‹Ý[OH™[HÚ]HŠB‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆYˆ˜\ˆOH“WÐTWÐ�TÑHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ +ˆ™^Ü�WÐTWÐ�TÑOIÚ‹ËÛØØ[ÜÝŒLM Í È‚ˆˆÈ™YYY›ÜˆØØ[[Ù[ÈÛ›Wˆ‹ˆÝ[OH™[HÚ]H‹ˆ +Bˆ[Yˆ˜\ˆOH”T”VUWÐTWÒÑVHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ +ˆ™^Ü�T”VUWÐTWÒÑVOIÞ[Ý\‹\\œ^]KZÙ^KZ\™I׈‹Ý[OH™[HÚ]H‚ˆ +Bˆ[Yˆ˜\ˆOH”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•Ž‚ˆ\œ›Ü—Ý^ ˜\[™ +ˆ™^Ü�Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•IÚYÚ ×ˆ‹ˆÝ[OH™[HÚ]H‹ˆ +B‚ˆ[™[H[™[ +ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ +B‚ˆÙÙÙ\‹™X�YÊ“Z\ÜÚ[™È™\]Z\™Y[�ˆ˜\œÎˆ \È‹Z\ÜÚ[™×Ü™\]Z\™Yݘ\œÊBˆÛÛœÛÛKœš[� +—ˆŠBˆÛÛœÛÛKœš[� +[™[ +BˆÛÛœÛÛKœš[� +BˆÞ\Ë™^] + JBˆÙÙÙ\‹š[™›Êˆ‘[�š\›Û›Y[�ÒÈ +Ü[Û˜[Z\ÜÚ[™Îˆ \ÊH‹ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÈ܈››Û™H‹ˆ +B‚‚™YˆÚXÚ×ÙØÚÙ\—Ú[œÝ[Y -logger = logging.getLogger(__name__) +H Oˆ›Û™N‚ˆYˆÚ][ �ÚXÚ +™ØÚÙ\ˆŠH\țۙN‚ˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆÓH›Ý›Ý[™[ˆUŠBˆÛÛœÛÛHHÛÛœÛÛJ +Bˆ\œ›Ü—Ý^H^ +Bˆ\œ›Ü—Ý^ ˜\[™ +‘ÐÒÑTˆ“ÕS”ÕSQ‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ +——ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +•H ÙØÚÙ\‰ÈÓHØ\țݛÝ[™[ˆ[Ý\ˆU —ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆ”X\ÙH[œÝ[ØÚÙ\ˆ[™[œÝ\™HH ÙØÚÙ\‰ÈÛÛ[X[™\È]˜Z[X›K——ˆ‹Ý[OH�Ú]H‚ˆ +B‚ˆ[™[H[™[ +ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ +BˆÛÛœÛÛKœš[� +—ˆ‹[™[ —ˆŠBˆÞ\Ë™^] + JBˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆÓH™\Ù[�ŠB‚‚™Yˆ[ÙØÚÙ\—Ú[XYÙJ +H Oˆ›Û™N‚ˆœ›ÛHØÚÙ\‹™\œ›ÜœÈ[\Ü�ØÚÙ\‘^Ù\[Û‚‚ˆÛÛœÛÛHHÛÛœÛÛJ +BˆÛY[�HÚXÚ×ÙØÚÙ\—ØÛÛ›™XÝ[ÛŠ +B‚ˆ�[�[YHHØYÜÙ][™ÜÊ +Kœ�[�[YBˆ[XYÙHH�[�[YKš[XYÙB‚ˆYˆ[XYÙWÙ^\ÝÊÛY[� [XYÙJN‚ˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆ[XYÙH[™XYH™\Ù[�ØØ[Nˆ \È‹[XYÙJBˆ™]\›‚‚ˆYˆ�[�[YKš[XYÙWÜ[ÜÛXÞHOH›™]™\ˆŽ‚ˆ˜Z\ÙH�[�[YQ\œ›ÜŠˆˆ‘ØÚÙ\ˆ[XYÙHÚ[XYÙH\ŸH\È›Ý]˜Z[X›HØØ[H[™‚ˆ”Õ’VÒSPQÑWÔSÔÓPÖO[™]™\ˆ›Ü˜šYÈ[[™È]‚ˆ +B‚ˆÙÙÙ\‹š[™›Ê”[[™ÈØÚÙ\ˆ[XYÙNˆ \È‹[XYÙJBˆÛÛœÛÛKœš[� -def validate_environment() -> None: - logger.info("Validating environment") - console = Console() - missing_required_vars = [] - missing_optional_vars = [] +BˆÛÛœÛÛKœš[� +ˆ–Ù[WT[[™È[XYÙVË×HÚ[XYÙ_HŠBˆÛÛœÛÛKœš[� +–Ù[HY[Ý×U\ÈÛ›H\[œÈÛˆš\œÝ�[ˆ[™X^HZÙHH™]ÈZ[�]\Ë‹‹–Ë×HŠBˆÛÛœÛÛKœš[� - settings = load_settings() +B‚ˆÚ]ÛÛœÛÛKœÝ]\Ê–Ø›ÛÞX[—QÝÛ›ØY[™È[XYÙH^Y\œË‹‹ˆ‹Ü[›™\�H™ÝÈŠH\ÈÝ]\΂ˆžN‚ˆ^Y\œ×Ú[™›ÎˆXÝÜ݋ݗHHßBˆ\ÝÝ\]HHˆ‚‚ˆ›Üˆ[™H[ˆÛY[� ˜\Kœ[ +[XYÙKÝ™X[OU�YKXÛÙOU�YJN‚ˆ\ÝÝ\]HH›ØÙ\Ü×Ü[Û[™J[™K^Y\œ×Ú[™›ËÝ]\Ë\ÝÝ\]JB‚ˆ^Ù\ØÚÙ\‘^Ù\[Ûˆ\ÈN‚ˆÙÙÙ\‹™X�YÊ‘˜Z[YÈ[ØÚÙ\ˆ[XYÙH \È‹[XYÙK^×Ú[™›ÏU�YJBˆÛÛœÛÛKœš[� - if codex.subscription_model(settings.llm.model): - if not codex.is_authenticated(): - console.print( - f"[red]STRIX_LLM={settings.llm.model} uses your ChatGPT subscription, " - "but you're not signed in.[/] Run [cyan]strix auth login chatgpt[/] first." - ) - sys.exit(1) - logger.info("Environment OK (ChatGPT subscription)") - return +Bˆ\œ›Ü—Ý^H^ - if not settings.llm.model: - missing_required_vars.append("STRIX_LLM") +Bˆ\œ›Ü—Ý^ ˜\[™ +‘�RSQÈSSPQÑH‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ +——ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +ˆ�ÛÝ[›ÝÝÛ›ØYˆÚ[XYÙ_Wˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ +ÝŠJKÝ[OH™[H™YŠB‚ˆ[™[H[™[ +ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ +BˆÛÛœÛÛKœš[� +[™[ —ˆŠBˆÞ\Ë™^] + JB‚ˆÙÙÙ\‹š[™›Ê‘ØÚÙ\ˆ[XYÙH \È™XYH‹[XYÙJBˆÝXØÙ\Ü×Ý^H^ - if not settings.llm.api_key: - missing_optional_vars.append("LLM_API_KEY") +BˆÝXØÙ\Ü×Ý^ ˜\[™ +‘ØÚÙ\ˆ[XYÙH™XYH‹Ý[OHˆÌŒ˜ÍMYHŠBˆÛÛœÛÛKœš[� +ÝXØÙ\Ü×Ý^ +BˆÛÛœÛÛKœš[� - if not settings.llm.api_base: - missing_optional_vars.append("LLM_API_BASE") - - if not settings.integrations.perplexity_api_key: - missing_optional_vars.append("PERPLEXITY_API_KEY") - - if missing_required_vars: - error_text = Text() - error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red") - error_text.append("\n\n", style="white") - - for var in missing_required_vars: - error_text.append(f"• {var}", style="bold yellow") - error_text.append(" is not set\n", style="white") - - if missing_optional_vars: - error_text.append("\nOptional environment variables:\n", style="dim white") - for var in missing_optional_vars: - error_text.append(f"• {var}", style="dim yellow") - error_text.append(" is not set\n", style="dim white") - - error_text.append("\nRequired environment variables:\n", style="white") - for var in missing_required_vars: - if var == "STRIX_LLM": - error_text.append("• ", style="white") - error_text.append("STRIX_LLM", style="bold cyan") - error_text.append( - " - Model name to use (e.g., 'openai/gpt-5.4' or " - "'anthropic/claude-opus-4-7')\n", - style="white", - ) - - if missing_optional_vars: - error_text.append("\nOptional environment variables:\n", style="white") - for var in missing_optional_vars: - if var == "LLM_API_BASE": - error_text.append("• ", style="white") - error_text.append("LLM_API_BASE", style="bold cyan") - error_text.append( - " - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n", - style="white", - ) - elif var == "PERPLEXITY_API_KEY": - error_text.append("• ", style="white") - error_text.append("PERPLEXITY_API_KEY", style="bold cyan") - error_text.append( - " - API key for Perplexity AI web search (enables real-time research)\n", - style="white", - ) - elif var == "STRIX_REASONING_EFFORT": - error_text.append("• ", style="white") - error_text.append("STRIX_REASONING_EFFORT", style="bold cyan") - error_text.append( - " - Reasoning effort level: none, minimal, low, medium, high, xhigh, " - "max (default: high)\n", - style="white", - ) - - error_text.append("\nExample setup:\n", style="white") - error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white") - - if missing_optional_vars: - for var in missing_optional_vars: - if var == "LLM_API_BASE": - error_text.append( - "export LLM_API_BASE='http://localhost:11434' " - "# needed for local models only\n", - style="dim white", - ) - elif var == "PERPLEXITY_API_KEY": - error_text.append( - "export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white" - ) - elif var == "STRIX_REASONING_EFFORT": - error_text.append( - "export STRIX_REASONING_EFFORT='high'\n", - style="dim white", - ) - - panel = Panel( - error_text, - title="[bold white]STRIX", - title_align="left", - border_style="red", - padding=(1, 2), - ) - - logger.debug("Missing required env vars: %s", missing_required_vars) - console.print("\n") - console.print(panel) - console.print() - sys.exit(1) - logger.info( - "Environment OK (optional missing: %s)", - missing_optional_vars or "none", - ) - - -def check_docker_installed() -> None: - if shutil.which("docker") is None: - logger.debug("Docker CLI not found in PATH") - console = Console() - error_text = Text() - error_text.append("DOCKER NOT INSTALLED", style="bold red") - error_text.append("\n\n", style="white") - error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white") - error_text.append( - "Please install Docker and ensure the 'docker' command is available.\n\n", style="white" - ) - - panel = Panel( - error_text, - title="[bold white]STRIX", - title_align="left", - border_style="red", - padding=(1, 2), - ) - console.print("\n", panel, "\n") - sys.exit(1) - logger.debug("Docker CLI present") - - -def pull_docker_image() -> None: - from docker.errors import DockerException - - console = Console() - client = check_docker_connection() - - image = load_settings().runtime.image - - if image_exists(client, image): - logger.debug("Docker image already present locally: %s", image) - return - - logger.info("Pulling docker image: %s", image) - console.print() - console.print(f"[dim]Pulling image[/] {image}") - console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]") - console.print() - - with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status: - try: - layers_info: dict[str, str] = {} - last_update = "" - - for line in client.api.pull(image, stream=True, decode=True): - last_update = process_pull_line(line, layers_info, status, last_update) - - except DockerException as e: - logger.debug("Failed to pull docker image %s", image, exc_info=True) - console.print() - error_text = Text() - error_text.append("FAILED TO PULL IMAGE", style="bold red") - error_text.append("\n\n", style="white") - error_text.append(f"Could not download: {image}\n", style="white") - error_text.append(str(e), style="dim red") - - panel = Panel( - error_text, - title="[bold white]STRIX", - title_align="left", - border_style="red", - padding=(1, 2), - ) - console.print(panel, "\n") - sys.exit(1) - - logger.info("Docker image %s ready", image) - success_text = Text() - success_text.append("Docker image ready", style="#22c55e") - console.print(success_text) - console.print() +B \ No newline at end of file diff --git a/strix/runtime/backends.py b/strix/runtime/backends.py index ec49f7a7..5398ed8b 100644 --- a/strix/runtime/backends.py +++ b/strix/runtime/backends.py @@ -1,4 +1,4 @@ -"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker).""" +­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker).""" from __future__ import annotations @@ -40,10 +40,12 @@ async def _docker_backend( 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() diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py index 969267d5..c70f2d2d 100644 --- a/strix/runtime/docker_client.py +++ b/strix/runtime/docker_client.py @@ -1,292 +1,114 @@ -"""StrixDockerSandboxClient — preserves the image's ENTRYPOINT and adds -NET_ADMIN/NET_RAW capabilities + host-gateway. +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ýš^ØÚÙ\”Ø[™›ÞÛY[�8 %™\Ù\�™\ÈH[XYÙIÜÈS•–TÒS•[™Y“‘UÐQRS‹Ó‘UÔ�UÈØ\Xš[]Y\È +ÈÜÝ YØ]]Ø^K‚‚•HÑÉÜÈØÚÙ\”Ø[™›ÞÛY[� —ØÜ™X]WØÛÛ�Z[™\˜Ù\È›Ý^ÜÙHHÛÚț܂™^[™[™ÈÜ™X]WÚÝØ\™ÜØ™Y›Ü™HÛÛ�Z[™\œË˜Ü™X]X\ÈØ[Y ˆÙHÝX˜Û\ܘ[™™Z[\[Y[�HY]Ù›ÙH™\˜˜][Hœ›ÛHHÑÈÛÝ\˜ÙKÚ]™YB™[\΂‚ŒKˆ›ÜHÑÉÜÈ[�ž\Ú[�VÈ�Z[—XÝ™\œšYNÈÝ\HÈ�Z[‹‹Yˆ‹ˆ‹Ù]‹Û�[—X\ÈÛÛ[X[™[œÝXY ˆ\È]ÈÝ\ˆ[XYÙI܈ØÚÙ\‹Y[�ž\Ú[� œÚXÝX[H�[ˆ8 %Ú]Ý]] ØZYËXÛX™]™\‚ˆÝ\�È[œÚYHHÛÛ�Z[™\ˆ[™›ÛÝݘ\ØØZYØ™]šY\ÈYØZ[œÝBˆXYÜ� ‚Œ‹ˆ\[™‘UÐQRS‹Ó‘UÔ�UÈÈØ\ØY +™\]Z\™YžH›X\ \ÔØ[™ˆÝ\ˆ˜]Ë\ÛØÚÙ]ÛÛÊK‚ŒËˆYÜÝ ™ØÚÙ\‹š[�\›˜[8¡¤ˆÜÝ YØ]]Ø^HÈ^˜WÚÜÝØÛÈBˆYÙ[�Ø[ˆ™XXÚÜÝ \Ù\�™Y\Ë‚‚”[›™YÈÜ[˜ZKXYÙ[�ÏOL ŒM �˜ ˆ�[\[™ÈHÑÈ™\]Z\™\œ™K[Y\™Ú[™ÈH\™[�›ÙKˆ˜XÚÈ\Ý™X[H›Üˆ[ˆ[š™XÝ[ÛˆÛÚË‚ˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�ÛÛ�^X‚š[\Ü�ÙÙÚ[™Âš[\Ü�Üš[\Ü�]ZY™œ›ÛH\[™È[\Ü�[žK]\˜[ Ø\Ý‚™œ›ÛHYÙ[�ËœØ[™›Þ ™\œ›ÜœÈ[\Ü�^ÜÙYÜ�[˜]˜Z[X›Q\œ›Ü‚™œ›ÛHYÙ[�ËœØ[™›Þ ›X[šY™\Ý[\Ü�X[šY™\Ý™œ›ÛHYÙ[�ËœØ[™›Þ œØ[™›Þ\Ë™ØÚÙ\ˆ[\Ü� +ˆØÚÙ\”Ø[™›ÞÛY[� ˆØÚÙ\”Ø[™›ÞÙ\ÜÚ[Û‹ˆØ�Z[ÙØÚÙ\—Ý›Û[YWÛ[Ý[�ËˆÙØÚÙ\—ÜÜ�ÚÙ^KˆÛX[šY™\ÝÜ™\]Z\™\×Ù�\ÙKˆÛX[šY™\ÝÜ™\]Z\™\×ÜÞ\רYZ[‹ŠB™œ›ÛHYÙ[�ËœØ[™›Þ œÙ\ÜÚ[Û‹œØ[™›ÞÜÙ\ÜÚ[Ûˆ[\Ü�Ø[™›ÞÙ\ÜÚ[Û‚™œ›ÛHYÙ[�ËœØ[™›Þ �\\È[\Ü�^ÜÙYÜ�[™Ú[�™œ›ÛHØÚÙ\ˆ[\Ü�\œ›ÜœÈ\ÈØÚÙ\—Ù\œ›ÜœÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹›[Ù[˘ÛÛ�Z[™\œÈ[\Ü�ÛÛ�Z[™\ˆÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�\\È[\Ü�ÙÐÛÛ™šYÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�\\È[\Ü�[Ý[�\ÈØÚÙ\”ÑÓ[Ý[�È\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�][È[\Ü�\œÙWÜ™\ÜÚ]ÜžWÝYÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛH™\]Y\ÝË™^Ù\[ÛœÈ[\Ü�™\]Y\Ý^Ù\[Û‚‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚—ÔÐS‘“ÖÓ‘UÓÔ’×ÑS•ˆH”Õ’VÑÐÒÑT—ÔÐS‘“ÖÓ‘UÓÔ’È‚‚‚™YˆÙ[œÝ\™WÚ[XYÙWØ]˜Z[X›JˆÛY[�ˆ[žKˆ[XYÙNˆÝ‹ˆ[ÜÛXÞNˆ]\˜[Ș]]È‹›™]™\ˆ—KŠH Oˆ›Û™N‚ˆYˆÛY[� š[XYÙWÙ^\ÝÊ[XYÙJN‚ˆ™]\›‚ˆYˆ[ÜÛXÞHOH›™]™\ˆŽ‚ˆ˜Z\ÙH�[�[YQ\œ›ÜŠˆˆ‘ØÚÙ\ˆ[XYÙHÚ[XYÙH\ŸH\È›Ý]˜Z[X›HØØ[H[™‚ˆ”Õ’VÒSPQÑWÔSÔÓPÖO[™]™\ˆ›Ü˜šYÈ[[™È]‚ˆ +Bˆ™\ËYÈH\œÙWÜ™\ÜÚ]ÜžWÝYÊ[XYÙJBˆÛY[� ™ØÚÙ\—ØÛY[� š[XYÙ\Ëœ[ +™\ËYÏ]YÈ܈›Û™K[ÝYÜÏQ˜[ÙJB‚‚™YˆÜØ[™›ÞÛ™]ÛÜšÊ +H OˆÝˆ›Û™N‚ˆ˜[YHHÜË™[�š\›Û‹™Ù] +ÔÐS‘“ÖÓ‘UÓÔ’×ÑS•‹ˆŠKœÝš\ -The SDK's ``DockerSandboxClient._create_container`` does not expose a hook for -extending ``create_kwargs`` before ``containers.create`` is called. We subclass -and reimplement the method body verbatim from the SDK source, with three -deltas: +Bˆ™]\›ˆ˜[YH܈›Û™B‚‚™YˆØ\WÜØ[™›ÞÛ™]ÛÜšÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆ™]ÛÜšÈHÜØ[™›ÞÛ™]ÛÜšÊ +BˆYˆ™]Ûܚ΂ˆÜ™X]WÚÝØ\™ÜÖÈ›™]ÛܚȗHH™]ÛܚˆÜ™X]WÚÝØ\™ÜËœÜ +œÜ�È‹›Û™JB‚‚™YˆØ\WÜ™\ÛÝ\˜ÙWÛ[Z]ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆˆˆ�\HÜ[Û˜[ÙÜ›Ý\™\ÛÝ\˜ÙHØ\Èœ›ÛHH[�š\›Û›Y[� ˆ[œÙ] Ø›[šÂˆ˜[Y\ÈX]™HØÚÙ\‰ÜÈY˜][ +[˜›Ý[™Y +KÛÈ\È\ÈÜ Z[ˆ\ˆÜÝ ˆˆˆ‚ˆY[WÛ[Z]HÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÓQSWÓSRU‹ˆŠKœÝš\ -1. Drop the SDK's ``entrypoint=["tail"]`` override; supply ``["tail", "-f", - "/dev/null"]`` as ``command`` instead. This lets our image's - ``docker-entrypoint.sh`` actually run — without it, ``caido-cli`` never - starts inside the container and ``bootstrap_caido`` retries against a - dead port. -2. Append NET_ADMIN/NET_RAW to ``cap_add`` (required by ``nmap -sS`` and - other raw-socket tools). -3. Add ``host.docker.internal`` → host-gateway to ``extra_hosts`` so the - agent can reach host-served apps. +BˆYˆY[WÛ[Z]‚ˆÜ™X]WÚÝØ\™ÜÖÈ›Y[WÛ[Z]—HHY[WÛ[Z]‚ˆÚWÜÚ^™HHÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÔÒWÔÒV‘H‹ˆŠKœÝš\ -Pinned to ``openai-agents==0.14.6``. Bumping the SDK requires -re-merging the parent body. Track upstream for an injection hook. -""" +BˆYˆÚWÜÚ^™N‚ˆÜ™X]WÚÝØ\™ÜÖÈœÚWÜÚ^™H—HHÚWÜÚ^™B‚ˆÜ\ÈHÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÐÔTÈ‹ˆŠKœÝš\ -from __future__ import annotations +BˆYˆÜ\΂ˆÚ]ÛÛ�^X‹œÝ\™\Üʘ[YQ\œ›Ü‹Ý™\™›ÝÑ\œ›ÜŠN‚ˆ˜[›×ØÜ\ÈH[� +›Ø] +Ü\ÊH +ˆ WÌ Ì Ì +BˆYˆ ˜[›×ØÜ\ÈH ŠŠ�ŒÈ H N‚ˆÜ™X]WÚÝØ\™ÜÖÈ›˜[›×ØÜ\È—HH˜[›×ØÜ\‚ˆY×Û[Z]HÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÔQ×ÓSRU‹ˆŠKœÝš\ -import contextlib -import logging -import os -import uuid -from typing import Any, cast +BˆYˆY×Û[Z]‚ˆÚ]ÛÛ�^X‹œÝ\™\Üʘ[YQ\œ›ÜŠN‚ˆÜ™X]WÚÝØ\™ÜÖÈœY×Û[Z]—HH[� +Y×Û[Z] +B‚‚™YˆØ\WÛÙ×Û[Z]ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆˆˆ�›Ý[™HÛÛ�Z[™\‰ÜÈœÛÛ‹Yš[HÙÈÛÈH�[˜]Ø^H›ØÙ\ÜÈ[ˆHØ[™›Þˆ +K™ËˆHÛÛ]�\ÞK[ÛÜÈÜš][™ÈÈÝÝ] +HØ[››Ýš[HÜÝ\Úˆ[™ZÙHHØÚÙ\ˆY[[ÛˆÝÛˆÚ]] ‚‚ˆ[›ZÙHHÙÜ›Ý\Ø\ÈX›Ý™K\ÈY˜][È +Š›ÛŠŠˆ8 %ØÚÙ\‰ÜÈÝÛˆY˜][ˆ\È[ˆ[˜›Ý[™YœÛÛ‹Yš[KÚXÚ\È[œØY™H›Üˆ[ˆ]]Û›Û[Ý\ÈYÙ[�]ˆ^XÝ]\È\˜š]˜\žHÛÛ[X[™ˈX^ Yš[X›Ý][ÛˆYX[œÈHÛ‹Y\ÚÈØ\\ˆX^ \Ú^™H +ˆX^ Yš[X ˆÙ]Õ’VÔÐS‘“ÖÓÑ×ÓPVÔÒV‘XÈ  ØÙ™˜ˆÈܘXÚÈÝ]ÈØÚÙ\‰ÜÈY˜][ ˆˆˆ‚ˆX^ÜÚ^™HHÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÓÑ×ÓPVÔÒV‘H‹�LHŠKœÝš\ -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 +BˆYˆX^ÜÚ^™K›ÝÙ\Š +H[ˆ +Œ‹›Ù™ˆ‹››Û™H‹�[›[Z]YŠN‚ˆ™]\›‚ˆX^Ùš[HHÜË™[�š\›Û‹™Ù] +”Õ’VÔÐS‘“ÖÓÑ×ÓPVÑ’SH‹ŒÈŠKœÝš\ +H܈ŒÈ‚ˆÜ™X]WÚÝØ\™ÜÖÈ›ÙרÛÛ™šYÈ—HHÙÐÛÛ™šYʈ\OSÙÐÛÛ™šYË�\\Ë’”ÓÓ‹ˆÛÛ™šYÏ^È›X^ \Ú^™HŽˆX^ÜÚ^™K›X^ Yš[HŽˆX^Ùš[_Kˆ +B‚‚™YˆØ\WÜ�[—ÛX™[ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆ�[—ÚYHÜË™Ù][�ДՒVÔ•S—ÒQŠBˆYˆ›Ý�[—ÚY‚ˆ™]\›‚ˆX™[ÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ +›X™[È‹ßJBˆYˆ›Ý\Ú[œÝ[˜ÙJX™[ËXÝ +N‚ˆ™]\›‚ˆX™[ÖȜݚ^ \�[‹ZY—HH�[—ÚYˆ�[—Ý\HHÜË™Ù][�ДՒVÔ•S—ÕTHŠBˆYˆ�[—Ý\N‚ˆX™[ÖȜݚ^ \�[‹]\H—HH�[—Ý\B‚‚˜Û\ÜÈÝš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠN‚ˆØ[™›ÞÛ™]ÛܚΈÝˆHˆ‚‚ˆ\Þ[˜ÈYˆÜ™\ÛÛ™WÙ^ÜÙYÜÜ� +Ù[‹Ü�ˆ[� +H Oˆ^ÜÙYÜ�[™Ú[�‚ˆžN‚ˆÙ[‹—ØÛÛ�Z[™\‹œ™[ØY -logger = logging.getLogger(__name__) +Bˆ^Ù\ØÚÙ\—Ù\œ›ÜœË�TQ\œ›Üˆ\ÈN‚ˆ˜Z\ÙH^ÜÙYÜ�[˜]˜Z[X›Q\œ›ÜŠˆÜ�\Ü� ˆ^ÜÙYÜÜ�Ï\Ù[‹œÝ]K™^ÜÙYÜÜ�ˈ™X\ÛÛ�H˜˜XÚÙ[™Ý[˜]˜Z[X›H‹ˆÛÛ�^^ˆ˜˜XÚÙ[™Žˆ™ØÚÙ\ˆ‹ˆ™]Z[Žˆ˜ÛÛ�Z[™\—Ü™[ØYÙ˜Z[Y‹ˆ›™]ÛܚȎˆÙ[‹œØ[™›ÞÛ™]ÛܚˈKˆØ]\ÙOYKˆ +Hœ›ÛHB‚ˆ]œÈHÙ]]ŠÙ[‹—ØÛÛ�Z[™\‹˜]œÈ‹ßJH܈ßBˆ™]ÛÜšÜÈH]œË™Ù] +“™]ÛÜšÔÙ][™ÜÈ‹ßJK™Ù] +“™]ÛÜšÜÈ‹ßJBˆ[™Ú[�H™]ÛÜšÜË™Ù] +Ù[‹œØ[™›ÞÛ™]ÛÜšÊH܈ßBˆ\H[™Ú[� ™Ù] +’TY™\ÜÈŠH܈[™Ú[� ™Ù] +‘Ûؘ[T��Y™\ÜÈŠBˆYˆ›Ý\Ú[œÝ[˜ÙJ\ ÝŠH܈›Ý\‚ˆ˜Z\ÙH^ÜÙYÜ�[˜]˜Z[X›Q\œ›ÜŠˆÜ�\Ü� ˆ^ÜÙYÜÜ�Ï\Ù[‹œÝ]K™^ÜÙYÜÜ�ˈ™X\ÛÛ�H˜˜XÚÙ[™Ý[˜]˜Z[X›H‹ˆÛÛ�^^ˆ˜˜XÚÙ[™Žˆ™ØÚÙ\ˆ‹ˆ™]Z[Žˆ˜ÛÛ�Z[™\—Û›ÝÛÛ—Û™]Ûܚȋˆ›™]ÛܚȎˆÙ[‹œØ[™›ÞÛ™]ÛܚˈKˆ +BˆÜÝHˆ–ÞÚ\WHˆYˆŽˆˆ[ˆ\[ÙH\ˆ™]\›ˆ^ÜÙYÜ�[™Ú[� +ÜÝZÜÝ Ü�\Ü� ÏQ˜[ÙJB‚‚˜Û\ÜÈÝš^ØÚÙ\”Ø[™›ÞÛY[� +ØÚÙ\”Ø[™›ÞÛY[� +N‚ˆÈÜÝ\™XÝÜšY\ÈÈš[™ [[Ý[�[�ÈHÛÛ�Z[™\‹Ù]žHHØÚÙ\‚ˆÈ˜XÚÙ[™™Y›Ü™HÜ™X]J +X ˆXXÚ][H\ÈÜÛÝ\˜ÙK\™Ù] ™XYÛÛ›_X ‚ˆÝš^Øš[™Û[Ý[�Έ\ÝÙXÝÜÝ‹[žWWH›Û™HH›Û™Bˆ[XYÙWÜ[ÜÛXÞNˆ]\˜[Ș]]È‹›™]™\ˆ—HH˜]]È‚‚ˆ\Þ[˜ÈYˆØÜ™X]WØÛÛ�Z[™\ŠˆÙ[‹ˆ[XYÙNˆÝ‹ˆ +‹ˆX[šY™\݈X[šY™\ݛۙHH›Û™Kˆ^ÜÙYÜÜ�Έ\VÚ[�  ‹‹—HH +KˆÙ\ÜÚ[Û—ÚYˆ]ZY •URQ›Û™HH›Û™Kˆ +H OˆÛÛ�Z[™\Ž‚ˆÈ KKKKH‘QÒSˆ‘T��USHÓÔHÙˆØÚÙ\”Ø[™›ÞÛY[� —ØÜ™X]WØÛÛ�Z[™\ˆ KKKKBˆÈÑÈ™YŽˆÜ˜ËØYÙ[�ËÜØ[™›Þ ÜØ[™›Þ\ËÙØÚÙ\‹œNŒM Í LM ÍÈ +Œ ŒM �ŠK‚ˆÙ[œÝ\™WÚ[XYÙWØ]˜Z[X›JÙ[‹[XYÙKÙ[‹š[XYÙWÜ[ÜÛXÞJB‚ˆ\ÜÙ\�Ù[‹š[XYÙWÙ^\ÝÊ[XYÙJBˆ[�š\›Û›Y[�ˆXÝÜ݋ݗH›Û™HH›Û™BˆYˆX[šY™\Ý‚ˆ[�š\›Û›Y[�H]ØZ]X[šY™\Ý ™[�š\›Û›Y[� œ™\ÛÛ™J +BˆÈÝš^[Hœ›ÛHHÑÈ›ÙNˆ›Ü[�ž\Ú[�Ý™\œšYH[™ˆÈÝ\HZ[ Yˆ Ù]‹Û�[\ÈÛÛ[X[™ÛÈH[XYÙI܈ÈS•–TÒS• +ØÚÙ\‹Y[�ž\Ú[� œÚ +H�[œÈÙ]\ [ˆ^XˆÈ‰˜™XÛÛY\È^XÈZ[ Yˆ Ù]‹Û�[›ÜˆHÙY\ X[]™K‚ˆÈÚ]Ý]\ËØZYËXÛH +ÈH[‹XÛÛ�Z[™\ˆÐH�\Ý™]™\ˆÙ]ˆÈ[š]X[^™Y ‚ˆÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWHHˆš[XYÙHŽˆ[XYÙKˆ™]XÚŽˆ�YKˆ˜ÛÛ[X[™ŽˆÈ�Z[‹‹Yˆ‹‹Ù]‹Û�[—Kˆ™[�š\›Û›Y[�Žˆ[�š\›Û›Y[� ˆBˆYˆX[šY™\Ý\țݛۙN‚ˆØÚÙ\—Û[Ý[�ÈHØ�Z[ÙØÚÙ\—Ý›Û[YWÛ[Ý[�ʈX[šY™\Ý ˆÙ\ÜÚ[Û—ÚY\Ù\ÜÚ[Û—ÚY ˆ +BˆYˆØÚÙ\—Û[Ý[�΂ˆÜ™X]WÚÝØ\™ÜÖÈ›[Ý[�È—HHØÚÙ\—Û[Ý[�ˆYˆÛX[šY™\ÝÜ™\]Z\™\×Ù�\ÙJX[šY™\Ý +N‚ˆÜ™X]WÚÝØ\™ÜË�\]Jˆ]šXÙ\ÏVÈ‹Ù]‹Ù�\ÙH—KˆØ\ØYVÈ”ÖT×ÐQRSˆ—KˆÙXÝ\š]WÛÜVȘ\\›[ÜŽ�[˜ÛÛ™š[™Y—Kˆ +Bˆ[YˆÛX[šY™\ÝÜ™\]Z\™\×ÜÞ\רYZ[ŠX[šY™\Ý +N‚ˆÜ™X]WÚÝØ\™ÜË�\]JˆØ\ØYVÈ”ÖT×ÐQRSˆ—KˆÙXÝ\š]WÛÜVȘ\\›[ÜŽ�[˜ÛÛ™š[™Y—Kˆ +BˆYˆ^ÜÙYÜÜ�΂ˆÜ™X]WÚÝØ\™ÜÖÈœÜ�È—HHˆÙØÚÙ\—ÜÜ�ÚÙ^JÜ� +Nˆ +ŒL�ËŒ Œ ŒH‹›Û™JH›ÜˆÜ�[ˆ^ÜÙYÜÜ�ˆBˆÈ KKKKHS‘‘T��USHÓÔH KKKKB‚ˆÈÝš^[š™XÝ[ÛœÈ8 %\[™ Û‰ÝÝ™\�Üš]KÛÈ•TÑKÔÖT×ÐQRSˆÝ\�š]™\Ë‚ˆØ\ØYHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ +˜Ø\ØY‹×JBˆYˆ›Ý\Ú[œÝ[˜ÙJØ\ØY \Ý +N‚ˆØ\ØYH\Ý +Ø\ØY +BˆÜ™X]WÚÝØ\™ÜÖȘØ\ØY—HHØ\ØYˆ›ÜˆØ\[ˆ +“‘UÐQRSˆ‹“‘UÔ�UÈŠN‚ˆYˆØ\›Ý[ˆØ\ØY‚ˆØ\ØY ˜\[™ +Ø\ +B‚ˆ^˜WÚÜÝÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ +™^˜WÚÜÝÈ‹ßJBˆ^˜WÚÜÝÖÈšÜÝ ™ØÚÙ\‹š[�\›˜[—HHšÜÝ YØ]]Ø^H‚‚ˆØ\WÜØ[™›ÞÛ™]ÛÜšÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÜ™\ÛÝ\˜ÙWÛ[Z]ÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÛÙ×Û[Z]ÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÜ�[—ÛX™[ÊÜ™X]WÚÝØ\™ÜÊB‚ˆÈÝš^[š™XÝ[ÛŽˆØØ[ÛÝ\˜ÙH™Y\ËÛÜ�YÚ[ÝÙ\Ý Yš\œÝÛÈBˆÈ™\ÝYÜXÈ[™ÈÛˆÜÙˆH™YH]ÛÝ™\œË‚ˆš[™Û[Ý[�ÈHÙ[‹œÝš^Øš[™Û[Ý[�È܈ -_SANDBOX_NETWORK_ENV = "STRIX_DOCKER_SANDBOX_NETWORK" +BˆYˆš[™Û[Ý[�΂ˆ[Ý[�ÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ +›[Ý[�È‹×JBˆ›ÜˆÜXÈ[ˆÛÜ�Y +š[™Û[Ý[�ËÙ^O[[X™HΈÝŠÖÈ�\™Ù]—JK˜ÛÝ[� +‹ÈŠJN‚ˆ[Ý[�˘\[™ +ˆØÚÙ\”ÑÓ[Ý[� +ˆ\™Ù]\ÜXÖÈ�\™Ù]—KˆÛÝ\˜ÙO\ÜXÖÈœÛÝ\˜ÙH—Kˆ\OH˜š[™‹ˆ™XYÛÛ›O\ÜXË™Ù] +œ™XYÛÛ›H‹˜[ÙJKˆ +Bˆ +B‚ˆÙÙÙ\‹™X�Yʈ�Ü™X][™ÈØ[™›ÞÛÛ�Z[™\Žˆ[XYÙOI\ÈØ\ÏI\È^ÜÙYÜÜ�ÏI\È‹ˆ[XYÙKˆØ\ØY ˆ\Ý +^ÜÙYÜÜ�ÊKˆ +BˆÛÛ�Z[™\ˆHÙ[‹™ØÚÙ\—ØÛY[� ˜ÛÛ�Z[™\œË˜Ü™X]J +Š˜Ü™X]WÚÝØ\™ÜÊBˆÙÙÙ\‹š[™›Êˆ”Ø[™›ÞÛÛ�Z[™\ˆÜ™X]YˆYI\È[XYÙOI\È‹ˆÛÛ�Z[™\‹œÚÜ�ÚYYˆ\Ø]ŠÛÛ�Z[™\‹œÚÜ�ÚYŠH[ÙH�È‹ˆ[XYÙKˆ +Bˆ™]\›ˆÛÛ�Z[™\‚‚ˆ\Þ[˜ÈYˆÜ™X]JÙ[‹ +ŠšÝØ\™ÜΈ[žJH OˆØ[™›ÞÙ\ÜÚ[ÛŽ‚ˆÙ\ÜÚ[ÛˆH]ØZ]Ý\\Š +K˜Ü™X]J +ŠšÝØ\™ÜÊBˆ™]ÛÜšÈHÜØ[™›ÞÛ™]ÛÜšÊ +Bˆ[›™\ˆHÙ\ÜÚ[Û‹—Ú[›™\‚ˆYˆ™]ÛÜšÈ[™\Ú[œÝ[˜ÙJ[›™\‹ØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠN‚ˆ[›™\‹—רÛ\Ü××ÈHÝš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[Û‚ˆØ\Ý +”Ýš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[Ûˆ‹[›™\ŠKœØ[™›ÞÛ™]ÛÜšÈH™]Ûܚˆ™]\›ˆÙ\ÜÚ[Û‚‚ˆ\Þ[˜ÈYˆ[]JÙ[‹Ù\ÜÚ[ÛŽˆØ[™›ÞÙ\ÜÚ[ÛŠH OˆØ[™›ÞÙ\ÜÚ[ÛŽ‚ˆÛÛ�Z[™\—ÚYHÙ]]ŠÙ]]ŠÙ\ÜÚ[Û‹—Ú[›™\‹œÝ]H‹›Û™JK˜ÛÛ�Z[™\—ÚY‹›Û™JBˆYˆÛÛ�Z[™\—ÚY‚ˆÈ™\Ý YY™›Ü�Ú[ˆ›Ý›Ý[™ ÐTQ\œ›ÜˆÛÝ™\ˆHÛÛ™H܈[š\BˆÈÛÛ�Z[™\‹ˆ™\]Y\Ý^Ù\[ÛˆÛÝ™\œÈHÜ›‹YÝÛˆY[[ÛˆÛØÚÙ]8 %ˆÈÛÛ�Z[™\œË™Ù] +H Oˆ[œÜXÝØÛÛ�Z[™\ˆ˜Z\Ù\È™\]Y\ÝɈÈÛÛ›™XÝ[Û‘\œ›Ü‹ÚXÚ\ÈHÚX›[™ÈÙˆØÚÙ\‹™\œ›ÜœË�TQ\œ›Ü‚ˆÈ[™\ˆ™\]Y\ÝË”™\]Y\Ý^Ù\[Ûˆ +›ÝHÝX˜Û\ÜÊKÛÈ]\ØØ\\ˆÈ[ˆTQ\œ›Ü‹[Û›HÝ\™\ÜÈ[™Ý\™˜XÙ\ÈH�[˜XÙX˜XÚÈ]™[‚ˆÈÝYÚ\ÈX\™ÝÛˆ\ÈYX[�È™H™\Ý YY™›Ü� ‚ˆÚ]ÛÛ�^X‹œÝ\™\ÜʈØÚÙ\—Ù\œ›ÜœË“›Ý›Ý[™ ØÚÙ\—Ù\œ›ÜœË�TQ\œ›Ü‹™\]Y\Ý^Ù\[Û‚ˆ +N‚ˆÙ[‹™ØÚÙ\—ØÛY[� ˜ÛÛ�Z[™\œË™Ù] +ÛÛ�Z[™\—ÚY +KšÚ[ -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}, - ) - - -def _apply_run_labels(create_kwargs: dict[str, Any]) -> None: - run_id = os.getenv("STRIX_RUN_ID") - if not run_id: - return - labels = create_kwargs.setdefault("labels", {}) - if not isinstance(labels, dict): - return - labels["strix-run-id"] = run_id - run_type = os.getenv("STRIX_RUN_TYPE") - if run_type: - labels["strix-run-type"] = run_type - - -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 - - async def _create_container( - self, - image: str, - *, - manifest: Manifest | None = None, - exposed_ports: tuple[int, ...] = (), - session_id: uuid.UUID | None = None, - ) -> Container: - # ----- BEGIN VERBATIM COPY of DockerSandboxClient._create_container ----- - # SDK ref: src/agents/sandbox/sandboxes/docker.py:1434-1477 (v0.14.6). - if not self.image_exists(image): - repo, tag = parse_repository_tag(image) - self.docker_client.images.pull(repo, tag=tag or None, all_tags=False) - - assert self.image_exists(image) - environment: dict[str, str] | None = None - if manifest: - environment = await manifest.environment.resolve() - # Strix delta from the SDK body: drop ``entrypoint`` override and - # supply ``tail -f /dev/null`` as ``command`` so the image's - # ENTRYPOINT (``docker-entrypoint.sh``) runs setup, then ``exec - # "$@"`` becomes ``exec tail -f /dev/null`` for the keep-alive. - # Without this, caido-cli + the in-container CA trust never get - # initialized. - create_kwargs: dict[str, Any] = { - "image": image, - "detach": True, - "command": ["tail", "-f", "/dev/null"], - "environment": environment, - } - if manifest is not None: - docker_mounts = _build_docker_volume_mounts( - manifest, - session_id=session_id, - ) - if docker_mounts: - create_kwargs["mounts"] = docker_mounts - if _manifest_requires_fuse(manifest): - create_kwargs.update( - devices=["/dev/fuse"], - cap_add=["SYS_ADMIN"], - security_opt=["apparmor:unconfined"], - ) - elif _manifest_requires_sys_admin(manifest): - create_kwargs.update( - cap_add=["SYS_ADMIN"], - security_opt=["apparmor:unconfined"], - ) - if exposed_ports: - create_kwargs["ports"] = { - _docker_port_key(port): ("127.0.0.1", None) for port in exposed_ports - } - # ----- END VERBATIM COPY ----- - - # Strix injections — append, don't overwrite, so FUSE/SYS_ADMIN survives. - cap_add = create_kwargs.setdefault("cap_add", []) - if not isinstance(cap_add, list): - cap_add = list(cap_add) - create_kwargs["cap_add"] = cap_add - for cap in ("NET_ADMIN", "NET_RAW"): - if cap not in cap_add: - cap_add.append(cap) - - 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) - _apply_run_labels(create_kwargs) - - # Strix injection: local source trees, sorted shallowest-first so a - # nested spec lands on top of the tree it covers. - bind_mounts = self.strix_bind_mounts or () - if bind_mounts: - mounts = create_kwargs.setdefault("mounts", []) - for spec in sorted(bind_mounts, key=lambda s: str(s["target"]).count("/")): - mounts.append( - DockerSDKMount( - target=spec["target"], - source=spec["source"], - type="bind", - read_only=spec.get("read_only", False), - ) - ) - - logger.debug( - "Creating sandbox container: image=%s caps=%s exposed_ports=%s", - image, - cap_add, - list(exposed_ports), - ) - container = self.docker_client.containers.create(**create_kwargs) - logger.info( - "Sandbox container created: id=%s image=%s", - container.short_id if hasattr(container, "short_id") else "?", - image, - ) - 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: - # Best-effort kill: NotFound/APIError cover a gone or unhappy - # container. RequestException covers a torn-down daemon socket — - # containers.get() -> inspect_container raises requests' - # ConnectionError, which is a sibling of docker.errors.APIError - # under requests.RequestException (not a subclass), so it escapes - # an APIError-only suppress and surfaces a full traceback even - # though this teardown is meant to be best-effort. - with contextlib.suppress( - docker_errors.NotFound, docker_errors.APIError, RequestException - ): - self.docker_client.containers.get(container_id).kill() - return await super().delete(session) +Bˆ™]\›ˆ]ØZ]Ý\\Š +K™[]JÙ\ÜÚ[ÛŠB \ No newline at end of file diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index e83ab119..6a3a3a84 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -1,218 +1,66 @@ -"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence.""" +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ•\Ýț܈ݚ^ ˜ÛÛ™šYË›ØY\Žˆ”Óӈݙ\œšY\Ë[X\È™\ÛÛ][Û‹\œÚ\Ý[˜ÙKˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�œÛÛ‚™œ›ÛH\[™È[\Ü�TWÐÒPÒÒS‘‚š[\Ü�]\Ý™œ›ÛHY[�XÈ[\Ü�[X\ÐÚÚXÙ\ËšY[ ˜[Y][Û‘\œ›Ü‚™œ›ÛHY[�XË™šY[È[\Ü�šY[[™›Â‚™œ›ÛHÝš^ ˜ÛÛ™šYÈ[\Ü�ØY\‚™œ›ÛHÝš^ ˜ÛÛ™šYËœÙ][™ÜÈ[\Ü�ÛÛ�^Ù][™Ü‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛH]Xˆ[\Ü�]‚‚—ÓWÑS•—ÒÑVTÈHˆ”Õ’VÓH‹ˆ“WÐTWÒÑVH‹ˆ“ÔS�RWÐTWÒÑVH‹ˆ“WÐTWÐ�TÑH‹ˆ“ÔS�RWÐTWÐ�TÑH‹ˆ“ÔS�RWÐ�TÑWÕT“‹ˆ“USWÐ�TÑWÕT“‹ˆ“ÓSPWÐTWÐ�TÑH‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹ˆ”Õ’VÑ“Ô�ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ“WÕSQSÕU‹ˆ”T”VUWÐTWÒÑVH‹ˆÈ�[�[YTÙ][™Üˆ”Õ’VÒSPQÑH‹ˆ”Õ’VÒSPQÑWÔSÔÓPÖH‹ˆ”Õ’VÔ•S•SQWÐ�PÒÑS‘‹ˆ”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈ‹ˆÈ[[Y]žTÙ][™Üˆ”Õ’VÕSSQU–H‹—B‚‚�]\Ý ™š^\™J]]Ý\ÙOU�YJB™YˆÜ™\Ù]ÛØY\—ÜÝ]J[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú +H Oˆ›Û™N‚ˆˆˆ”™\Ù][Ù[HÛؘ[È[™ÛX\ˆÛ›ÝÛˆ[�ˆ˜\œÈ›Üˆ]\›Z[š\ÝXÈ�[œËˆˆˆ‚ˆ›ÜˆÙ^H[ˆÓWÑS•—ÒÑVT΂ˆ[ÛšÙ^\]Ú ™[[�ŠÙ^K˜Z\Ú[™ÏQ˜[ÙJBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ØØXÚY‹›Û™JBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ÛÝ™\œšYH‹›Û™JB‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈÜ™XYÚœÛÛ—ÛÝ™\œšY\ÂˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛZ\ÜÚ[™×Ùš[J\Ü]ˆ] +H Oˆ›Û™N‚ˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê\Ü] È››ÜKšœÛÛˆŠHOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\רÛÜœ�\ÚœÛÛŠ\Ü]ˆ] +H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +žÛ›Ý˜[YœÛÛˆ‹[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Û›Û—ÙXÝÙ[�Š\Ü]ˆ] +H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ››Ý‹˜H‹™XÝ—_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛX\×Ý×Û™\ÝYÜÙ][™ÜÊ\Ü]ˆ] +H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +ˆœÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ›^K[[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH�]‹N‹ˆ +Bˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHˆ›HŽˆÈ›[Ù[Žˆ›^K[[Ù[ŸKˆš[�Yܘ][ۜȎˆÈœ\œ^]WØ\WÚÙ^HŽˆœÈŸKˆB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÜÚÚ\×ÚÙ^\ר[™XYWÚ[—Ù[�š\›ÛŠˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹™œ›ÛKY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆÈ[�ˆÚ[œÈ OˆH”ÓÓˆ˜[YH\È›ÝÝ\™˜XÙY\È[ˆ[š]ÝØ\™Ë‚ˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[�—ÝÚ[œ×ØXÜ›ÜÜ×ÙšY[Ø[X\Ù\ʈ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈ\WÚÙ^H™\ÛÛ™\Èœ›ÛH[X\ÐÚÚXÙ\Ê“WÐTWÒÑVH‹“ÔS�RWÐTWÒÑVHŠKˆH[�‚ˆÈÙ]ÈÛ™H[X\ÈÚ[HH\œÚ\ÝYš[HÛÈ[›Ý\‹ˆ[�ˆ]\ÝÝ[Ú[‹ÛˆÈHÝ[Hš[H˜[YH]\ݛݙHÝ\™˜XÙY\È[ˆ[š]ÝØ\™È +ÚXÚÝ]˜[šÜÈ[�ŠK‚ˆ[ÛšÙ^\]Ú œÙ][�Š“ÔS�RWÐTWÒÑVH‹œÚËY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ“WÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[�—ÝÚ[œ×ØØ\ÙWÚ[œÙ[œÚ]]™[Jˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈÙ][™ÜÈ\ÙHØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKÛÈHÝÙ\˜Ø\ÙH[�ˆ˜\ˆ[ÛÈÛÝ[�È\ÈÙ] ‚ˆ[ÛšÙ^\]Ú œÙ][�ŠœÝš^ÛH‹™œ›ÛKY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ý\Ù\×ÚœÛÛ—ÝÚ[—Û›×Ø[X\×Ú[—Ù[�š\›ÛŠ\Ü]ˆ] +H Oˆ›Û™N‚ˆÈ›È[X\ÈÙˆ\WÚÙ^H\ÈÙ][ˆH[�š\›Û›Y[� OˆHš[H˜[YH\È\ÙY ]™[‚ˆÈÚ[ˆ]\ÈÝÜ™Y[™\ˆH›Û‹Yš\œÝ[X\Ë‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ“ÔS�RWÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] +HOHÈ›HŽˆȘ\WÚÙ^HŽˆœÚËYš[HŸ_B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\×Ü™Z™XÝ×ÜÝX—Û›ÝXÙWݘ[Y\Ê +H Oˆ›Û™N‚ˆÚ]]\Ý œ˜Z\Ù\ʘ[Y][Û‘\œ›ÜŠN‚ˆÛÛ�^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏM� +B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\רXØÙ\×Ù›ÛÜŠ +H Oˆ›Û™N‚ˆ\ÜÙ\�ÛÛ�^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏLL � +K�ÛÛÛÝ]]ÛX^Øž]\ÈOH L �‚‚™Yˆ\ÝÜ�[�[YWØØZYר›ÛÝÝØZ]Ùœ›ÛWÙ[�Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú +H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÐÐRQ×ГÓÕÕÐRUÔÈ‹ŽL ŠB‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ +Kœ�[�[YK˜ØZYר›ÛÝÝØZ]ÜÈOHL ‚‚™Yˆ\ÝÜ�[�[YWÚ[XYÙWÜ[ÜÛXÞWÙœ›ÛWÙ[�Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú +H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÒSPQÑWÔSÔÓPÖH‹›™]™\ˆŠB‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ +Kœ�[�[YKš[XYÙWÜ[ÜÛXÞHOH›™]™\ˆ‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈØ[X\Ù\×ٛ܂ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ[X\Ù\×ٛܗÜÚ[\WØ[X\Ê +H Oˆ›Û™N‚ˆš[™›ÈHšY[[™›Ê[X\ÏH”ÒSTWÐSPTÈŠBˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHÈ”ÒSTWÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×ٛܗØ[X\רÚÚXÙ\Ê +H Oˆ›Û™N‚ˆš[™›ÎˆšY[[™›ÈHšY[ +È\NˆYۛܙVØ\ÜÚYÛ›Y[�BˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\Ê‘’T”Õ‹”ÑPÓÓ‘ŠKˆ +Bˆ\ÜÙ\�Ø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[�Bˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHȔ՗ÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×Ù›Ü—Û›×Ø[X\Ê +H Oˆ›Û™N‚ˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊šY[[™›Ê +JHOH×B‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\WØÛÛ™šY×ÛÝ™\œšYH +ÈØYÜÙ][™ÜÈ›Ý[™ ]š\ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ\WÛÝ™\œšYWØ[™ÛØYÜÙ][™Ü×Ü›Ý[™Ýš\ +\Ü]ˆ] +H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ +ˆœÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆœ›Ý[™ ]š\ [[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH�]‹N‹ˆ +B‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ] +BˆÙ][™ÜÈHØY\‹›ØYÜÙ][™ÜÊ +B‚ˆ\ÜÙ\�Ù][™ÜË›K›[Ù[OHœ›Ý[™ ]š\ [[Ù[‚ˆ\ÜÙ\�Ù][™ÜËš[�Yܘ][ۜ˜\œ^]WØ\WÚÙ^HOHœÈ‚ˆÈÙXÛÛ™Ø[\ÈY[[Ú^™Y OˆØ[YHØš™XÝ ‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ +H\ÈÙ][™Ü‚‚™Yˆ\ÝØ\WØÛÛ™šY×ÛÝ™\œšYWÚ[�˜[Y]\רØXÚJ\Ü]ˆ] +H Oˆ›Û™N‚ˆš\œÝH\Ü] È™š\œÝ šœÛÛˆ‚ˆš\œÝ �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™š\œÝ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH�]‹NŠBˆÙXÛÛ™H\Ü] ÈœÙXÛÛ™ šœÛÛˆ‚ˆÙXÛÛ™ �Üš]WÝ^ +œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆœÙXÛÛ™ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH�]‹NŠB‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJš\œÝ +Bˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ +K›K›[Ù[OH™š\œÝ [[Ù[‚‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJÙXÛÛ™ +Bˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ +K›K›[Ù[OHœÙXÛÛ™ [[Ù[‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\œÚ\ÝØÝ\œ™[�ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[�ÝÜš]\×Ù[�—Ø›ØÚÊ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú +H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ÈœÝXˆˆ ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù] +B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[� -from __future__ import annotations +B‚ˆ\ÜÙ\�\™Ù] ™^\ÝÊ +Bˆ\ÜÙ\�œÛÛ‹›ØYÊ\™Ù] œ™XYÝ^ +[˜ÛÙ[™ÏH�]‹NŠJHOHˆ™[�ˆŽˆÈ”Õ’VÓHŽˆœ\œÚ\ÝY [[Ù[ŸBˆB‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[�ÜÙ]×Ì Œ Û[ÙJ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú +H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù] +B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[� -import json -from typing import TYPE_CHECKING +B‚ˆ\ÜÙ\�\™Ù] œÝ] -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_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 - - -# --------------------------------------------------------------------------- # -# _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 +KœÝÛ[ÙH ˆ ÍÍÍÈOH ÍŒ  \ No newline at end of file diff --git a/tests/test_image_pull_policy.py b/tests/test_image_pull_policy.py new file mode 100644 index 00000000..a9184833 --- /dev/null +++ b/tests/test_image_pull_policy.py @@ -0,0 +1,85 @@ +­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Regression tests for sandbox image acquisition policy.""" + +from __future__ import annotations + +from typing import Literal +from unittest.mock import MagicMock, patch + +import pytest + +from strix.config.settings import RuntimeSettings, Settings +from strix.interface.environment import pull_docker_image +from strix.runtime.docker_client import StrixDockerSandboxClient + + +def _missing_image_client( + *, pull_policy: Literal["auto", "never"] = "auto" +) -> StrixDockerSandboxClient: + client = StrixDockerSandboxClient.__new__(StrixDockerSandboxClient) + client.docker_client = MagicMock() + client.image_pull_policy = pull_policy + client.image_exists = MagicMock(return_value=False) + return client + + +def test_cli_never_policy_rejects_missing_image_without_pull() -> None: + docker_client = MagicMock() + settings = Settings( + runtime=RuntimeSettings( + STRIX_IMAGE="example.invalid/sandbox@sha256:deadbeef", + STRIX_IMAGE_PULL_POLICY="never", + ) + ) + + with ( + patch("strix.interface.environment.check_docker_connection", return_value=docker_client), + patch("strix.interface.environment.image_exists", return_value=False), + patch("strix.interface.environment.load_settings", return_value=settings), + pytest.raises( + RuntimeError, + match=r"example\.invalid/sandbox@sha256:deadbeef.*forbids pulling", + ), + ): + pull_docker_image() + + docker_client.api.pull.assert_not_called() + + +@pytest.mark.asyncio +async def test_container_creation_rechecks_never_policy_without_pull() -> None: + client = _missing_image_client(pull_policy="never") + + with pytest.raises( + RuntimeError, + match=r"example\.invalid/sandbox@sha256:deadbeef.*forbids pulling", + ): + await client._create_container("example.invalid/sandbox@sha256:deadbeef") + + client.docker_client.images.pull.assert_not_called() + client.docker_client.containers.create.assert_not_called() + + +@pytest.mark.asyncio +async def test_container_creation_uses_present_image_with_never_policy() -> None: + client = _missing_image_client(pull_policy="never") + client.image_exists = MagicMock(return_value=True) + client.docker_client.containers.create.return_value = MagicMock(short_id="abc123") + + await client._create_container("example.invalid/sandbox@sha256:deadbeef") + + client.docker_client.images.pull.assert_not_called() + client.docker_client.containers.create.assert_called_once() + + +@pytest.mark.asyncio +async def test_container_creation_keeps_default_auto_pull() -> None: + client = _missing_image_client() + client.image_exists = MagicMock(side_effect=[False, True]) + client.docker_client.containers.create.return_value = MagicMock(short_id="abc123") + + await client._create_container("example.invalid/sandbox:latest") + + client.docker_client.images.pull.assert_called_once_with( + "example.invalid/sandbox", tag="latest", all_tags=False + ) + client.docker_client.containers.create.assert_called_once() From 02ff1d0a28a76bf46af5ce3aa19a4b7017da8a6c Mon Sep 17 00:00:00 2001 From: maxi-maxima <240684166@qq.com> Date: Wed, 12 Aug 2026 10:20:16 +0800 Subject: [PATCH 2/3] Normalize source line endings --- strix/config/settings.py | 221 +++++------------- strix/interface/environment.py | 315 +++++++++++++++++--------- strix/runtime/backends.py | 132 +++-------- strix/runtime/docker_client.py | 397 ++++++++++++++++++++++++--------- 4 files changed, 595 insertions(+), 470 deletions(-) diff --git a/strix/config/settings.py b/strix/config/settings.py index c6f6572f..78bcec9d 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -1,161 +1,60 @@ -­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""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") - caido_boot_wait_s: int = Field(default=300, gt=0, alias="STRIX_CAIDO_BOOT_WAIT_S") - # 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) +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ýš^\XØ][ÛˆÙ][™ÜÈ8 %Y[�XË\Ù][™ÜÈÝÙ\™Y ˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚™œ›ÛH\[™È[\Ü�]\˜[‚™œ›ÛHY[�XÈ[\Ü�[X\ÐÚÚXÙ\ËšY[™œ›ÛHY[�X×ÜÙ][™ÜÈ[\Ü�˜\ÙTÙ][™ÜËÙ][™ÜÐÛÛ™šYÑXÝ‚‚”™X\ÛÛš[™ÑY™›Ü�H]\˜[È››Û™H‹›Z[š[X[‹›ÝÈ‹›YY][H‹šYÚ‹žYÚ‹›X^—B‚‘Q�USÓPVÕT“”ÈH L ‚—Ð�TÑWÐÓÓ‘’QÈHÙ][™ÜÐÛÛ™šYÑXÝ +ˆØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKˆÜ[]WØžWÛ˜[YOU�YKˆ^˜OHšYۛܙH‹ŠB‚‚˜Û\ÜÈTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�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�RWÐTWÒÑVHŠKˆ™\�Q˜[ÙKˆ +Bˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[ +ˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\ʈ“WÐTWÐ�TÑH‹ˆ“ÔS�RWÐTWÐ�TÑH‹ˆ“ÔS�RWÐ�TÑWÕT“‹ˆ“USWÐ�TÑWÕT“‹ˆ“ÓSPWÐTWÐ�TÑH‹ˆ +Kˆ +Bˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[ +ˆY˜][S›Û™Kˆ[X\ÏH“WÑV�WÒPQT”È‹ˆ™\�Q˜[ÙKˆ +Bˆ™X\ÛÛš[™×ÙY™›Ü�ˆ™X\ÛÛš[™ÑY™›Ü�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Ñ“Ô�ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ +Bˆ›Û\ØØXÚNˆ›ÛÛHšY[ +ˆY˜][U�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[Ý]ˆ[�HšY[ +Y˜][LÌ [X\ÏH“WÕSQSÕUŠBˆÝ™X[WÚYWÝ[Y[Ý]ˆ[�HšY[ +Y˜][LÌ ÙOL [X\ÏH“WÔÕ‘PSWÒQWÕSQSÕUŠBˆX^ÝÛÛØØ[×Ü\—Ý\›Žˆ[�HšY[ +ˆY˜][LÌ‹ˆÙOL ˆ[X\ÏH“WÓPVÕÓÓÐÐS×ÔT—ÕT“ˆ‹ˆ +B‚‚˜Û\ÜÈY\TÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ[Ù[ˆÝˆ›Û™HHšY[ +Y˜][S›Û™K[X\ÏH”Õ’VÑQTWÓSÑSŠBˆ™X\ÛÛš[™×ÙY™›Ü�ˆ™X\ÛÛš[™ÑY™›Ü�›Û™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‹™\�Q˜[ÙJBˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[ +Y˜][S›Û™K[X\ÏH‘QTWÓWÐTWÐ�TÑHŠBˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[ +ˆY˜][S›Û™Kˆ[X\ÏH‘QTWÓWÑV�WÒPQT”È‹ˆ™\�Q˜[ÙKˆ +B‚‚˜Û\ÜÈÛÛ�^Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆˆˆ�ÛÛ�^ ]Ú[™ÝÈX[˜YÙ[Y[�ˆ\‹]ÛÛ [Ý]]Ø\È[™\ÝÜžHÛÛ\XÝ[Û‹ˆˆˆ‚‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ]]רÛÛ\X݈›ÛÛHšY[ +Y˜][U�YK[X\ÏH”Õ’VÐÓÓ•VÐUU×ÐÓÓTPÕŠBˆÛÛ\XÝØ�Y™™\—ÝÚÙ[œÎˆ[�HšY[ +Y˜][LŒÌ ÝL [X\ÏH”Õ’VÐÓÓ•VЕQ‘‘T—ÕÒÑS”ÈŠBˆÙY\ÝÚÙ[œÎˆ[�HšY[ +Y˜][NÌ ÝL [X\ÏH”Õ’VÐÓÓ•VÒÑQTÕÒÑS”ÈŠBˆ˜[˜XÚרÛÛ�^ÝÚÙ[œÎˆ[�HšY[ +ˆY˜][LŒ Ì ÝL [X\ÏH”Õ’VÐÓÓ•VÑ�S�PÒ×ÕÒÑS”È‚ˆ +BˆÝ[[X\žWÛX^ÝÚÙ[œÎˆ[�HšY[ +Y˜][MÌM‹ÝL [X\ÏH”Õ’VÐÓÓ•VÔÕSSPT–WÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^ÝÚÙ[œÎˆ[�HšY[ +Y˜][NÌ ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^Û[™\Έ[�HšY[ +Y˜][L—Ì ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÓS‘TÈŠBˆÈ›Û܈X›Ý™HH�[˜Ø][Û‹[›ÝXÙHÚ^™HÛÈH™]šY]È[Ø^\Èš]Ë‚ˆÛÛÛÝ]]ÛX^Øž]\Έ[�HšY[ +ˆY˜][ML +ˆ L � ÙOLL � [X\ÏH”Õ’VÕÓÓÓÕUUÓPVЖUTÈ‚ˆ +B‚‚˜Û\ÜÈ�[�[YTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�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Ð�PÒÑS‘ŠBˆØZYר›ÛÝÝØZ]ÜΈ[�HšY[ +Y˜][LÌ ÝL [X\ÏH”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈŠBˆÈX^ØÜ™Y[œÚÝ Ú[XYÙHÛÛÝ]]ÈÙ\]™H\ˆYÙ[�ÛÛ�^ + H›Û™JK‚ˆX^ØÛÛ�^Ú[XYÙ\Έ[�HšY[ +Y˜][LËÙOL [X\ÏH”Õ’VÓPVÐÓÓ•VÒSPQÑTÈŠB‚‚˜Û\ÜÈ[[Y]žTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ[˜X›Yˆ›ÛÛHšY[ +Y˜][U�YK[X\ÏH”Õ’VÕSSQU–HŠB‚‚˜Û\ÜÈ[�Yܘ][Û”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ\œ^]WØ\WÚÙ^NˆÝˆ›Û™HHšY[ +ˆY˜][S›Û™Kˆ[X\ÏH”T”VUWÐTWÒÑVH‹ˆ™\�Q˜[ÙKˆ +BˆÜÝX[—Ø\WÚÙ^NˆÝˆ›Û™HHšY[ +ˆY˜][S›Û™Kˆ[X\ÏH”ÔÕPS—ÐTWÒÑVH‹ˆ™\�Q˜[ÙKˆ +B‚‚˜Û\ÜÈšY]Ù\”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆÈ˜\ÙHT“ÙˆHÝš^™[^HHØØ[šY]Ù\ˆ›ÞY\Èț܈[XZ[ˆÈ™\šYšXØ][Ûˆ[™[˜Üž\Y™\Ü�[]™\žKˆHœ›ÝÜÙ\ˆ™]™\ˆ[ÜȈÈH™[^H\™XÝNÈHØØ[Ù\�™\ˆ\ÈHÛ›HØ[\‹‚ˆ\Ý\›ˆÝˆHšY[ +Y˜][HšÎ‹ËØ\ œÝš^ ˜ZH‹[X\ÏH”Õ’VÐTÕT“ŠB‚‚˜Û\ÜÈÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆNˆTÙ][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOSTÙ][™ÜÊBˆY\NˆY\TÙ][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOQY\TÙ][™ÜÊBˆ�[�[YNˆ�[�[YTÙ][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOT�[�[YTÙ][™ÜÊBˆÛÛ�^ˆÛÛ�^Ù][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOPÛÛ�^Ù][™ÜÊBˆ[[Y]žNˆ[[Y]žTÙ][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOU[[Y]žTÙ][™ÜÊBˆ[�Yܘ][ۜΈ[�Yܘ][Û”Ù][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOR[�Yܘ][Û”Ù][™ÜÊBˆšY]Ù\ŽˆšY]Ù\”Ù][™ÜÈHšY[ +Y˜][Ù˜XÝÜžOUšY]Ù\”Ù][™ÜÊB \ No newline at end of file diff --git a/strix/interface/environment.py b/strix/interface/environment.py index 4e6379d5..8f6b3a03 100644 --- a/strix/interface/environment.py +++ b/strix/interface/environment.py @@ -1,115 +1,224 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ý\�\[�š\›Û›Y[�˜[Y][Ûˆ[™ØÚÙ\ˆ[XYÙHX[˜YÙ[Y[� ˆˆˆ‚‚š[\Ü�ÙÙÚ[™Âš[\Ü�Ú][š[\Ü�Þ\‚™œ›ÛHšXÚ ˜ÛÛœÛÛH[\Ü�ÛÛœÛÛB™œ›ÛHšXÚ œ[™[[\Ü�[™[™œ›ÛHšXÚ �^[\Ü�^‚™œ›ÛHÝš^ ˜ÛÛ™šYÈ[\Ü�ÛÙ^ ØYÜÙ][™Ü™œ›ÛHÝš^ š[�\™˜XÙK�][È[\Ü� -ˆÚXÚ×ÙØÚÙ\—ØÛÛ›™XÝ[Û‹ˆ[XYÙWÙ^\Ýˈ›ØÙ\Ü×Ü[Û[™KŠB‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚™Yˆ˜[Y]WÙ[�š\›Û›Y[� +­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Startup environment validation and Docker image management.""" -H Oˆ›Û™N‚ˆÙÙÙ\‹š[™›Ê•˜[Y][™È[�š\›Û›Y[�ŠBˆÛÛœÛÛHHÛÛœÛÛJ -BˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÈH×BˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÈH×B‚ˆÙ][™ÜÈHØYÜÙ][™ÜÊ -B‚ˆYˆÛÙ^ œÝXœØÜš\[Û—Û[Ù[ -Ù][™ÜË›K›[Ù[ -N‚ˆYˆ›ÝÛÙ^ š\ר]][�XØ]Y +import logging +import shutil +import sys -N‚ˆÛÛœÛÛKœš[� -ˆˆ–Ü™YTÕ’VÓO^ÜÙ][™ÜË›K›[Ù[H\Ù\È[Ý\ˆÚ]ÔÝXœØÜš\[Û‹‚ˆ˜�][ÝIÜ™H›ÝÚYÛ™Y[‹–Ë×H�[ˆØÞX[—\Ýš^]]ÙÚ[ˆÚ]ÜË×Hš\œÝ ˆ‚ˆ -BˆÞ\Ë™^] - JBˆÙÙÙ\‹š[™›Ê‘[�š\›Û›Y[�ÒÈ -Ú]ÔÝXœØÜš\[ÛŠHŠBˆ™]\›‚‚ˆYˆ›ÝÙ][™ÜË›K›[Ù[‚ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œË˜\[™ -”Õ’VÓHŠB‚ˆYˆ›ÝÙ][™ÜË›K˜\WÚÙ^N‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ -“WÐTWÒÑVHŠB‚ˆYˆ›ÝÙ][™ÜË›K˜\Wؘ\ÙN‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ -“WÐTWÐ�TÑHŠB‚ˆYˆ›ÝÙ][™ÜËš[�Yܘ][ۜ˜\œ^]WØ\WÚÙ^N‚ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œË˜\[™ -”T”VUWÐTWÒÑVHŠB‚ˆYˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆ\œ›Ü—Ý^H^ +from rich.console import Console +from rich.panel import Panel +from rich.text import Text -Bˆ\œ›Ü—Ý^ ˜\[™ -“RTÔÒS‘È‘TURT‘QS•’T“Ó“QS•�T’PP“TÈ‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ -——ˆ‹Ý[OH�Ú]HŠB‚ˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ -ˆ¸ (ˆݘ\ŸH‹Ý[OH˜›ÛY[ÝÈŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆ\È›ÝÙ]ˆ‹Ý[OH�Ú]HŠB‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ -—“Ü[Û˜[[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH™[HÚ]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ -ˆ¸ (ˆݘ\ŸH‹Ý[OH™[HY[ÝÈŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆ\È›ÝÙ]ˆ‹Ý[OH™[HÚ]HŠB‚ˆ\œ›Ü—Ý^ ˜\[™ -—”™\]Z\™Y[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH�Ú]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×Ü™\]Z\™Yݘ\œÎ‚ˆYˆ˜\ˆOH”Õ’VÓHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ -¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -”Õ’VÓH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆˆ H[Ù[˜[YHÈ\ÙH -K™Ë‹ ÛÜ[˜ZKÙÜ MK� È܈‚ˆ‰Ø[�›ÜXËØÛ]YK[Ü\ËM MÉÊWˆ‹ˆÝ[OH�Ú]H‹ˆ -B‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ\œ›Ü—Ý^ ˜\[™ -—“Ü[Û˜[[�š\›Û›Y[�˜\šXX›\Ηˆ‹Ý[OH�Ú]HŠBˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆYˆ˜\ˆOH“WÐTWÐ�TÑHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ -¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -“WÐTWÐ�TÑH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆˆ HÝ\ÝÛHTH˜\ÙHT“Yˆ\Ú[™ÈØØ[[Ù[È -K™Ë‹Û[XKTÝY[ÊWˆ‹ˆÝ[OH�Ú]H‹ˆ -Bˆ[Yˆ˜\ˆOH”T”VUWÐTWÒÑVHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ -¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -”T”VUWÐTWÒÑVH‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆˆ HTHÙ^H›Üˆ\œ^]HRHÙXˆÙX\˜Ú -[˜X›\È™X[ ][YH™\ÙX\˜Ú -Wˆ‹ˆÝ[OH�Ú]H‹ˆ -Bˆ[Yˆ˜\ˆOH”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•Ž‚ˆ\œ›Ü—Ý^ ˜\[™ -¸ (ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹Ý[OH˜›ÛÞX[ˆŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆˆ H™X\ÛÛš[™ÈY™›Ü�]™[ˆ›Û™KZ[š[X[ ÝËYY][KYÚ YÚ ‚ˆ›X^ -Y˜][ˆYÚ -Wˆ‹ˆÝ[OH�Ú]H‹ˆ -B‚ˆ\œ›Ü—Ý^ ˜\[™ -—‘^[\HÙ]\—ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -™^Ü�Õ’VÓOIÛÜ[˜ZKÙÜ MK� ׈‹Ý[OH™[HÚ]HŠB‚ˆYˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆ›Üˆ˜\ˆ[ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÎ‚ˆYˆ˜\ˆOH“WÐTWÐ�TÑHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ -ˆ™^Ü�WÐTWÐ�TÑOIÚ‹ËÛØØ[ÜÝŒLM Í È‚ˆˆÈ™YYY›ÜˆØØ[[Ù[ÈÛ›Wˆ‹ˆÝ[OH™[HÚ]H‹ˆ -Bˆ[Yˆ˜\ˆOH”T”VUWÐTWÒÑVHŽ‚ˆ\œ›Ü—Ý^ ˜\[™ -ˆ™^Ü�T”VUWÐTWÒÑVOIÞ[Ý\‹\\œ^]KZÙ^KZ\™I׈‹Ý[OH™[HÚ]H‚ˆ -Bˆ[Yˆ˜\ˆOH”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•Ž‚ˆ\œ›Ü—Ý^ ˜\[™ -ˆ™^Ü�Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•IÚYÚ ×ˆ‹ˆÝ[OH™[HÚ]H‹ˆ -B‚ˆ[™[H[™[ -ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ -B‚ˆÙÙÙ\‹™X�YÊ“Z\ÜÚ[™È™\]Z\™Y[�ˆ˜\œÎˆ \È‹Z\ÜÚ[™×Ü™\]Z\™Yݘ\œÊBˆÛÛœÛÛKœš[� -—ˆŠBˆÛÛœÛÛKœš[� -[™[ -BˆÛÛœÛÛKœš[� +from strix.config import codex, load_settings +from strix.interface.utils import ( + check_docker_connection, + image_exists, + process_pull_line, +) -BˆÞ\Ë™^] - JBˆÙÙÙ\‹š[™›Êˆ‘[�š\›Û›Y[�ÒÈ -Ü[Û˜[Z\ÜÚ[™Îˆ \ÊH‹ˆZ\ÜÚ[™×ÛÜ[Û˜[ݘ\œÈ܈››Û™H‹ˆ -B‚‚™YˆÚXÚ×ÙØÚÙ\—Ú[œÝ[Y -H Oˆ›Û™N‚ˆYˆÚ][ �ÚXÚ -™ØÚÙ\ˆŠH\țۙN‚ˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆÓH›Ý›Ý[™[ˆUŠBˆÛÛœÛÛHHÛÛœÛÛJ -Bˆ\œ›Ü—Ý^H^ +logger = logging.getLogger(__name__) -Bˆ\œ›Ü—Ý^ ˜\[™ -‘ÐÒÑTˆ“ÕS”ÕSQ‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ -——ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -•H ÙØÚÙ\‰ÈÓHØ\țݛÝ[™[ˆ[Ý\ˆU —ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆ”X\ÙH[œÝ[ØÚÙ\ˆ[™[œÝ\™HH ÙØÚÙ\‰ÈÛÛ[X[™\È]˜Z[X›K——ˆ‹Ý[OH�Ú]H‚ˆ -B‚ˆ[™[H[™[ -ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ -BˆÛÛœÛÛKœš[� -—ˆ‹[™[ —ˆŠBˆÞ\Ë™^] - JBˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆÓH™\Ù[�ŠB‚‚™Yˆ[ÙØÚÙ\—Ú[XYÙJ -H Oˆ›Û™N‚ˆœ›ÛHØÚÙ\‹™\œ›ÜœÈ[\Ü�ØÚÙ\‘^Ù\[Û‚‚ˆÛÛœÛÛHHÛÛœÛÛJ -BˆÛY[�HÚXÚ×ÙØÚÙ\—ØÛÛ›™XÝ[ÛŠ -B‚ˆ�[�[YHHØYÜÙ][™ÜÊ -Kœ�[�[YBˆ[XYÙHH�[�[YKš[XYÙB‚ˆYˆ[XYÙWÙ^\ÝÊÛY[� [XYÙJN‚ˆÙÙÙ\‹™X�YÊ‘ØÚÙ\ˆ[XYÙH[™XYH™\Ù[�ØØ[Nˆ \È‹[XYÙJBˆ™]\›‚‚ˆYˆ�[�[YKš[XYÙWÜ[ÜÛXÞHOH›™]™\ˆŽ‚ˆ˜Z\ÙH�[�[YQ\œ›ÜŠˆˆ‘ØÚÙ\ˆ[XYÙHÚ[XYÙH\ŸH\È›Ý]˜Z[X›HØØ[H[™‚ˆ”Õ’VÒSPQÑWÔSÔÓPÖO[™]™\ˆ›Ü˜šYÈ[[™È]‚ˆ -B‚ˆÙÙÙ\‹š[™›Ê”[[™ÈØÚÙ\ˆ[XYÙNˆ \È‹[XYÙJBˆÛÛœÛÛKœš[� -BˆÛÛœÛÛKœš[� -ˆ–Ù[WT[[™È[XYÙVË×HÚ[XYÙ_HŠBˆÛÛœÛÛKœš[� -–Ù[HY[Ý×U\ÈÛ›H\[œÈÛˆš\œÝ�[ˆ[™X^HZÙHH™]ÈZ[�]\Ë‹‹–Ë×HŠBˆÛÛœÛÛKœš[� +def validate_environment() -> None: + logger.info("Validating environment") + console = Console() + missing_required_vars = [] + missing_optional_vars = [] -B‚ˆÚ]ÛÛœÛÛKœÝ]\Ê–Ø›ÛÞX[—QÝÛ›ØY[™È[XYÙH^Y\œË‹‹ˆ‹Ü[›™\�H™ÝÈŠH\ÈÝ]\΂ˆžN‚ˆ^Y\œ×Ú[™›ÎˆXÝÜ݋ݗHHßBˆ\ÝÝ\]HHˆ‚‚ˆ›Üˆ[™H[ˆÛY[� ˜\Kœ[ -[XYÙKÝ™X[OU�YKXÛÙOU�YJN‚ˆ\ÝÝ\]HH›ØÙ\Ü×Ü[Û[™J[™K^Y\œ×Ú[™›ËÝ]\Ë\ÝÝ\]JB‚ˆ^Ù\ØÚÙ\‘^Ù\[Ûˆ\ÈN‚ˆÙÙÙ\‹™X�YÊ‘˜Z[YÈ[ØÚÙ\ˆ[XYÙH \È‹[XYÙK^×Ú[™›ÏU�YJBˆÛÛœÛÛKœš[� + settings = load_settings() -Bˆ\œ›Ü—Ý^H^ + if codex.subscription_model(settings.llm.model): + if not codex.is_authenticated(): + console.print( + f"[red]STRIX_LLM={settings.llm.model} uses your ChatGPT subscription, " + "but you're not signed in.[/] Run [cyan]strix auth login chatgpt[/] first." + ) + sys.exit(1) + logger.info("Environment OK (ChatGPT subscription)") + return -Bˆ\œ›Ü—Ý^ ˜\[™ -‘�RSQÈSSPQÑH‹Ý[OH˜›Û™YŠBˆ\œ›Ü—Ý^ ˜\[™ -——ˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -ˆ�ÛÝ[›ÝÝÛ›ØYˆÚ[XYÙ_Wˆ‹Ý[OH�Ú]HŠBˆ\œ›Ü—Ý^ ˜\[™ -ÝŠJKÝ[OH™[H™YŠB‚ˆ[™[H[™[ -ˆ\œ›Ü—Ý^ ˆ]OH–Ø›ÛÚ]WTÕ’V‹ˆ]WØ[YÛ�H›Y�‹ˆ›Ü™\—ÜÝ[OHœ™Y‹ˆY[™ÏJ K ŠKˆ -BˆÛÛœÛÛKœš[� -[™[ —ˆŠBˆÞ\Ë™^] - JB‚ˆÙÙÙ\‹š[™›Ê‘ØÚÙ\ˆ[XYÙH \È™XYH‹[XYÙJBˆÝXØÙ\Ü×Ý^H^ + if not settings.llm.model: + missing_required_vars.append("STRIX_LLM") -BˆÝXØÙ\Ü×Ý^ ˜\[™ -‘ØÚÙ\ˆ[XYÙH™XYH‹Ý[OHˆÌŒ˜ÍMYHŠBˆÛÛœÛÛKœš[� -ÝXØÙ\Ü×Ý^ -BˆÛÛœÛÛKœš[� + if not settings.llm.api_key: + missing_optional_vars.append("LLM_API_KEY") -B \ No newline at end of file + if not settings.llm.api_base: + missing_optional_vars.append("LLM_API_BASE") + + if not settings.integrations.perplexity_api_key: + missing_optional_vars.append("PERPLEXITY_API_KEY") + + if missing_required_vars: + error_text = Text() + error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red") + error_text.append("\n\n", style="white") + + for var in missing_required_vars: + error_text.append(f"• {var}", style="bold yellow") + error_text.append(" is not set\n", style="white") + + if missing_optional_vars: + error_text.append("\nOptional environment variables:\n", style="dim white") + for var in missing_optional_vars: + error_text.append(f"• {var}", style="dim yellow") + error_text.append(" is not set\n", style="dim white") + + error_text.append("\nRequired environment variables:\n", style="white") + for var in missing_required_vars: + if var == "STRIX_LLM": + error_text.append("• ", style="white") + error_text.append("STRIX_LLM", style="bold cyan") + error_text.append( + " - Model name to use (e.g., 'openai/gpt-5.4' or " + "'anthropic/claude-opus-4-7')\n", + style="white", + ) + + if missing_optional_vars: + error_text.append("\nOptional environment variables:\n", style="white") + for var in missing_optional_vars: + if var == "LLM_API_BASE": + error_text.append("• ", style="white") + error_text.append("LLM_API_BASE", style="bold cyan") + error_text.append( + " - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n", + style="white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append("• ", style="white") + error_text.append("PERPLEXITY_API_KEY", style="bold cyan") + error_text.append( + " - API key for Perplexity AI web search (enables real-time research)\n", + style="white", + ) + elif var == "STRIX_REASONING_EFFORT": + error_text.append("• ", style="white") + error_text.append("STRIX_REASONING_EFFORT", style="bold cyan") + error_text.append( + " - Reasoning effort level: none, minimal, low, medium, high, xhigh, " + "max (default: high)\n", + style="white", + ) + + error_text.append("\nExample setup:\n", style="white") + error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white") + + if missing_optional_vars: + for var in missing_optional_vars: + if var == "LLM_API_BASE": + error_text.append( + "export LLM_API_BASE='http://localhost:11434' " + "# needed for local models only\n", + style="dim white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append( + "export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white" + ) + elif var == "STRIX_REASONING_EFFORT": + error_text.append( + "export STRIX_REASONING_EFFORT='high'\n", + style="dim white", + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + + logger.debug("Missing required env vars: %s", missing_required_vars) + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + logger.info( + "Environment OK (optional missing: %s)", + missing_optional_vars or "none", + ) + + +def check_docker_installed() -> None: + if shutil.which("docker") is None: + logger.debug("Docker CLI not found in PATH") + console = Console() + error_text = Text() + error_text.append("DOCKER NOT INSTALLED", style="bold red") + error_text.append("\n\n", style="white") + error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white") + error_text.append( + "Please install Docker and ensure the 'docker' command is available.\n\n", style="white" + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n", panel, "\n") + sys.exit(1) + logger.debug("Docker CLI present") + + +def pull_docker_image() -> None: + from docker.errors import DockerException + + console = Console() + client = check_docker_connection() + + runtime = load_settings().runtime + image = runtime.image + + if image_exists(client, image): + logger.debug("Docker image already present locally: %s", image) + return + + if runtime.image_pull_policy == "never": + raise RuntimeError( + f"Docker image {image!r} is not available locally and " + "STRIX_IMAGE_PULL_POLICY=never forbids pulling it" + ) + + logger.info("Pulling docker image: %s", image) + console.print() + console.print(f"[dim]Pulling image[/] {image}") + console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]") + console.print() + + with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status: + try: + layers_info: dict[str, str] = {} + last_update = "" + + for line in client.api.pull(image, stream=True, decode=True): + last_update = process_pull_line(line, layers_info, status, last_update) + + except DockerException as e: + logger.debug("Failed to pull docker image %s", image, exc_info=True) + console.print() + error_text = Text() + error_text.append("FAILED TO PULL IMAGE", style="bold red") + error_text.append("\n\n", style="white") + error_text.append(f"Could not download: {image}\n", style="white") + error_text.append(str(e), style="dim red") + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print(panel, "\n") + sys.exit(1) + + logger.info("Docker image %s ready", image) + success_text = Text() + success_text.append("Docker image ready", style="#22c55e") + console.print(success_text) + console.print() diff --git a/strix/runtime/backends.py b/strix/runtime/backends.py index 5398ed8b..7bc9b9e2 100644 --- a/strix/runtime/backends.py +++ b/strix/runtime/backends.py @@ -1,107 +1,31 @@ -­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker).""" +­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ø[™›Þ˜XÚÙ[™™YÚ\ÝžH8 %Ù[XÝYšXHÕ’VÔ•S•SQWÐ�PÒÑS‘ +Y˜][ˆØÚÙ\ŠKˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�ÙÙÚ[™Â™œ›ÛHÛÛXÝ[ۜ˘X˜È[\Ü�]ØZ]X›KØ[X›B™œ›ÛH\[™È[\Ü�TWÐÒPÒÒS‘Ë[žB‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛHYÙ[�ËœØ[™›Þ ›X[šY™\Ý[\Ü�X[šY™\Ý‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚”Ø[™›Þ˜XÚÙ[™HØ[X›VË‹‹‹]ØZ]X›VÝ\VÐ[žK[žWWWB‚‚˜\Þ[˜ÈYˆÙØÚÙ\—ؘXÚÙ[™ +ˆ +‹ˆ[XYÙNˆÝ‹ˆX[šY™\݈X[šY™\Ý ˆ^ÜÙYÜÜ�Έ\VÚ[�  ‹‹—Kˆš[™Û[Ý[�Έ\ÝÙXÝÜÝ‹[žWWH›Û™HH›Û™KŠH Oˆ\VÐ[žK[žWN‚ˆˆˆ�œš[™È\HÙ\ÜÚ[Ûˆ˜XÚÙYžHHØØ[ØÚÙ\ˆY[[Û‹‚‚ˆ\Ù\ȘÛ\ÜΘÝš^ØÚÙ\”Ø[™›ÞÛY[�È[š™XÝ‘UÐQRSˆ ˆ‘UÔ�UÈØ\È +ÈÜÝ ™ØÚÙ\‹š[�\›˜[ÜÝ YØ]]Ø^Kˆ[\Ü�ˆØÚÙ\˜^š[HÛÈ\Þ[Y[�È]\™Ù]H›Û‹QØÚÙ\‚ˆ˜XÚÙ[™Û‰Ý™YYHØÚÙ\‹\HXœ˜\žH[œÝ[Y ‚‚ˆÙ\ÜÚ[Û‹œÝ\� -from __future__ import annotations +X\ÈÚ]X]\šX[^™\ÈHX[šY™\Ý[�ÈH�[›š[™ÂˆÛÛ�Z[™\ˆ8 %HÑÉÜÈÛY[� ˜Ü™X]J +XÛ›H�Z[ÈH[›™\ˆÙ\ÜÚ[Û‚ˆØš™XÝÚ]Ý]\Z[™È] ˆ\Þ[˜ÈÚ]Ù\ÜÚ[ÛŽ˜ÛÝ[Ø[]ÛË�]ˆÝš^X[˜YÙ\ÈÙ\ÜÚ[ÛˆY™][YH^XÚ]HšXHÛY[� ™[]J +XÛÈÙBˆšYÙÙ\ˆÝ\� -import logging -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any +XÝ\œÙ[™\Ë‚ˆˆˆ‚ˆ[\Ü�ØÚÙ\‚ˆœ›ÛHYÙ[�ËœØ[™›Þ œØ[™›Þ\Ë™ØÚÙ\ˆ[\Ü�ØÚÙ\”Ø[™›ÞÛY[�Ü[ۜ‚ˆœ›ÛHÝš^ ˜ÛÛ™šYË›ØY\ˆ[\Ü�ØYÜÙ][™Üˆœ›ÛHÝš^ œ�[�[YK™ØÚÙ\—ØÛY[�[\Ü�Ýš^ØÚÙ\”Ø[™›ÞÛY[�‚ˆÛY[�HÝš^ØÚÙ\”Ø[™›ÞÛY[� +ØÚÙ\‹™œ›ÛWÙ[�Š +JBˆÛY[� œÝš^Øš[™Û[Ý[�ÈHš[™Û[Ý[�È܈×BˆÛY[� š[XYÙWÜ[ÜÛXÞHHØYÜÙ][™ÜÊ +Kœ�[�[YKš[XYÙWÜ[ÜÛXÞBˆÜ[ÛœÈHØÚÙ\”Ø[™›ÞÛY[�Ü[ÛœÊ[XYÙOZ[XYÙK^ÜÙYÜÜ�ÏY^ÜÙYÜÜ�ÊBˆÙ\ÜÚ[ÛˆH]ØZ]ÛY[� ˜Ü™X]JÜ[ÛœÏ[Ü[ÛœËX[šY™\Ý[X[šY™\Ý +Bˆ]ØZ]Ù\ÜÚ[Û‹œÝ\� - -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) +Bˆ™]\›ˆÛY[� Ù\ÜÚ[Û‚‚‚—Ð�PÒÑS‘ΈXÝÜÝ‹Ø[™›Þ˜XÚÙ[™HHˆ™ØÚÙ\ˆŽˆÙØÚÙ\—ؘXÚÙ[™ ŸB‚—Ð’S‘ÓSÕS•Ð�PÒÑS‘ΈÙ]ÜÝ—HHÈ™ØÚÙ\ˆŸB‚‚™YˆÙ]ؘXÚÙ[™ +˜[YNˆÝŠH OˆØ[™›Þ˜XÚÙ[™‚ˆˆˆ”™]\›ˆH˜XÚÙ[™˜XÝÜžH›Üˆ˜[YX܈˜Z\ÙK‚‚ˆ\™Ü΂ˆ˜[YNˆ˜XÚÙ[™Y[�YšY\ˆ +K™Ëˆ™ØÚÙ\ˆ˜ +KˆX]Ú\È^X݈›È˜[˜XÚˈ[šÛ›ÝÛˆ˜[Y\ȘZ\ÙHÛÈÛÛ™šYÈ\ÜÈÝ\™˜XÙBˆ[[YYX][H[œÝXYÙˆÚ[[�HXÚÚ[™ÈHY˜][ ‚ˆˆˆ‚ˆ˜XÚÙ[™HÐ�PÒÑS‘Ë™Ù] +˜[YJBˆYˆ˜XÚÙ[™\țۙN‚ˆÝ\Ü�YH‹‹š›Ú[ŠÛÜ�Y +Ð�PÒÑS‘ÊJBˆ˜Z\ÙH˜[YQ\œ›ÜŠˆˆ•[šÛ›ÝÛˆÕ’VÔ•S•SQWÐ�PÒÑS‘ˆÛ˜[YH\ŸH +Ý\Ü�YˆÜÝ\Ü�YJH‹ˆ +BˆÙÙÙ\‹™X�YÊ”Ù[XÝYØ[™›Þ˜XÚÙ[™ˆ \È‹˜[YJBˆ™]\›ˆ˜XÚÙ[™‚‚™Yˆ™YÚ\Ý\—ؘXÚÙ[™ +ˆ˜[YNˆÝ‹ˆ˜XÚÙ[™ˆØ[™›Þ˜XÚÙ[™ ˆ +‹ˆÝ\Ü�רš[™Û[Ý[�Έ›ÛÛH˜[ÙKŠH Oˆ›Û™N‚ˆˆˆ”™YÚ\Ý\ˆHÝ\ÝÛH˜XÚÙ[™[™\ˆ˜[YX ‚‚ˆ[�[™Y›ÜˆÝۜݙX[H\Ù\œÈÚÈÚ\Z\ˆÝÛˆ�[�[YH8 %™YÚ\Ý\‚ˆ™Y›Ü™H[žHÙ\ÜÚ[Û—ÛX[˜YÙ\‹˜Ü™X]WÛÜ—Ü™]\ÙXØ[ ˆ™K\™YÚ\Ý\š[™Âˆ[ˆ^\Ý[™È˜[YHÝ™\�Üš]\ÈHš[܈[�žKˆÝ\Ü�רš[™Û[Ý[�؈Y˜][ÈȘ[ÙNˆH™[[ÝH�[�[YHØ[››ÝÙYHHØ[\‰ÜÈš[\Þ\Ý[KÛˆ]\È[™YØØ[ÛÝ\˜Ù\È\ÈX[šY™\Ý[�šY\ÈÈ\ØY[œÝXY ‚ˆˆˆ‚ˆÐ�PÒÑS‘ÖÛ˜[YWHH˜XÚÙ[™ˆYˆÝ\Ü�רš[™Û[Ý[�΂ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘˘Y +˜[YJBˆ[ÙN‚ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘Ë™\ØØ\™ +˜[YJBˆÙÙÙ\‹š[™›Ê”™YÚ\Ý\™YØ[™›Þ˜XÚÙ[™ˆ \È +š[™[Ý[�Έ \ÊH‹˜[YKÝ\Ü�רš[™Û[Ý[�ÊB‚‚™Yˆ˜XÚÙ[™ÜÝ\Ü�רš[™Û[Ý[�ʘ[YNˆÝŠH Oˆ›ÛÛ‚ˆ™]\›ˆ˜[YH[ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘‚‚™YˆÝ\Ü�YؘXÚÙ[™Ê +H Oˆ\ÝÜÝ—N‚ˆ™]\›ˆÛÜ�Y +Ð�PÒÑS‘ÊB \ No newline at end of file diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py index c70f2d2d..fc3a56e5 100644 --- a/strix/runtime/docker_client.py +++ b/strix/runtime/docker_client.py @@ -1,114 +1,307 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ýš^ØÚÙ\”Ø[™›ÞÛY[�8 %™\Ù\�™\ÈH[XYÙIÜÈS•–TÒS•[™Y“‘UÐQRS‹Ó‘UÔ�UÈØ\Xš[]Y\È -ÈÜÝ YØ]]Ø^K‚‚•HÑÉÜÈØÚÙ\”Ø[™›ÞÛY[� —ØÜ™X]WØÛÛ�Z[™\˜Ù\È›Ý^ÜÙHHÛÚț܂™^[™[™ÈÜ™X]WÚÝØ\™ÜØ™Y›Ü™HÛÛ�Z[™\œË˜Ü™X]X\ÈØ[Y ˆÙHÝX˜Û\ܘ[™™Z[\[Y[�HY]Ù›ÙH™\˜˜][Hœ›ÛHHÑÈÛÝ\˜ÙKÚ]™YB™[\΂‚ŒKˆ›ÜHÑÉÜÈ[�ž\Ú[�VÈ�Z[—XÝ™\œšYNÈÝ\HÈ�Z[‹‹Yˆ‹ˆ‹Ù]‹Û�[—X\ÈÛÛ[X[™[œÝXY ˆ\È]ÈÝ\ˆ[XYÙI܈ØÚÙ\‹Y[�ž\Ú[� œÚXÝX[H�[ˆ8 %Ú]Ý]] ØZYËXÛX™]™\‚ˆÝ\�È[œÚYHHÛÛ�Z[™\ˆ[™›ÛÝݘ\ØØZYØ™]šY\ÈYØZ[œÝBˆXYÜ� ‚Œ‹ˆ\[™‘UÐQRS‹Ó‘UÔ�UÈÈØ\ØY -™\]Z\™YžH›X\ \ÔØ[™ˆÝ\ˆ˜]Ë\ÛØÚÙ]ÛÛÊK‚ŒËˆYÜÝ ™ØÚÙ\‹š[�\›˜[8¡¤ˆÜÝ YØ]]Ø^HÈ^˜WÚÜÝØÛÈBˆYÙ[�Ø[ˆ™XXÚÜÝ \Ù\�™Y\Ë‚‚”[›™YÈÜ[˜ZKXYÙ[�ÏOL ŒM �˜ ˆ�[\[™ÈHÑÈ™\]Z\™\œ™K[Y\™Ú[™ÈH\™[�›ÙKˆ˜XÚÈ\Ý™X[H›Üˆ[ˆ[š™XÝ[ÛˆÛÚË‚ˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�ÛÛ�^X‚š[\Ü�ÙÙÚ[™Âš[\Ü�Üš[\Ü�]ZY™œ›ÛH\[™È[\Ü�[žK]\˜[ Ø\Ý‚™œ›ÛHYÙ[�ËœØ[™›Þ ™\œ›ÜœÈ[\Ü�^ÜÙYÜ�[˜]˜Z[X›Q\œ›Ü‚™œ›ÛHYÙ[�ËœØ[™›Þ ›X[šY™\Ý[\Ü�X[šY™\Ý™œ›ÛHYÙ[�ËœØ[™›Þ œØ[™›Þ\Ë™ØÚÙ\ˆ[\Ü� -ˆØÚÙ\”Ø[™›ÞÛY[� ˆØÚÙ\”Ø[™›ÞÙ\ÜÚ[Û‹ˆØ�Z[ÙØÚÙ\—Ý›Û[YWÛ[Ý[�ËˆÙØÚÙ\—ÜÜ�ÚÙ^KˆÛX[šY™\ÝÜ™\]Z\™\×Ù�\ÙKˆÛX[šY™\ÝÜ™\]Z\™\×ÜÞ\רYZ[‹ŠB™œ›ÛHYÙ[�ËœØ[™›Þ œÙ\ÜÚ[Û‹œØ[™›ÞÜÙ\ÜÚ[Ûˆ[\Ü�Ø[™›ÞÙ\ÜÚ[Û‚™œ›ÛHYÙ[�ËœØ[™›Þ �\\È[\Ü�^ÜÙYÜ�[™Ú[�™œ›ÛHØÚÙ\ˆ[\Ü�\œ›ÜœÈ\ÈØÚÙ\—Ù\œ›ÜœÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹›[Ù[˘ÛÛ�Z[™\œÈ[\Ü�ÛÛ�Z[™\ˆÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�\\È[\Ü�ÙÐÛÛ™šYÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�\\È[\Ü�[Ý[�\ÈØÚÙ\”ÑÓ[Ý[�È\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛHØÚÙ\‹�][È[\Ü�\œÙWÜ™\ÜÚ]ÜžWÝYÈÈ\NˆYۛܙVÚ[\Ü� ][�\Y [�\ÙY ZYۛܙWB™œ›ÛH™\]Y\ÝË™^Ù\[ÛœÈ[\Ü�™\]Y\Ý^Ù\[Û‚‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚—ÔÐS‘“ÖÓ‘UÓÔ’×ÑS•ˆH”Õ’VÑÐÒÑT—ÔÐS‘“ÖÓ‘UÓÔ’È‚‚‚™YˆÙ[œÝ\™WÚ[XYÙWØ]˜Z[X›JˆÛY[�ˆ[žKˆ[XYÙNˆÝ‹ˆ[ÜÛXÞNˆ]\˜[Ș]]È‹›™]™\ˆ—KŠH Oˆ›Û™N‚ˆYˆÛY[� š[XYÙWÙ^\ÝÊ[XYÙJN‚ˆ™]\›‚ˆYˆ[ÜÛXÞHOH›™]™\ˆŽ‚ˆ˜Z\ÙH�[�[YQ\œ›ÜŠˆˆ‘ØÚÙ\ˆ[XYÙHÚ[XYÙH\ŸH\È›Ý]˜Z[X›HØØ[H[™‚ˆ”Õ’VÒSPQÑWÔSÔÓPÖO[™]™\ˆ›Ü˜šYÈ[[™È]‚ˆ -Bˆ™\ËYÈH\œÙWÜ™\ÜÚ]ÜžWÝYÊ[XYÙJBˆÛY[� ™ØÚÙ\—ØÛY[� š[XYÙ\Ëœ[ -™\ËYÏ]YÈ܈›Û™K[ÝYÜÏQ˜[ÙJB‚‚™YˆÜØ[™›ÞÛ™]ÛÜšÊ -H OˆÝˆ›Û™N‚ˆ˜[YHHÜË™[�š\›Û‹™Ù] -ÔÐS‘“ÖÓ‘UÓÔ’×ÑS•‹ˆŠKœÝš\ +­r‡^Ñf¥–ئ{O,yÊ'vî¶›­"""StrixDockerSandboxClient — preserves the image's ENTRYPOINT and adds +NET_ADMIN/NET_RAW capabilities + host-gateway. -Bˆ™]\›ˆ˜[YH܈›Û™B‚‚™YˆØ\WÜØ[™›ÞÛ™]ÛÜšÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆ™]ÛÜšÈHÜØ[™›ÞÛ™]ÛÜšÊ -BˆYˆ™]Ûܚ΂ˆÜ™X]WÚÝØ\™ÜÖÈ›™]ÛܚȗHH™]ÛܚˆÜ™X]WÚÝØ\™ÜËœÜ -œÜ�È‹›Û™JB‚‚™YˆØ\WÜ™\ÛÝ\˜ÙWÛ[Z]ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆˆˆ�\HÜ[Û˜[ÙÜ›Ý\™\ÛÝ\˜ÙHØ\Èœ›ÛHH[�š\›Û›Y[� ˆ[œÙ] Ø›[šÂˆ˜[Y\ÈX]™HØÚÙ\‰ÜÈY˜][ -[˜›Ý[™Y -KÛÈ\È\ÈÜ Z[ˆ\ˆÜÝ ˆˆˆ‚ˆY[WÛ[Z]HÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÓQSWÓSRU‹ˆŠKœÝš\ +The SDK's ``DockerSandboxClient._create_container`` does not expose a hook for +extending ``create_kwargs`` before ``containers.create`` is called. We subclass +and reimplement the method body verbatim from the SDK source, with three +deltas: -BˆYˆY[WÛ[Z]‚ˆÜ™X]WÚÝØ\™ÜÖÈ›Y[WÛ[Z]—HHY[WÛ[Z]‚ˆÚWÜÚ^™HHÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÔÒWÔÒV‘H‹ˆŠKœÝš\ +1. Drop the SDK's ``entrypoint=["tail"]`` override; supply ``["tail", "-f", + "/dev/null"]`` as ``command`` instead. This lets our image's + ``docker-entrypoint.sh`` actually run — without it, ``caido-cli`` never + starts inside the container and ``bootstrap_caido`` retries against a + dead port. +2. Append NET_ADMIN/NET_RAW to ``cap_add`` (required by ``nmap -sS`` and + other raw-socket tools). +3. Add ``host.docker.internal`` → host-gateway to ``extra_hosts`` so the + agent can reach host-served apps. -BˆYˆÚWÜÚ^™N‚ˆÜ™X]WÚÝØ\™ÜÖÈœÚWÜÚ^™H—HHÚWÜÚ^™B‚ˆÜ\ÈHÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÐÔTÈ‹ˆŠKœÝš\ +Pinned to ``openai-agents==0.14.6``. Bumping the SDK requires +re-merging the parent body. Track upstream for an injection hook. +""" -BˆYˆÜ\΂ˆÚ]ÛÛ�^X‹œÝ\™\Üʘ[YQ\œ›Ü‹Ý™\™›ÝÑ\œ›ÜŠN‚ˆ˜[›×ØÜ\ÈH[� -›Ø] -Ü\ÊH -ˆ WÌ Ì Ì -BˆYˆ ˜[›×ØÜ\ÈH ŠŠ�ŒÈ H N‚ˆÜ™X]WÚÝØ\™ÜÖÈ›˜[›×ØÜ\È—HH˜[›×ØÜ\‚ˆY×Û[Z]HÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÔQ×ÓSRU‹ˆŠKœÝš\ +from __future__ import annotations -BˆYˆY×Û[Z]‚ˆÚ]ÛÛ�^X‹œÝ\™\Üʘ[YQ\œ›ÜŠN‚ˆÜ™X]WÚÝØ\™ÜÖÈœY×Û[Z]—HH[� -Y×Û[Z] -B‚‚™YˆØ\WÛÙ×Û[Z]ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆˆˆ�›Ý[™HÛÛ�Z[™\‰ÜÈœÛÛ‹Yš[HÙÈÛÈH�[˜]Ø^H›ØÙ\ÜÈ[ˆHØ[™›Þˆ -K™ËˆHÛÛ]�\ÞK[ÛÜÈÜš][™ÈÈÝÝ] -HØ[››Ýš[HÜÝ\Úˆ[™ZÙHHØÚÙ\ˆY[[ÛˆÝÛˆÚ]] ‚‚ˆ[›ZÙHHÙÜ›Ý\Ø\ÈX›Ý™K\ÈY˜][È -Š›ÛŠŠˆ8 %ØÚÙ\‰ÜÈÝÛˆY˜][ˆ\È[ˆ[˜›Ý[™YœÛÛ‹Yš[KÚXÚ\È[œØY™H›Üˆ[ˆ]]Û›Û[Ý\ÈYÙ[�]ˆ^XÝ]\È\˜š]˜\žHÛÛ[X[™ˈX^ Yš[X›Ý][ÛˆYX[œÈHÛ‹Y\ÚÈØ\\ˆX^ \Ú^™H -ˆX^ Yš[X ˆÙ]Õ’VÔÐS‘“ÖÓÑ×ÓPVÔÒV‘XÈ  ØÙ™˜ˆÈܘXÚÈÝ]ÈØÚÙ\‰ÜÈY˜][ ˆˆˆ‚ˆX^ÜÚ^™HHÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÓÑ×ÓPVÔÒV‘H‹�LHŠKœÝš\ +import contextlib +import logging +import os +import uuid +from typing import Any, Literal, cast -BˆYˆX^ÜÚ^™K›ÝÙ\Š -H[ˆ -Œ‹›Ù™ˆ‹››Û™H‹�[›[Z]YŠN‚ˆ™]\›‚ˆX^Ùš[HHÜË™[�š\›Û‹™Ù] -”Õ’VÔÐS‘“ÖÓÑ×ÓPVÑ’SH‹ŒÈŠKœÝš\ +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 -H܈ŒÈ‚ˆÜ™X]WÚÝØ\™ÜÖÈ›ÙרÛÛ™šYÈ—HHÙÐÛÛ™šYʈ\OSÙÐÛÛ™šYË�\\Ë’”ÓÓ‹ˆÛÛ™šYÏ^È›X^ \Ú^™HŽˆX^ÜÚ^™K›X^ Yš[HŽˆX^Ùš[_Kˆ -B‚‚™YˆØ\WÜ�[—ÛX™[ÊÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWJH Oˆ›Û™N‚ˆ�[—ÚYHÜË™Ù][�ДՒVÔ•S—ÒQŠBˆYˆ›Ý�[—ÚY‚ˆ™]\›‚ˆX™[ÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ -›X™[È‹ßJBˆYˆ›Ý\Ú[œÝ[˜ÙJX™[ËXÝ -N‚ˆ™]\›‚ˆX™[ÖȜݚ^ \�[‹ZY—HH�[—ÚYˆ�[—Ý\HHÜË™Ù][�ДՒVÔ•S—ÕTHŠBˆYˆ�[—Ý\N‚ˆX™[ÖȜݚ^ \�[‹]\H—HH�[—Ý\B‚‚˜Û\ÜÈÝš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠN‚ˆØ[™›ÞÛ™]ÛܚΈÝˆHˆ‚‚ˆ\Þ[˜ÈYˆÜ™\ÛÛ™WÙ^ÜÙYÜÜ� -Ù[‹Ü�ˆ[� -H Oˆ^ÜÙYÜ�[™Ú[�‚ˆžN‚ˆÙ[‹—ØÛÛ�Z[™\‹œ™[ØY -Bˆ^Ù\ØÚÙ\—Ù\œ›ÜœË�TQ\œ›Üˆ\ÈN‚ˆ˜Z\ÙH^ÜÙYÜ�[˜]˜Z[X›Q\œ›ÜŠˆÜ�\Ü� ˆ^ÜÙYÜÜ�Ï\Ù[‹œÝ]K™^ÜÙYÜÜ�ˈ™X\ÛÛ�H˜˜XÚÙ[™Ý[˜]˜Z[X›H‹ˆÛÛ�^^ˆ˜˜XÚÙ[™Žˆ™ØÚÙ\ˆ‹ˆ™]Z[Žˆ˜ÛÛ�Z[™\—Ü™[ØYÙ˜Z[Y‹ˆ›™]ÛܚȎˆÙ[‹œØ[™›ÞÛ™]ÛܚˈKˆØ]\ÙOYKˆ -Hœ›ÛHB‚ˆ]œÈHÙ]]ŠÙ[‹—ØÛÛ�Z[™\‹˜]œÈ‹ßJH܈ßBˆ™]ÛÜšÜÈH]œË™Ù] -“™]ÛÜšÔÙ][™ÜÈ‹ßJK™Ù] -“™]ÛÜšÜÈ‹ßJBˆ[™Ú[�H™]ÛÜšÜË™Ù] -Ù[‹œØ[™›ÞÛ™]ÛÜšÊH܈ßBˆ\H[™Ú[� ™Ù] -’TY™\ÜÈŠH܈[™Ú[� ™Ù] -‘Ûؘ[T��Y™\ÜÈŠBˆYˆ›Ý\Ú[œÝ[˜ÙJ\ ÝŠH܈›Ý\‚ˆ˜Z\ÙH^ÜÙYÜ�[˜]˜Z[X›Q\œ›ÜŠˆÜ�\Ü� ˆ^ÜÙYÜÜ�Ï\Ù[‹œÝ]K™^ÜÙYÜÜ�ˈ™X\ÛÛ�H˜˜XÚÙ[™Ý[˜]˜Z[X›H‹ˆÛÛ�^^ˆ˜˜XÚÙ[™Žˆ™ØÚÙ\ˆ‹ˆ™]Z[Žˆ˜ÛÛ�Z[™\—Û›ÝÛÛ—Û™]Ûܚȋˆ›™]ÛܚȎˆÙ[‹œØ[™›ÞÛ™]ÛܚˈKˆ -BˆÜÝHˆ–ÞÚ\WHˆYˆŽˆˆ[ˆ\[ÙH\ˆ™]\›ˆ^ÜÙYÜ�[™Ú[� -ÜÝZÜÝ Ü�\Ü� ÏQ˜[ÙJB‚‚˜Û\ÜÈÝš^ØÚÙ\”Ø[™›ÞÛY[� -ØÚÙ\”Ø[™›ÞÛY[� -N‚ˆÈÜÝ\™XÝÜšY\ÈÈš[™ [[Ý[�[�ÈHÛÛ�Z[™\‹Ù]žHHØÚÙ\‚ˆÈ˜XÚÙ[™™Y›Ü™HÜ™X]J -X ˆXXÚ][H\ÈÜÛÝ\˜ÙK\™Ù] ™XYÛÛ›_X ‚ˆÝš^Øš[™Û[Ý[�Έ\ÝÙXÝÜÝ‹[žWWH›Û™HH›Û™Bˆ[XYÙWÜ[ÜÛXÞNˆ]\˜[Ș]]È‹›™]™\ˆ—HH˜]]È‚‚ˆ\Þ[˜ÈYˆØÜ™X]WØÛÛ�Z[™\ŠˆÙ[‹ˆ[XYÙNˆÝ‹ˆ -‹ˆX[šY™\݈X[šY™\ݛۙHH›Û™Kˆ^ÜÙYÜÜ�Έ\VÚ[�  ‹‹—HH +logger = logging.getLogger(__name__) -KˆÙ\ÜÚ[Û—ÚYˆ]ZY •URQ›Û™HH›Û™Kˆ -H OˆÛÛ�Z[™\Ž‚ˆÈ KKKKH‘QÒSˆ‘T��USHÓÔHÙˆØÚÙ\”Ø[™›ÞÛY[� —ØÜ™X]WØÛÛ�Z[™\ˆ KKKKBˆÈÑÈ™YŽˆÜ˜ËØYÙ[�ËÜØ[™›Þ ÜØ[™›Þ\ËÙØÚÙ\‹œNŒM Í LM ÍÈ -Œ ŒM �ŠK‚ˆÙ[œÝ\™WÚ[XYÙWØ]˜Z[X›JÙ[‹[XYÙKÙ[‹š[XYÙWÜ[ÜÛXÞJB‚ˆ\ÜÙ\�Ù[‹š[XYÙWÙ^\ÝÊ[XYÙJBˆ[�š\›Û›Y[�ˆXÝÜ݋ݗH›Û™HH›Û™BˆYˆX[šY™\Ý‚ˆ[�š\›Û›Y[�H]ØZ]X[šY™\Ý ™[�š\›Û›Y[� œ™\ÛÛ™J -BˆÈÝš^[Hœ›ÛHHÑÈ›ÙNˆ›Ü[�ž\Ú[�Ý™\œšYH[™ˆÈÝ\HZ[ Yˆ Ù]‹Û�[\ÈÛÛ[X[™ÛÈH[XYÙI܈ÈS•–TÒS• -ØÚÙ\‹Y[�ž\Ú[� œÚ -H�[œÈÙ]\ [ˆ^XˆÈ‰˜™XÛÛY\È^XÈZ[ Yˆ Ù]‹Û�[›ÜˆHÙY\ X[]™K‚ˆÈÚ]Ý]\ËØZYËXÛH -ÈH[‹XÛÛ�Z[™\ˆÐH�\Ý™]™\ˆÙ]ˆÈ[š]X[^™Y ‚ˆÜ™X]WÚÝØ\™ÜΈXÝÜÝ‹[žWHHˆš[XYÙHŽˆ[XYÙKˆ™]XÚŽˆ�YKˆ˜ÛÛ[X[™ŽˆÈ�Z[‹‹Yˆ‹‹Ù]‹Û�[—Kˆ™[�š\›Û›Y[�Žˆ[�š\›Û›Y[� ˆBˆYˆX[šY™\Ý\țݛۙN‚ˆØÚÙ\—Û[Ý[�ÈHØ�Z[ÙØÚÙ\—Ý›Û[YWÛ[Ý[�ʈX[šY™\Ý ˆÙ\ÜÚ[Û—ÚY\Ù\ÜÚ[Û—ÚY ˆ -BˆYˆØÚÙ\—Û[Ý[�΂ˆÜ™X]WÚÝØ\™ÜÖÈ›[Ý[�È—HHØÚÙ\—Û[Ý[�ˆYˆÛX[šY™\ÝÜ™\]Z\™\×Ù�\ÙJX[šY™\Ý -N‚ˆÜ™X]WÚÝØ\™ÜË�\]Jˆ]šXÙ\ÏVÈ‹Ù]‹Ù�\ÙH—KˆØ\ØYVÈ”ÖT×ÐQRSˆ—KˆÙXÝ\š]WÛÜVȘ\\›[ÜŽ�[˜ÛÛ™š[™Y—Kˆ -Bˆ[YˆÛX[šY™\ÝÜ™\]Z\™\×ÜÞ\רYZ[ŠX[šY™\Ý -N‚ˆÜ™X]WÚÝØ\™ÜË�\]JˆØ\ØYVÈ”ÖT×ÐQRSˆ—KˆÙXÝ\š]WÛÜVȘ\\›[ÜŽ�[˜ÛÛ™š[™Y—Kˆ -BˆYˆ^ÜÙYÜÜ�΂ˆÜ™X]WÚÝØ\™ÜÖÈœÜ�È—HHˆÙØÚÙ\—ÜÜ�ÚÙ^JÜ� -Nˆ -ŒL�ËŒ Œ ŒH‹›Û™JH›ÜˆÜ�[ˆ^ÜÙYÜÜ�ˆBˆÈ KKKKHS‘‘T��USHÓÔH KKKKB‚ˆÈÝš^[š™XÝ[ÛœÈ8 %\[™ Û‰ÝÝ™\�Üš]KÛÈ•TÑKÔÖT×ÐQRSˆÝ\�š]™\Ë‚ˆØ\ØYHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ -˜Ø\ØY‹×JBˆYˆ›Ý\Ú[œÝ[˜ÙJØ\ØY \Ý -N‚ˆØ\ØYH\Ý -Ø\ØY -BˆÜ™X]WÚÝØ\™ÜÖȘØ\ØY—HHØ\ØYˆ›ÜˆØ\[ˆ -“‘UÐQRSˆ‹“‘UÔ�UÈŠN‚ˆYˆØ\›Ý[ˆØ\ØY‚ˆØ\ØY ˜\[™ -Ø\ -B‚ˆ^˜WÚÜÝÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ -™^˜WÚÜÝÈ‹ßJBˆ^˜WÚÜÝÖÈšÜÝ ™ØÚÙ\‹š[�\›˜[—HHšÜÝ YØ]]Ø^H‚‚ˆØ\WÜØ[™›ÞÛ™]ÛÜšÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÜ™\ÛÝ\˜ÙWÛ[Z]ÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÛÙ×Û[Z]ÊÜ™X]WÚÝØ\™ÜÊBˆØ\WÜ�[—ÛX™[ÊÜ™X]WÚÝØ\™ÜÊB‚ˆÈÝš^[š™XÝ[ÛŽˆØØ[ÛÝ\˜ÙH™Y\ËÛÜ�YÚ[ÝÙ\Ý Yš\œÝÛÈBˆÈ™\ÝYÜXÈ[™ÈÛˆÜÙˆH™YH]ÛÝ™\œË‚ˆš[™Û[Ý[�ÈHÙ[‹œÝš^Øš[™Û[Ý[�È܈ -BˆYˆš[™Û[Ý[�΂ˆ[Ý[�ÈHÜ™X]WÚÝØ\™ÜËœÙ]Y˜][ -›[Ý[�È‹×JBˆ›ÜˆÜXÈ[ˆÛÜ�Y -š[™Û[Ý[�ËÙ^O[[X™HΈÝŠÖÈ�\™Ù]—JK˜ÛÝ[� -‹ÈŠJN‚ˆ[Ý[�˘\[™ -ˆØÚÙ\”ÑÓ[Ý[� -ˆ\™Ù]\ÜXÖÈ�\™Ù]—KˆÛÝ\˜ÙO\ÜXÖÈœÛÝ\˜ÙH—Kˆ\OH˜š[™‹ˆ™XYÛÛ›O\ÜXË™Ù] -œ™XYÛÛ›H‹˜[ÙJKˆ -Bˆ -B‚ˆÙÙÙ\‹™X�Yʈ�Ü™X][™ÈØ[™›ÞÛÛ�Z[™\Žˆ[XYÙOI\ÈØ\ÏI\È^ÜÙYÜÜ�ÏI\È‹ˆ[XYÙKˆØ\ØY ˆ\Ý -^ÜÙYÜÜ�ÊKˆ -BˆÛÛ�Z[™\ˆHÙ[‹™ØÚÙ\—ØÛY[� ˜ÛÛ�Z[™\œË˜Ü™X]J -Š˜Ü™X]WÚÝØ\™ÜÊBˆÙÙÙ\‹š[™›Êˆ”Ø[™›ÞÛÛ�Z[™\ˆÜ™X]YˆYI\È[XYÙOI\È‹ˆÛÛ�Z[™\‹œÚÜ�ÚYYˆ\Ø]ŠÛÛ�Z[™\‹œÚÜ�ÚYŠH[ÙH�È‹ˆ[XYÙKˆ -Bˆ™]\›ˆÛÛ�Z[™\‚‚ˆ\Þ[˜ÈYˆÜ™X]JÙ[‹ -ŠšÝØ\™ÜΈ[žJH OˆØ[™›ÞÙ\ÜÚ[ÛŽ‚ˆÙ\ÜÚ[ÛˆH]ØZ]Ý\\Š -K˜Ü™X]J -ŠšÝØ\™ÜÊBˆ™]ÛÜšÈHÜØ[™›ÞÛ™]ÛÜšÊ -Bˆ[›™\ˆHÙ\ÜÚ[Û‹—Ú[›™\‚ˆYˆ™]ÛÜšÈ[™\Ú[œÝ[˜ÙJ[›™\‹ØÚÙ\”Ø[™›ÞÙ\ÜÚ[ÛŠN‚ˆ[›™\‹—רÛ\Ü××ÈHÝš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[Û‚ˆØ\Ý -”Ýš^ØÚÙ\”Ø[™›ÞÙ\ÜÚ[Ûˆ‹[›™\ŠKœØ[™›ÞÛ™]ÛÜšÈH™]Ûܚˆ™]\›ˆÙ\ÜÚ[Û‚‚ˆ\Þ[˜ÈYˆ[]JÙ[‹Ù\ÜÚ[ÛŽˆØ[™›ÞÙ\ÜÚ[ÛŠH OˆØ[™›ÞÙ\ÜÚ[ÛŽ‚ˆÛÛ�Z[™\—ÚYHÙ]]ŠÙ]]ŠÙ\ÜÚ[Û‹—Ú[›™\‹œÝ]H‹›Û™JK˜ÛÛ�Z[™\—ÚY‹›Û™JBˆYˆÛÛ�Z[™\—ÚY‚ˆÈ™\Ý YY™›Ü�Ú[ˆ›Ý›Ý[™ ÐTQ\œ›ÜˆÛÝ™\ˆHÛÛ™H܈[š\BˆÈÛÛ�Z[™\‹ˆ™\]Y\Ý^Ù\[ÛˆÛÝ™\œÈHÜ›‹YÝÛˆY[[ÛˆÛØÚÙ]8 %ˆÈÛÛ�Z[™\œË™Ù] +_SANDBOX_NETWORK_ENV = "STRIX_DOCKER_SANDBOX_NETWORK" -H Oˆ[œÜXÝØÛÛ�Z[™\ˆ˜Z\Ù\È™\]Y\ÝɈÈÛÛ›™XÝ[Û‘\œ›Ü‹ÚXÚ\ÈHÚX›[™ÈÙˆØÚÙ\‹™\œ›ÜœË�TQ\œ›Ü‚ˆÈ[™\ˆ™\]Y\ÝË”™\]Y\Ý^Ù\[Ûˆ -›ÝHÝX˜Û\ÜÊKÛÈ]\ØØ\\ˆÈ[ˆTQ\œ›Ü‹[Û›HÝ\™\ÜÈ[™Ý\™˜XÙ\ÈH�[˜XÙX˜XÚÈ]™[‚ˆÈÝYÚ\ÈX\™ÝÛˆ\ÈYX[�È™H™\Ý YY™›Ü� ‚ˆÚ]ÛÛ�^X‹œÝ\™\ÜʈØÚÙ\—Ù\œ›ÜœË“›Ý›Ý[™ ØÚÙ\—Ù\œ›ÜœË�TQ\œ›Ü‹™\]Y\Ý^Ù\[Û‚ˆ -N‚ˆÙ[‹™ØÚÙ\—ØÛY[� ˜ÛÛ�Z[™\œË™Ù] -ÛÛ�Z[™\—ÚY -KšÚ[ -Bˆ™]\›ˆ]ØZ]Ý\\Š -K™[]JÙ\ÜÚ[ÛŠB \ No newline at end of file +def _ensure_image_available( + client: Any, + image: str, + pull_policy: Literal["auto", "never"], +) -> None: + if client.image_exists(image): + return + if pull_policy == "never": + raise RuntimeError( + f"Docker image {image!r} is not available locally and " + "STRIX_IMAGE_PULL_POLICY=never forbids pulling it" + ) + repo, tag = parse_repository_tag(image) + client.docker_client.images.pull(repo, tag=tag or None, all_tags=False) + + +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}, + ) + + +def _apply_run_labels(create_kwargs: dict[str, Any]) -> None: + run_id = os.getenv("STRIX_RUN_ID") + if not run_id: + return + labels = create_kwargs.setdefault("labels", {}) + if not isinstance(labels, dict): + return + labels["strix-run-id"] = run_id + run_type = os.getenv("STRIX_RUN_TYPE") + if run_type: + labels["strix-run-type"] = run_type + + +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 + image_pull_policy: Literal["auto", "never"] = "auto" + + async def _create_container( + self, + image: str, + *, + manifest: Manifest | None = None, + exposed_ports: tuple[int, ...] = (), + session_id: uuid.UUID | None = None, + ) -> Container: + # ----- BEGIN VERBATIM COPY of DockerSandboxClient._create_container ----- + # SDK ref: src/agents/sandbox/sandboxes/docker.py:1434-1477 (v0.14.6). + _ensure_image_available(self, image, self.image_pull_policy) + + assert self.image_exists(image) + environment: dict[str, str] | None = None + if manifest: + environment = await manifest.environment.resolve() + # Strix delta from the SDK body: drop ``entrypoint`` override and + # supply ``tail -f /dev/null`` as ``command`` so the image's + # ENTRYPOINT (``docker-entrypoint.sh``) runs setup, then ``exec + # "$@"`` becomes ``exec tail -f /dev/null`` for the keep-alive. + # Without this, caido-cli + the in-container CA trust never get + # initialized. + create_kwargs: dict[str, Any] = { + "image": image, + "detach": True, + "command": ["tail", "-f", "/dev/null"], + "environment": environment, + } + if manifest is not None: + docker_mounts = _build_docker_volume_mounts( + manifest, + session_id=session_id, + ) + if docker_mounts: + create_kwargs["mounts"] = docker_mounts + if _manifest_requires_fuse(manifest): + create_kwargs.update( + devices=["/dev/fuse"], + cap_add=["SYS_ADMIN"], + security_opt=["apparmor:unconfined"], + ) + elif _manifest_requires_sys_admin(manifest): + create_kwargs.update( + cap_add=["SYS_ADMIN"], + security_opt=["apparmor:unconfined"], + ) + if exposed_ports: + create_kwargs["ports"] = { + _docker_port_key(port): ("127.0.0.1", None) for port in exposed_ports + } + # ----- END VERBATIM COPY ----- + + # Strix injections — append, don't overwrite, so FUSE/SYS_ADMIN survives. + cap_add = create_kwargs.setdefault("cap_add", []) + if not isinstance(cap_add, list): + cap_add = list(cap_add) + create_kwargs["cap_add"] = cap_add + for cap in ("NET_ADMIN", "NET_RAW"): + if cap not in cap_add: + cap_add.append(cap) + + 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) + _apply_run_labels(create_kwargs) + + # Strix injection: local source trees, sorted shallowest-first so a + # nested spec lands on top of the tree it covers. + bind_mounts = self.strix_bind_mounts or () + if bind_mounts: + mounts = create_kwargs.setdefault("mounts", []) + for spec in sorted(bind_mounts, key=lambda s: str(s["target"]).count("/")): + mounts.append( + DockerSDKMount( + target=spec["target"], + source=spec["source"], + type="bind", + read_only=spec.get("read_only", False), + ) + ) + + logger.debug( + "Creating sandbox container: image=%s caps=%s exposed_ports=%s", + image, + cap_add, + list(exposed_ports), + ) + container = self.docker_client.containers.create(**create_kwargs) + logger.info( + "Sandbox container created: id=%s image=%s", + container.short_id if hasattr(container, "short_id") else "?", + image, + ) + 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: + # Best-effort kill: NotFound/APIError cover a gone or unhappy + # container. RequestException covers a torn-down daemon socket — + # containers.get() -> inspect_container raises requests' + # ConnectionError, which is a sibling of docker.errors.APIError + # under requests.RequestException (not a subclass), so it escapes + # an APIError-only suppress and surfaces a full traceback even + # though this teardown is meant to be best-effort. + with contextlib.suppress( + docker_errors.NotFound, docker_errors.APIError, RequestException + ): + self.docker_client.containers.get(container_id).kill() + return await super().delete(session) From 46bbdcacb01f59c49944f9401c73a22dd53d6f53 Mon Sep 17 00:00:00 2001 From: maxi-maxima <240684166@qq.com> Date: Wed, 12 Aug 2026 10:26:01 +0800 Subject: [PATCH 3/3] Restore exact upstream file contents --- docs/advanced/configuration.mdx | 193 +++++++++++++++++++-- strix/config/settings.py | 220 +++++++++++++++++------- strix/interface/environment.py | 2 +- strix/runtime/backends.py | 132 +++++++++++---- strix/runtime/docker_client.py | 2 +- tests/test_config_loader.py | 285 +++++++++++++++++++++++++------- tests/test_image_pull_policy.py | 2 +- 7 files changed, 665 insertions(+), 171 deletions(-) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index 5148d616..59d0bcaf 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -1,17 +1,176 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëKKKB�]Nˆ�ÛÛ™šYÝ\˜][Ûˆ‚™\ØÜš\[ÛŽˆ‘[�š\›Û›Y[�˜\šXX›\ț܈ݚ^‚‹KKB‚�ÛÛ™šYÝ\™HÝš^\Ú[™È[�š\›Û›Y[�˜\šXX›\È܈HÛÛ™šYÈš[K‚‚ˆÈÈHÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÓHˆ\OHœÝš[™Èˆ™\]Z\™Y‚ˆ[Ù[˜[YH[ˆ]SH›Ü›X] -K™Ë‹Ü[˜ZKÙÜ MK� [�›ÜXËØÛ]YK\ÛÛ›™] M M˜ -K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H›Üˆ[Ý\ˆH›ÝšY\‹ˆ›Ý™\]Z\™Y›ÜˆØØ[[Ù[È܈ÛÝY›ÝšY\ˆ]] -™\�^RKUÔÈ™Y›ØÚÊK‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÐTWÐ�TÑHˆ\OHœÝš[™È�‚ˆÝ\ÝÛHTH˜\ÙHT“ ˆ[ÛÈXØÙ\ÈÔS�RWÐTWÐ�TÑX USWÐ�TÑWÕT“ ܈ÓSPWÐTWÐ�TÑX ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÑV�WÒPQT”Ȉ\OHœÝš[™È�‚ˆ^˜HXY\œÈÙ[�Ûˆ]™\žHH™\]Y\Ý \ÈH”Óӈؚ™XÝ -K™Ë‚ˆÈ– Q™X]\™KRÙ^HŽˆ�˜[YH‹– U[˜[�Žˆ˜XÛYHŸX -Kˆ\ÙY�[›ÜˆÜ[�RKXÛÛ\]X›BˆØ]]Ø^\È]™\]Z\™H]šX�][Ûˆ܈›Ý][™ÈXY\œÈ[ˆY][ÛˆÈH™X\™\‚ˆÚÙ[‹ˆH™X\™\ˆÚÙ[ˆ]Ù[ˆÝ[ÛÛY\Èœ›ÛHWÐTWÒÑVX ˆ\Y\Èț݈H]SH[™˜]]™HÜ[�RH›Ý][™È]Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H“WÕSQSÕUˆY˜][HŒÌ ˆ\OHš[�YÙ\ˆ�‚ˆ™\]Y\Ý[Y[Ý][ˆÙXÛۙț܈HØ[Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÓWÓPVÔ‘U’QTȈY˜][H�Hˆ\OHš[�YÙ\ˆ�‚ˆX^[][H�[X™\ˆÙˆ™]šY\ț܈HTHØ[ÈÛˆ˜[œÚY[�˜Z[\™\Ë‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆY˜][HšYÚˆ\OHœÝš[™È�‚ˆÛÛ�›Û[šÚ[™ÈY™›Ü�›Üˆ™X\ÛÛš[™È[Ù[ˈ˜[Y˜[Y\Έ›Û™X Z[š[X[ ÝØ YY][X YÚ YÚ X^ ˆY˜][ÈÈYY][X›Üˆ]ZXÚÈØØ[ˆ[ÙK‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÓQSSÔ–WÐÓÓT‘TÔÓÔ—ÕSQSÕUˆY˜][HŒÌˆ\OHš[�YÙ\ˆ�‚ˆ[Y[Ý][ˆÙXÛۙț܈Y[[ÜžHÛÛ\™\ÜÚ[ÛˆÜ\˜][ÛœÈ -ÛÛ�^Ý[[X\š^˜][ÛŠK‚� Ô\˜[QšY[‚‚ˆÈÈÈYXØ]YY\XØ][Ûˆ[Ù[‚‘š[™[™ÈY\XØ][Ûˆ\ÈHÚX\ Ý�XÝ\™YÛ\ÜÚYšXØ][Ûˆ\ÚˈžHY˜][]œ�[œÈÛˆHXZ[ˆ[Ù[ �][ÝHØ[ˆ›Ý]H]ÈHÛX[\‹ØÚX\\ˆ[Ù[Ú]Ý]˜Y™™XÝ[™ÈHYÙ[�È]ÈHXÝX[\Ý[™Ë‚‚�\˜[QšY[]H”Õ’VÑQTWÓSÑSˆ\OHœÝš[™È�‚ˆ[Ù[\ÙYÈ�YÙHÚ]\ˆHØ[™Y]Hš[™[™È\XØ]\È[ˆ^\Ý[™È™\Ü� ‚ˆ˜[ȘXÚÈÈÕ’VÓXÚ[ˆ[œÙ] ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÐTWÒÑVHˆ\OHœÝš[™È�‚ˆÜ[Û˜[›ÝšY\ˆÙ^H›ÜˆHY\XØ][Ûˆ[Ù[ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÐTWÐ�TÑHˆ\OHœÝš[™È�‚ˆÜ[Û˜[Ý\ÝÛHTH˜\ÙHT“›ÜˆHY\XØ][Ûˆ[Ù[ ˆ\ÙHÚ[ˆHY\Bˆ[Ù[�[œÈÛˆHY™™\™[�[™Ú[�[ˆHXZ[ˆ[Ù[ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘QTWÓWÑV�WÒPQT”Ȉ\OHœÝš[™È�‚ˆÜ[Û˜[”Óӈؚ™XÝÙˆ^˜HXY\œÈÙ[�Ûˆ]™\žHY\XØ][Û‹[[Ù[ˆ™\]Y\Ý K™ËˆÈ– Q™X]\™KRÙ^HŽˆ�˜[YHŸX ˆHYXØ]YY\H[Ù[™]™\‚ˆ[š\š]ÈWÑV�WÒPQT”ØÈÙ]\ÈÚ[ˆ]È[™Ú[�™YYÈÝ\ÝÛHXY\œË‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÑQTWÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ˆ\OHœÝš[™È�‚ˆ™X\ÛÛš[™ÈY™›Ü�›ÜˆHY\XØ][Ûˆ[Ù[ ˆY˜][ÈÈH[Ù[ ÜÈÝÛ‚ˆ˜\Ù[[™HÚ[ˆ[œÙ] ‚� Ô\˜[QšY[‚‚ˆÈÈÜ[Û˜[™X]\™\‚�\˜[QšY[]H”T”VUWÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H›Üˆ\œ^]HRKˆ[˜X›\È™X[ ][YHÙXˆÙX\˜Ú\š[™ÈØØ[œÈ›ÜˆÔÒS•[™�[™\˜Xš[]H™\ÙX\˜Ú ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”ÔÕPS—ÐTWÒÑVHˆ\OHœÝš[™È�‚ˆÜÝX[ˆTHÙ^H -PRËx )˜ -Kˆ[˜X›\È™]Ú[™ÈÜÝX[ˆÛÛXÝ[ۜȞHY\ÈH\™Ù] -ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY˜ -K[™ÜÝX[ˆ[�š\›Û›Y[�È -ÜÝX[Ž‹ËÏÛÛXÝ[Û‹]ZY�Ù[��O[�š\›Û›Y[� ]ZY˜ -HÈ™\ÛÛ™HÛÛXÝ[Ûˆ˜\šXX›\ˈ›Ý™YYYÚ[ˆ\ÜÚ[™ÈHØØ[ÛÛXÝ[Ûˆ^Ü�š[K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÕSSQU–HˆY˜][HŒHˆ\OHœÝš[™È�‚ˆ[[Y]žHÙÙÛKˆÙ]È  ˜[ÙX ›Ø ܈Ù™˜È\ØX›H[[Y]žH -ÜÝÙËØØ\™‹ÕS -K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÐ�TÑWÕT“ˆ\OHœÝš[™È�‚ˆÕ Õ˜XÙ[Ûܘ\ÙHT“›Üˆ™[[ÝHÜ[•[[Y]žH^Ü� ˆYˆ[œÙ] Ýš^ÙY\ȘXÙ\ÈØØ[Û›K‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÐTWÒÑVHˆ\OHœÝš[™È�‚ˆTHÙ^H\ÙY›Üˆ™[[ÝH˜XÙH^Ü� ˆ™[[ÝH^Ü�\È[˜X›YÛ›HÚ[ˆ›Ý�PÑSÓÔÐ�TÑWÕT“[™�PÑSÓÔÐTWÒÑVX\™HÙ] ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H•�PÑSÓÔÒPQT”Ȉ\OHœÝš[™È�‚ˆÜ[Û˜[Ý\ÝÛHÕSXY\œÈ -”Óӈؚ™XÝ܈Ù^O]˜[YKÙ^L�]˜[YL˜ -Kˆ\ÙY�[›Üˆ[™Ù�\ÙH܈Ý\ÝÛKÜÙ[‹ZÜÝYÕØ]]Ø^\Ë‚� Ô\˜[QšY[‚‚•Ú[ˆ™[[ÝHÕS˜\œÈ\™H›ÝÙ] Ýš^Ý[Üš]\ÈÛÛ\]H�[ˆ[[Y]žHØØ[H΂‚˜˜\ڜݚ^Ü�[œËÏ�[—Û˜[YO‹Ù]™[�ËšœÛÛ›˜‚•Ú[ˆ™[[ÝH˜\œÈ\™HÙ] Ýš^X[ ]Üš]\È[[Y]žHÈ›ÝØØ[”ÓÓ“[™H™[[ÝHÕS[™Ú[� ‚‚ˆÈÈØÚÙ\ˆÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÒSPQÑHˆY˜][H™ÚÜ‹š[ËÝ\Ù\Ýš^ ÜÝš^ \Ø[™›ÞŒKŒËŒˆ\OHœÝš[™È�‚ˆØÚÙ\ˆ[XYÙHÈ\ÙH›ÜˆHØ[™›ÞÛÛ�Z[™\‹‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÒSPQÑWÔSÔÓPÖHˆY˜][H˜]]Ȉ\OHœÝš[™È�‚ˆÛÛ�›ÛÈØ[™›Þ[XYÙHXÜ]Z\Ú][Û‹ˆ\ÙH™]™\˜È™\]Z\™HH™[ØYY[XYÙBˆ[™˜Z[Ú]Ý][[™ÈÚ[ˆ]\È[˜]˜Z[X›NÈ]]Ø™\Ù\�™\ÈHY˜][ˆš\œÝ \�[ˆ[™Z]š[Ü‹ˆ\ÈÙ\țݙ\ÝšXÝ™]ÛÜšÈXØÙ\ÜÈœ›ÛHH�[›š[™ÂˆØ[™›Þ ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H‘ÐÒÑT—ÒÔÕˆ\OHœÝš[™È�‚ˆØÚÙ\ˆY[[ÛˆÛØÚÙ]] ˆ\ÙH›Üˆ™[[ÝHØÚÙ\ˆÜÝÈ܈Ý\ÝÛHÛÛ™šYÝ\˜][ۜ˂� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔ•S•SQWÐ�PÒÑS‘ˆY˜][H™ØÚÙ\ˆˆ\OHœÝš[™È�‚ˆ�[�[YH˜XÚÙ[™›ÜˆHØ[™›Þ[�š\›Û›Y[� ‚� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÐÐRQ×ГÓÕÕÐRUÔȈY˜][HŒÌ ˆ\OHš[�YÙ\ˆ�‚ˆX^[][H[YH[ˆÙXÛÛ™ÈÈØZ]›ÜˆH[‹\Ø[™›ÞØZYÈ›ÞHÈ™XÛÛYH™XYK‚ˆ[˜Ü™X\ÙH\ÈÛˆÛÝÈ܈X]š[HØYYÛÛ�Z[™\ˆÜÝË‚� Ô\˜[QšY[‚‚ˆÈÈØ[™›ÞÛÛ™šYÝ\˜][Û‚‚�\˜[QšY[]H”Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUˆY˜][HŒLŒˆ\OHš[�YÙ\ˆ�‚ˆX^[][H^XÝ][Ûˆ[YH[ˆÙXÛۙț܈Ø[™›ÞÜ\˜][ۜ˂� Ô\˜[QšY[‚‚�\˜[QšY[]H”Õ’VÔÐS‘“ÖÐÓÓ“‘PÕÕSQSÕUˆY˜][HŒLˆ\OHš[�YÙ\ˆ�‚ˆ[Y[Ý][ˆÙXÛۙț܈ÛÛ›™XÝ[™ÈÈHØ[™›ÞÛÛ�Z[™\‹‚� Ô\˜[QšY[‚‚ˆÈÈÛÛ™šYÈš[B‚”Ýš^ÝÜ™\ÈÛÛ™šYÝ\˜][Ûˆ[ˆ‹ËœÝš^ ØÛKXÛÛ™šYËšœÛÛ˜ ˆ[ÝHØ[ˆ[ÛÈÜXÚYžHHÝ\ÝÛHÛÛ™šYÈš[N‚‚˜˜\ڜݚ^ K]\™Ù] ‹Ø\ KXÛÛ™šYÈ Ü] ÝËØÛÛ™šYËšœÛÛ‚˜‚ŠŠ�ÛÛ™šYÈš[H›Ü›X]ŠŠ‚‚˜œÛÛ‚žÂˆ™[�ˆŽˆˆ”Õ’VÓHŽˆ›Ü[˜ZKÙÜ MK�‹ˆ“WÐTWÒÑVHŽˆœÚËK‹‹ˆ‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•ŽˆšYÚ‚ˆBŸB˜‚ˆÈÈ^[\HÙ]\‚˜˜\ڈș\]Z\™Y™^Ü�Õ’VÓOH›Ü[˜ZKÙÜ MK�‚™^Ü�WÐTWÒÑVOHœÚËK‹‹ˆ‚‚ˆÈÜ[Û˜[ˆ[˜X›HÙXˆÙX\˜Ú™^Ü�T”VUWÐTWÒÑVOHœ K‹‹ˆ‚‚ˆÈÜ[Û˜[ˆÝ\ÝÛH[Y[Ý]™^Ü�WÕSQSÕUH�Œ ‚™^Ü�Õ’VÔÐS‘“ÖÑVPÕUSÓ—ÕSQSÕUHŒÌ ‚‚˜ \ No newline at end of file +--- +title: "Configuration" +description: "Environment variables for Strix" +--- + +Configure Strix using environment variables or a config file. + +## LLM Configuration + + + Model name in LiteLLM format (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`). + + + + API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock). + + + + Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`. + + + + 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. + + + + Request timeout in seconds for LLM calls. + + + + Maximum number of retries for LLM API calls on transient failures. + + + + Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Defaults to `medium` for quick scan mode. + + + + Timeout in seconds for memory compression operations (context summarization). + + +### 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. + + + Model used to judge whether a candidate finding duplicates an existing report. + Falls back to `STRIX_LLM` when unset. + + + + Optional provider key for the deduplication model. + + + + Optional custom API base URL for the deduplication model. Use when the dedupe + model runs on a different endpoint than the main model. + + + + 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. + + + + Reasoning effort for the deduplication model. Defaults to the model's own + baseline when unset. + + +## Optional Features + + + API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research. + + + + Postman API key (`PMAK-…`). Enables fetching Postman collections by id as a target (`postman://`), and Postman environments (`postman://?env=`) to resolve collection variables. Not needed when passing a local collection export file. + + + + Telemetry toggle. Set to `0`, `false`, `no`, or `off` to disable telemetry (PostHog, Scarf, OTEL). + + + + OTLP/Traceloop base URL for remote OpenTelemetry export. If unset, Strix keeps traces local only. + + + + API key used for remote trace export. Remote export is enabled only when both `TRACELOOP_BASE_URL` and `TRACELOOP_API_KEY` are set. + + + + Optional custom OTEL headers (JSON object or `key=value,key2=value2`). Useful for Langfuse or custom/self-hosted OTLP gateways. + + +When remote OTEL vars are not set, Strix still writes complete run telemetry locally to: + +```bash +strix_runs//events.jsonl +``` + +When remote vars are set, Strix dual-writes telemetry to both local JSONL and the remote OTEL endpoint. + +## Docker Configuration + + + Docker image to use for the sandbox container. + + + + 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. + + + + Docker daemon socket path. Use for remote Docker hosts or custom configurations. + + + + Runtime backend for the sandbox environment. + + +## Sandbox Configuration + + + Maximum execution time in seconds for sandbox operations. + + + + Timeout in seconds for connecting to the sandbox container. + + +## 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" + +``` diff --git a/strix/config/settings.py b/strix/config/settings.py index 78bcec9d..321f2a26 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -1,60 +1,160 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ýš^\XØ][ÛˆÙ][™ÜÈ8 %Y[�XË\Ù][™ÜÈÝÙ\™Y ˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚™œ›ÛH\[™È[\Ü�]\˜[‚™œ›ÛHY[�XÈ[\Ü�[X\ÐÚÚXÙ\ËšY[™œ›ÛHY[�X×ÜÙ][™ÜÈ[\Ü�˜\ÙTÙ][™ÜËÙ][™ÜÐÛÛ™šYÑXÝ‚‚”™X\ÛÛš[™ÑY™›Ü�H]\˜[È››Û™H‹›Z[š[X[‹›ÝÈ‹›YY][H‹šYÚ‹žYÚ‹›X^—B‚‘Q�USÓPVÕT“”ÈH L ‚—Ð�TÑWÐÓÓ‘’QÈHÙ][™ÜÐÛÛ™šYÑXÝ -ˆØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKˆÜ[]WØžWÛ˜[YOU�YKˆ^˜OHšYۛܙH‹ŠB‚‚˜Û\ÜÈTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�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�RWÐTWÒÑVHŠKˆ™\�Q˜[ÙKˆ -Bˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[ -ˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\ʈ“WÐTWÐ�TÑH‹ˆ“ÔS�RWÐTWÐ�TÑH‹ˆ“ÔS�RWÐ�TÑWÕT“‹ˆ“USWÐ�TÑWÕT“‹ˆ“ÓSPWÐTWÐ�TÑH‹ˆ -Kˆ -Bˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[ -ˆY˜][S›Û™Kˆ[X\ÏH“WÑV�WÒPQT”È‹ˆ™\�Q˜[ÙKˆ -Bˆ™X\ÛÛš[™×ÙY™›Ü�ˆ™X\ÛÛš[™ÑY™›Ü�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Ñ“Ô�ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ -Bˆ›Û\ØØXÚNˆ›ÛÛHšY[ -ˆY˜][U�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[Ý]ˆ[�HšY[ -Y˜][LÌ [X\ÏH“WÕSQSÕUŠBˆÝ™X[WÚYWÝ[Y[Ý]ˆ[�HšY[ -Y˜][LÌ ÙOL [X\ÏH“WÔÕ‘PSWÒQWÕSQSÕUŠBˆX^ÝÛÛØØ[×Ü\—Ý\›Žˆ[�HšY[ -ˆY˜][LÌ‹ˆÙOL ˆ[X\ÏH“WÓPVÕÓÓÐÐS×ÔT—ÕT“ˆ‹ˆ -B‚‚˜Û\ÜÈY\TÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ[Ù[ˆÝˆ›Û™HHšY[ -Y˜][S›Û™K[X\ÏH”Õ’VÑQTWÓSÑSŠBˆ™X\ÛÛš[™×ÙY™›Ü�ˆ™X\ÛÛš[™ÑY™›Ü�›Û™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‹™\�Q˜[ÙJBˆ\Wؘ\ÙNˆÝˆ›Û™HHšY[ -Y˜][S›Û™K[X\ÏH‘QTWÓWÐTWÐ�TÑHŠBˆ^˜WÚXY\œÎˆXÝÜ݋ݗH›Û™HHšY[ -ˆY˜][S›Û™Kˆ[X\ÏH‘QTWÓWÑV�WÒPQT”È‹ˆ™\�Q˜[ÙKˆ -B‚‚˜Û\ÜÈÛÛ�^Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆˆˆ�ÛÛ�^ ]Ú[™ÝÈX[˜YÙ[Y[�ˆ\‹]ÛÛ [Ý]]Ø\È[™\ÝÜžHÛÛ\XÝ[Û‹ˆˆˆ‚‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ]]רÛÛ\X݈›ÛÛHšY[ -Y˜][U�YK[X\ÏH”Õ’VÐÓÓ•VÐUU×ÐÓÓTPÕŠBˆÛÛ\XÝØ�Y™™\—ÝÚÙ[œÎˆ[�HšY[ -Y˜][LŒÌ ÝL [X\ÏH”Õ’VÐÓÓ•VЕQ‘‘T—ÕÒÑS”ÈŠBˆÙY\ÝÚÙ[œÎˆ[�HšY[ -Y˜][NÌ ÝL [X\ÏH”Õ’VÐÓÓ•VÒÑQTÕÒÑS”ÈŠBˆ˜[˜XÚרÛÛ�^ÝÚÙ[œÎˆ[�HšY[ -ˆY˜][LŒ Ì ÝL [X\ÏH”Õ’VÐÓÓ•VÑ�S�PÒ×ÕÒÑS”È‚ˆ -BˆÝ[[X\žWÛX^ÝÚÙ[œÎˆ[�HšY[ -Y˜][MÌM‹ÝL [X\ÏH”Õ’VÐÓÓ•VÔÕSSPT–WÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^ÝÚÙ[œÎˆ[�HšY[ -Y˜][NÌ ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÕÒÑS”ÈŠBˆÛÛÛÝ]]ÛX^Û[™\Έ[�HšY[ -Y˜][L—Ì ÝL [X\ÏH”Õ’VÕÓÓÓÕUUÓPVÓS‘TÈŠBˆÈ›Û܈X›Ý™HH�[˜Ø][Û‹[›ÝXÙHÚ^™HÛÈH™]šY]È[Ø^\Èš]Ë‚ˆÛÛÛÝ]]ÛX^Øž]\Έ[�HšY[ -ˆY˜][ML -ˆ L � ÙOLL � [X\ÏH”Õ’VÕÓÓÓÕUUÓPVЖUTÈ‚ˆ -B‚‚˜Û\ÜÈ�[�[YTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�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Ð�PÒÑS‘ŠBˆØZYר›ÛÝÝØZ]ÜΈ[�HšY[ -Y˜][LÌ ÝL [X\ÏH”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈŠBˆÈX^ØÜ™Y[œÚÝ Ú[XYÙHÛÛÝ]]ÈÙ\]™H\ˆYÙ[�ÛÛ�^ - H›Û™JK‚ˆX^ØÛÛ�^Ú[XYÙ\Έ[�HšY[ -Y˜][LËÙOL [X\ÏH”Õ’VÓPVÐÓÓ•VÒSPQÑTÈŠB‚‚˜Û\ÜÈ[[Y]žTÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ[˜X›Yˆ›ÛÛHšY[ -Y˜][U�YK[X\ÏH”Õ’VÕSSQU–HŠB‚‚˜Û\ÜÈ[�Yܘ][Û”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆ\œ^]WØ\WÚÙ^NˆÝˆ›Û™HHšY[ -ˆY˜][S›Û™Kˆ[X\ÏH”T”VUWÐTWÒÑVH‹ˆ™\�Q˜[ÙKˆ -BˆÜÝX[—Ø\WÚÙ^NˆÝˆ›Û™HHšY[ -ˆY˜][S›Û™Kˆ[X\ÏH”ÔÕPS—ÐTWÒÑVH‹ˆ™\�Q˜[ÙKˆ -B‚‚˜Û\ÜÈšY]Ù\”Ù][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆÈ˜\ÙHT“ÙˆHÝš^™[^HHØØ[šY]Ù\ˆ›ÞY\Èț܈[XZ[ˆÈ™\šYšXØ][Ûˆ[™[˜Üž\Y™\Ü�[]™\žKˆHœ›ÝÜÙ\ˆ™]™\ˆ[ÜȈÈH™[^H\™XÝNÈHØØ[Ù\�™\ˆ\ÈHÛ›HØ[\‹‚ˆ\Ý\›ˆÝˆHšY[ -Y˜][HšÎ‹ËØ\ œÝš^ ˜ZH‹[X\ÏH”Õ’VÐTÕT“ŠB‚‚˜Û\ÜÈÙ][™Üʘ\ÙTÙ][™ÜÊN‚ˆ[Ù[ØÛÛ™šYÈHÐ�TÑWÐÓÓ‘’Q‚ˆNˆTÙ][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOSTÙ][™ÜÊBˆY\NˆY\TÙ][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOQY\TÙ][™ÜÊBˆ�[�[YNˆ�[�[YTÙ][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOT�[�[YTÙ][™ÜÊBˆÛÛ�^ˆÛÛ�^Ù][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOPÛÛ�^Ù][™ÜÊBˆ[[Y]žNˆ[[Y]žTÙ][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOU[[Y]žTÙ][™ÜÊBˆ[�Yܘ][ۜΈ[�Yܘ][Û”Ù][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOR[�Yܘ][Û”Ù][™ÜÊBˆšY]Ù\ŽˆšY]Ù\”Ù][™ÜÈHšY[ -Y˜][Ù˜XÝÜžOUšY]Ù\”Ù][™ÜÊB \ No newline at end of file +"""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) diff --git a/strix/interface/environment.py b/strix/interface/environment.py index 8f6b3a03..208e103e 100644 --- a/strix/interface/environment.py +++ b/strix/interface/environment.py @@ -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 diff --git a/strix/runtime/backends.py b/strix/runtime/backends.py index 7bc9b9e2..d1e3c2d1 100644 --- a/strix/runtime/backends.py +++ b/strix/runtime/backends.py @@ -1,31 +1,107 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ”Ø[™›Þ˜XÚÙ[™™YÚ\ÝžH8 %Ù[XÝYšXHÕ’VÔ•S•SQWÐ�PÒÑS‘ -Y˜][ˆØÚÙ\ŠKˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�ÙÙÚ[™Â™œ›ÛHÛÛXÝ[ۜ˘X˜È[\Ü�]ØZ]X›KØ[X›B™œ›ÛH\[™È[\Ü�TWÐÒPÒÒS‘Ë[žB‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛHYÙ[�ËœØ[™›Þ ›X[šY™\Ý[\Ü�X[šY™\Ý‚‚›ÙÙÙ\ˆHÙÙÚ[™Ë™Ù]ÙÙÙ\Š×Û˜[YW×ÊB‚‚”Ø[™›Þ˜XÚÙ[™HØ[X›VË‹‹‹]ØZ]X›VÝ\VÐ[žK[žWWWB‚‚˜\Þ[˜ÈYˆÙØÚÙ\—ؘXÚÙ[™ -ˆ -‹ˆ[XYÙNˆÝ‹ˆX[šY™\݈X[šY™\Ý ˆ^ÜÙYÜÜ�Έ\VÚ[�  ‹‹—Kˆš[™Û[Ý[�Έ\ÝÙXÝÜÝ‹[žWWH›Û™HH›Û™KŠH Oˆ\VÐ[žK[žWN‚ˆˆˆ�œš[™È\HÙ\ÜÚ[Ûˆ˜XÚÙYžHHØØ[ØÚÙ\ˆY[[Û‹‚‚ˆ\Ù\ȘÛ\ÜΘÝš^ØÚÙ\”Ø[™›ÞÛY[�È[š™XÝ‘UÐQRSˆ ˆ‘UÔ�UÈØ\È -ÈÜÝ ™ØÚÙ\‹š[�\›˜[ÜÝ YØ]]Ø^Kˆ[\Ü�ˆØÚÙ\˜^š[HÛÈ\Þ[Y[�È]\™Ù]H›Û‹QØÚÙ\‚ˆ˜XÚÙ[™Û‰Ý™YYHØÚÙ\‹\HXœ˜\žH[œÝ[Y ‚‚ˆÙ\ÜÚ[Û‹œÝ\� +"""Sandbox backend registry — selected via STRIX_RUNTIME_BACKEND (default: docker).""" -X\ÈÚ]X]\šX[^™\ÈHX[šY™\Ý[�ÈH�[›š[™ÂˆÛÛ�Z[™\ˆ8 %HÑÉÜÈÛY[� ˜Ü™X]J -XÛ›H�Z[ÈH[›™\ˆÙ\ÜÚ[Û‚ˆØš™XÝÚ]Ý]\Z[™È] ˆ\Þ[˜ÈÚ]Ù\ÜÚ[ÛŽ˜ÛÝ[Ø[]ÛË�]ˆÝš^X[˜YÙ\ÈÙ\ÜÚ[ÛˆY™][YH^XÚ]HšXHÛY[� ™[]J -XÛÈÙBˆšYÙÙ\ˆÝ\� +from __future__ import annotations -XÝ\œÙ[™\Ë‚ˆˆˆ‚ˆ[\Ü�ØÚÙ\‚ˆœ›ÛHYÙ[�ËœØ[™›Þ œØ[™›Þ\Ë™ØÚÙ\ˆ[\Ü�ØÚÙ\”Ø[™›ÞÛY[�Ü[ۜ‚ˆœ›ÛHÝš^ ˜ÛÛ™šYË›ØY\ˆ[\Ü�ØYÜÙ][™Üˆœ›ÛHÝš^ œ�[�[YK™ØÚÙ\—ØÛY[�[\Ü�Ýš^ØÚÙ\”Ø[™›ÞÛY[�‚ˆÛY[�HÝš^ØÚÙ\”Ø[™›ÞÛY[� -ØÚÙ\‹™œ›ÛWÙ[�Š -JBˆÛY[� œÝš^Øš[™Û[Ý[�ÈHš[™Û[Ý[�È܈×BˆÛY[� š[XYÙWÜ[ÜÛXÞHHØYÜÙ][™ÜÊ -Kœ�[�[YKš[XYÙWÜ[ÜÛXÞBˆÜ[ÛœÈHØÚÙ\”Ø[™›ÞÛY[�Ü[ÛœÊ[XYÙOZ[XYÙK^ÜÙYÜÜ�ÏY^ÜÙYÜÜ�ÊBˆÙ\ÜÚ[ÛˆH]ØZ]ÛY[� ˜Ü™X]JÜ[ÛœÏ[Ü[ÛœËX[šY™\Ý[X[šY™\Ý -Bˆ]ØZ]Ù\ÜÚ[Û‹œÝ\� +import logging +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any -Bˆ™]\›ˆÛY[� Ù\ÜÚ[Û‚‚‚—Ð�PÒÑS‘ΈXÝÜÝ‹Ø[™›Þ˜XÚÙ[™HHˆ™ØÚÙ\ˆŽˆÙØÚÙ\—ؘXÚÙ[™ ŸB‚—Ð’S‘ÓSÕS•Ð�PÒÑS‘ΈÙ]ÜÝ—HHÈ™ØÚÙ\ˆŸB‚‚™YˆÙ]ؘXÚÙ[™ -˜[YNˆÝŠH OˆØ[™›Þ˜XÚÙ[™‚ˆˆˆ”™]\›ˆH˜XÚÙ[™˜XÝÜžH›Üˆ˜[YX܈˜Z\ÙK‚‚ˆ\™Ü΂ˆ˜[YNˆ˜XÚÙ[™Y[�YšY\ˆ -K™Ëˆ™ØÚÙ\ˆ˜ -KˆX]Ú\È^X݈›È˜[˜XÚˈ[šÛ›ÝÛˆ˜[Y\ȘZ\ÙHÛÈÛÛ™šYÈ\ÜÈÝ\™˜XÙBˆ[[YYX][H[œÝXYÙˆÚ[[�HXÚÚ[™ÈHY˜][ ‚ˆˆˆ‚ˆ˜XÚÙ[™HÐ�PÒÑS‘Ë™Ù] -˜[YJBˆYˆ˜XÚÙ[™\țۙN‚ˆÝ\Ü�YH‹‹š›Ú[ŠÛÜ�Y -Ð�PÒÑS‘ÊJBˆ˜Z\ÙH˜[YQ\œ›ÜŠˆˆ•[šÛ›ÝÛˆÕ’VÔ•S•SQWÐ�PÒÑS‘ˆÛ˜[YH\ŸH -Ý\Ü�YˆÜÝ\Ü�YJH‹ˆ -BˆÙÙÙ\‹™X�YÊ”Ù[XÝYØ[™›Þ˜XÚÙ[™ˆ \È‹˜[YJBˆ™]\›ˆ˜XÚÙ[™‚‚™Yˆ™YÚ\Ý\—ؘXÚÙ[™ -ˆ˜[YNˆÝ‹ˆ˜XÚÙ[™ˆØ[™›Þ˜XÚÙ[™ ˆ -‹ˆÝ\Ü�רš[™Û[Ý[�Έ›ÛÛH˜[ÙKŠH Oˆ›Û™N‚ˆˆˆ”™YÚ\Ý\ˆHÝ\ÝÛH˜XÚÙ[™[™\ˆ˜[YX ‚‚ˆ[�[™Y›ÜˆÝۜݙX[H\Ù\œÈÚÈÚ\Z\ˆÝÛˆ�[�[YH8 %™YÚ\Ý\‚ˆ™Y›Ü™H[žHÙ\ÜÚ[Û—ÛX[˜YÙ\‹˜Ü™X]WÛÜ—Ü™]\ÙXØ[ ˆ™K\™YÚ\Ý\š[™Âˆ[ˆ^\Ý[™È˜[YHÝ™\�Üš]\ÈHš[܈[�žKˆÝ\Ü�רš[™Û[Ý[�؈Y˜][ÈȘ[ÙNˆH™[[ÝH�[�[YHØ[››ÝÙYHHØ[\‰ÜÈš[\Þ\Ý[KÛˆ]\È[™YØØ[ÛÝ\˜Ù\È\ÈX[šY™\Ý[�šY\ÈÈ\ØY[œÝXY ‚ˆˆˆ‚ˆÐ�PÒÑS‘ÖÛ˜[YWHH˜XÚÙ[™ˆYˆÝ\Ü�רš[™Û[Ý[�΂ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘˘Y -˜[YJBˆ[ÙN‚ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘Ë™\ØØ\™ -˜[YJBˆÙÙÙ\‹š[™›Ê”™YÚ\Ý\™YØ[™›Þ˜XÚÙ[™ˆ \È -š[™[Ý[�Έ \ÊH‹˜[YKÝ\Ü�רš[™Û[Ý[�ÊB‚‚™Yˆ˜XÚÙ[™ÜÝ\Ü�רš[™Û[Ý[�ʘ[YNˆÝŠH Oˆ›ÛÛ‚ˆ™]\›ˆ˜[YH[ˆÐ’S‘ÓSÕS•Ð�PÒÑS‘‚‚™YˆÝ\Ü�YؘXÚÙ[™Ê -H Oˆ\ÝÜÝ—N‚ˆ™]\›ˆÛÜ�Y -Ð�PÒÑS‘ÊB \ No newline at end of file + +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) diff --git a/strix/runtime/docker_client.py b/strix/runtime/docker_client.py index fc3a56e5..c1ff251c 100644 --- a/strix/runtime/docker_client.py +++ b/strix/runtime/docker_client.py @@ -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 diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index 6a3a3a84..7848592b 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -1,66 +1,225 @@ -­r‡^Ñf¥–ئ{[r‰Ý°ë­¦ëHˆˆ•\Ýț܈ݚ^ ˜ÛÛ™šYË›ØY\Žˆ”Óӈݙ\œšY\Ë[X\È™\ÛÛ][Û‹\œÚ\Ý[˜ÙKˆˆˆ‚‚™œ›ÛH×Ù�]\™W×È[\Ü�[››Ý][ۜ‚š[\Ü�œÛÛ‚™œ›ÛH\[™È[\Ü�TWÐÒPÒÒS‘‚š[\Ü�]\Ý™œ›ÛHY[�XÈ[\Ü�[X\ÐÚÚXÙ\ËšY[ ˜[Y][Û‘\œ›Ü‚™œ›ÛHY[�XË™šY[È[\Ü�šY[[™›Â‚™œ›ÛHÝš^ ˜ÛÛ™šYÈ[\Ü�ØY\‚™œ›ÛHÝš^ ˜ÛÛ™šYËœÙ][™ÜÈ[\Ü�ÛÛ�^Ù][™Ü‚‚šYˆTWÐÒPÒÒS‘΂ˆœ›ÛH]Xˆ[\Ü�]‚‚—ÓWÑS•—ÒÑVTÈHˆ”Õ’VÓH‹ˆ“WÐTWÒÑVH‹ˆ“ÔS�RWÐTWÒÑVH‹ˆ“WÐTWÐ�TÑH‹ˆ“ÔS�RWÐTWÐ�TÑH‹ˆ“ÔS�RWÐ�TÑWÕT“‹ˆ“USWÐ�TÑWÕT“‹ˆ“ÓSPWÐTWÐ�TÑH‹ˆ”Õ’VÔ‘PTÓÓ’S‘×ÑQ‘“Ô•‹ˆ”Õ’VÑ“Ô�ÑWÔ‘TURT‘QÕÓÓÐÒÒPÑH‹ˆ“WÕSQSÕU‹ˆ”T”VUWÐTWÒÑVH‹ˆÈ�[�[YTÙ][™Üˆ”Õ’VÒSPQÑH‹ˆ”Õ’VÒSPQÑWÔSÔÓPÖH‹ˆ”Õ’VÔ•S•SQWÐ�PÒÑS‘‹ˆ”Õ’VÐÐRQ×ГÓÕÕÐRUÔÈ‹ˆÈ[[Y]žTÙ][™Üˆ”Õ’VÕSSQU–H‹—B‚‚�]\Ý ™š^\™J]]Ý\ÙOU�YJB™YˆÜ™\Ù]ÛØY\—ÜÝ]J[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú -H Oˆ›Û™N‚ˆˆˆ”™\Ù][Ù[HÛؘ[È[™ÛX\ˆÛ›ÝÛˆ[�ˆ˜\œÈ›Üˆ]\›Z[š\ÝXÈ�[œËˆˆˆ‚ˆ›ÜˆÙ^H[ˆÓWÑS•—ÒÑVT΂ˆ[ÛšÙ^\]Ú ™[[�ŠÙ^K˜Z\Ú[™ÏQ˜[ÙJBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ØØXÚY‹›Û™JBˆ[ÛšÙ^\]Ú œÙ]]ŠØY\‹—ÛÝ™\œšYH‹›Û™JB‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈÜ™XYÚœÛÛ—ÛÝ™\œšY\ÂˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛZ\ÜÚ[™×Ùš[J\Ü]ˆ] -H Oˆ›Û™N‚ˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê\Ü] È››ÜKšœÛÛˆŠHOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\רÛÜœ�\ÚœÛÛŠ\Ü]ˆ] -H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -žÛ›Ý˜[YœÛÛˆ‹[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Û›Û—ÙXÝÙ[�Š\Ü]ˆ] -H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ››Ý‹˜H‹™XÝ—_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÛX\×Ý×Û™\ÝYÜÙ][™ÜÊ\Ü]ˆ] -H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -ˆœÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ›^K[[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH�]‹N‹ˆ -Bˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHˆ›HŽˆÈ›[Ù[Žˆ›^K[[Ù[ŸKˆš[�Yܘ][ۜȎˆÈœ\œ^]WØ\WÚÙ^HŽˆœÈŸKˆB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×ÜÚÚ\×ÚÙ^\ר[™XYWÚ[—Ù[�š\›ÛŠˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹™œ›ÛKY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆÈ[�ˆÚ[œÈ OˆH”ÓÓˆ˜[YH\È›ÝÝ\™˜XÙY\È[ˆ[š]ÝØ\™Ë‚ˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[�—ÝÚ[œ×ØXÜ›ÜÜ×ÙšY[Ø[X\Ù\ʈ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈ\WÚÙ^H™\ÛÛ™\Èœ›ÛH[X\ÐÚÚXÙ\Ê“WÐTWÒÑVH‹“ÔS�RWÐTWÒÑVHŠKˆH[�‚ˆÈÙ]ÈÛ™H[X\ÈÚ[HH\œÚ\ÝYš[HÛÈ[›Ý\‹ˆ[�ˆ]\ÝÝ[Ú[‹ÛˆÈHÝ[Hš[H˜[YH]\ݛݙHÝ\™˜XÙY\È[ˆ[š]ÝØ\™È -ÚXÚÝ]˜[šÜÈ[�ŠK‚ˆ[ÛšÙ^\]Ú œÙ][�Š“ÔS�RWÐTWÒÑVH‹œÚËY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ“WÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ù[�—ÝÚ[œ×ØØ\ÙWÚ[œÙ[œÚ]]™[Jˆ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]ÚŠH Oˆ›Û™N‚ˆÈÙ][™ÜÈ\ÙHØ\ÙWÜÙ[œÚ]]™OQ˜[ÙKÛÈHÝÙ\˜Ø\ÙH[�ˆ˜\ˆ[ÛÈÛÝ[�È\ÈÙ] ‚ˆ[ÛšÙ^\]Ú œÙ][�ŠœÝš^ÛH‹™œ›ÛKY[�ˆŠBˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™œ›ÛKYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHßB‚‚™Yˆ\ÝÜ™XYÚœÛÛ—ÛÝ™\œšY\×Ý\Ù\×ÚœÛÛ—ÝÚ[—Û›×Ø[X\×Ú[—Ù[�š\›ÛŠ\Ü]ˆ] -H Oˆ›Û™N‚ˆÈ›È[X\ÈÙˆ\WÚÙ^H\ÈÙ][ˆH[�š\›Û›Y[� OˆHš[H˜[YH\È\ÙY ]™[‚ˆÈÚ[ˆ]\ÈÝÜ™Y[™\ˆH›Û‹Yš\œÝ[X\Ë‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ“ÔS�RWÐTWÒÑVHŽˆœÚËYš[HŸ_JK[˜ÛÙ[™ÏH�]‹NŠBˆ\ÜÙ\�ØY\‹—Ü™XYÚœÛÛ—ÛÝ™\œšY\Ê] -HOHÈ›HŽˆȘ\WÚÙ^HŽˆœÚËYš[HŸ_B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\×Ü™Z™XÝ×ÜÝX—Û›ÝXÙWݘ[Y\Ê -H Oˆ›Û™N‚ˆÚ]]\Ý œ˜Z\Ù\ʘ[Y][Û‘\œ›ÜŠN‚ˆÛÛ�^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏM� -B‚‚™Yˆ\ÝÝÛÛÛÝ]]ÛX^Øž]\רXØÙ\×Ù›ÛÜŠ -H Oˆ›Û™N‚ˆ\ÜÙ\�ÛÛ�^Ù][™ÜÊÕ’VÕÓÓÓÕUUÓPVЖUTÏLL � -K�ÛÛÛÝ]]ÛX^Øž]\ÈOH L �‚‚™Yˆ\ÝÜ�[�[YWØØZYר›ÛÝÝØZ]Ùœ›ÛWÙ[�Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú -H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÐÐRQ×ГÓÕÕÐRUÔÈ‹ŽL ŠB‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ -Kœ�[�[YK˜ØZYר›ÛÝÝØZ]ÜÈOHL ‚‚™Yˆ\ÝÜ�[�[YWÚ[XYÙWÜ[ÜÛXÞWÙœ›ÛWÙ[�Š[ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú -H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÒSPQÑWÔSÔÓPÖH‹›™]™\ˆŠB‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ -Kœ�[�[YKš[XYÙWÜ[ÜÛXÞHOH›™]™\ˆ‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈØ[X\Ù\×ٛ܂ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ[X\Ù\×ٛܗÜÚ[\WØ[X\Ê -H Oˆ›Û™N‚ˆš[™›ÈHšY[[™›Ê[X\ÏH”ÒSTWÐSPTÈŠBˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHÈ”ÒSTWÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×ٛܗØ[X\רÚÚXÙ\Ê -H Oˆ›Û™N‚ˆš[™›ÎˆšY[[™›ÈHšY[ -È\NˆYۛܙVØ\ÜÚYÛ›Y[�BˆY˜][S›Û™Kˆ˜[Y][Û—Ø[X\ÏP[X\ÐÚÚXÙ\Ê‘’T”Õ‹”ÑPÓÓ‘ŠKˆ -Bˆ\ÜÙ\�Ø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[�Bˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊š[™›ÊHOHȔ՗ÐSPTÈ—B‚‚™Yˆ\ÝØ[X\Ù\×Ù›Ü—Û›×Ø[X\Ê -H Oˆ›Û™N‚ˆ\ÜÙ\�ØY\‹—Ø[X\Ù\×ٛ܊šY[[™›Ê -JHOH×B‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\WØÛÛ™šY×ÛÝ™\œšYH -ÈØYÜÙ][™ÜÈ›Ý[™ ]š\ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝØ\WÛÝ™\œšYWØ[™ÛØYÜÙ][™Ü×Ü›Ý[™Ýš\ -\Ü]ˆ] -H Oˆ›Û™N‚ˆ]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆ] �Üš]WÝ^ -ˆœÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆœ›Ý[™ ]š\ [[Ù[‹”T”VUWÐTWÒÑVHŽˆœÈŸ_JKˆ[˜ÛÙ[™ÏH�]‹N‹ˆ -B‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ] -BˆÙ][™ÜÈHØY\‹›ØYÜÙ][™ÜÊ -B‚ˆ\ÜÙ\�Ù][™ÜË›K›[Ù[OHœ›Ý[™ ]š\ [[Ù[‚ˆ\ÜÙ\�Ù][™ÜËš[�Yܘ][ۜ˜\œ^]WØ\WÚÙ^HOHœÈ‚ˆÈÙXÛÛ™Ø[\ÈY[[Ú^™Y OˆØ[YHØš™XÝ ‚ˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ -H\ÈÙ][™Ü‚‚™Yˆ\ÝØ\WØÛÛ™šY×ÛÝ™\œšYWÚ[�˜[Y]\רØXÚJ\Ü]ˆ] -H Oˆ›Û™N‚ˆš\œÝH\Ü] È™š\œÝ šœÛÛˆ‚ˆš\œÝ �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆ™š\œÝ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH�]‹NŠBˆÙXÛÛ™H\Ü] ÈœÙXÛÛ™ šœÛÛˆ‚ˆÙXÛÛ™ �Üš]WÝ^ -œÛÛ‹™[\ÊÈ™[�ˆŽˆÈ”Õ’VÓHŽˆœÙXÛÛ™ [[Ù[Ÿ_JK[˜ÛÙ[™ÏH�]‹NŠB‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJš\œÝ -Bˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ -K›K›[Ù[OH™š\œÝ [[Ù[‚‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJÙXÛÛ™ -Bˆ\ÜÙ\�ØY\‹›ØYÜÙ][™ÜÊ -K›K›[Ù[OHœÙXÛÛ™ [[Ù[‚‚‚ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKHˆÈ\œÚ\ÝØÝ\œ™[�ˆÈ KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKH‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[�ÝÜš]\×Ù[�—Ø›ØÚÊ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú -H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ÈœÝXˆˆ ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù] -B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[� +"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence.""" -B‚ˆ\ÜÙ\�\™Ù] ™^\ÝÊ -Bˆ\ÜÙ\�œÛÛ‹›ØYÊ\™Ù] œ™XYÝ^ -[˜ÛÙ[™ÏH�]‹NŠJHOHˆ™[�ˆŽˆÈ”Õ’VÓHŽˆœ\œÚ\ÝY [[Ù[ŸBˆB‚‚™Yˆ\ÝÜ\œÚ\ÝØÝ\œ™[�ÜÙ]×Ì Œ Û[ÙJ\Ü]ˆ] [ÛšÙ^\]Úˆ]\Ý “[ÛšÙ^T]Ú -H Oˆ›Û™N‚ˆ[ÛšÙ^\]Ú œÙ][�ДՒVÓH‹œ\œÚ\ÝY [[Ù[ŠBˆ\™Ù]H\Ü] ȘÛKXÛÛ™šYËšœÛÛˆ‚ˆØY\‹˜\WØÛÛ™šY×ÛÝ™\œšYJ\™Ù] -B‚ˆØY\‹œ\œÚ\ÝØÝ\œ™[� +from __future__ import annotations -B‚ˆ\ÜÙ\�\™Ù] œÝ] +import json +from typing import TYPE_CHECKING -KœÝÛ[ÙH ˆ ÍÍÍÈOH ÍŒ  \ No newline at end of file +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 diff --git a/tests/test_image_pull_policy.py b/tests/test_image_pull_policy.py index a9184833..3c46d46f 100644 --- a/tests/test_image_pull_policy.py +++ b/tests/test_image_pull_policy.py @@ -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