mirror of
https://github.com/usestrix/strix.git
synced 2026-10-16 04:28:23 +00:00
Add identity token sources for Anthropic Workload Identity Federation beyond a token file: - ANTHROPIC_IDENTITY_TOKEN_COMMAND: a command run on every exchange whose stdout is the JWT (run without a shell; errors never include stdout). Saved to the Strix config like the other ANTHROPIC_* settings. - `strix auth login entra [--manual] [--tenant ID] [--app ID]`: PKCE sign-in as a Microsoft Entra ID user, using the audience app as its own public client so the stored refresh token only reaches that audience. Tokens are refreshed automatically; defaults come from STRIX_ENTRA_TENANT_ID / STRIX_ENTRA_APP_ID. Source order: token file, command, literal token, Entra sign-in, GitHub Actions OIDC. `strix auth status` and `logout` now cover both ChatGPT and Entra, and `strix auth test [--config PATH]` runs one exchange with the source a scan would use, reporting the source, claims and result without printing any token. Docs: new "Anthropic via Microsoft Entra ID" guide (Entra and Console setup, Entra users on any host, Azure VM managed identities, everyday use, security, troubleshooting), plus the WIF settings in the configuration reference and `strix auth` in the CLI reference.
239 lines
8.6 KiB
Python
239 lines
8.6 KiB
Python
"""Strix application settings — pydantic-settings powered."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import AliasChoices, Field, field_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh", "max"]
|
|
ApiType = Literal["responses", "chat_completions"]
|
|
|
|
DEFAULT_MAX_TURNS = 500
|
|
|
|
|
|
def _lowercase(value: object) -> object:
|
|
"""Enum-like env values are matched case-insensitively (``None`` is ``none``)."""
|
|
return value.strip().lower() if isinstance(value, str) else value
|
|
|
|
|
|
_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_type: ApiType | None = Field(
|
|
default=None,
|
|
validation_alias=AliasChoices("STRIX_API_TYPE", "STRIX_FORCE_API"),
|
|
description="Force 'responses' or 'chat_completions' API path",
|
|
)
|
|
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",
|
|
)
|
|
# Providers cache prompts in fixed-size token blocks, so a fully cached prompt
|
|
# can read back up to a block short. 128 covers the largest common size
|
|
# (OpenAI; DeepSeek and GLM use 64, vLLM defaults to 16).
|
|
cache_block_tokens: int = Field(default=128, ge=1, alias="STRIX_CACHE_BLOCK_TOKENS")
|
|
openrouter_sticky_sessions: bool = Field(
|
|
default=False,
|
|
alias="STRIX_OPENROUTER_STICKY_SESSIONS",
|
|
)
|
|
disable_streaming: bool = Field(
|
|
default=False,
|
|
alias="LLM_DISABLE_STREAMING",
|
|
)
|
|
timeout: int = Field(default=300, alias="LLM_TIMEOUT")
|
|
preflight_timeout: int = Field(default=30, ge=1, alias="LLM_PREFLIGHT_TIMEOUT")
|
|
stream_idle_timeout: int = Field(default=300, ge=0, alias="LLM_STREAM_IDLE_TIMEOUT")
|
|
stream_first_event_timeout: int = Field(
|
|
default=120, ge=0, alias="LLM_STREAM_FIRST_EVENT_TIMEOUT"
|
|
)
|
|
stream_total_timeout: int = Field(default=600, ge=0, alias="LLM_STREAM_TOTAL_TIMEOUT")
|
|
max_tool_calls_per_turn: int = Field(
|
|
default=32,
|
|
ge=0,
|
|
alias="LLM_MAX_TOOL_CALLS_PER_TURN",
|
|
)
|
|
|
|
@field_validator("api_type", "reasoning_effort", mode="before")
|
|
@classmethod
|
|
def _normalize_case(cls, value: object) -> object:
|
|
return _lowercase(value)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
@field_validator("reasoning_effort", mode="before")
|
|
@classmethod
|
|
def _normalize_case(cls, value: object) -> object:
|
|
return _lowercase(value)
|
|
|
|
|
|
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",
|
|
)
|
|
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")
|
|
|
|
|
|
WebSearchProvider = Literal["auto", "perplexity", "exa"]
|
|
ExaSearchType = Literal["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"]
|
|
|
|
|
|
class IntegrationSettings(BaseSettings):
|
|
model_config = _BASE_CONFIG
|
|
|
|
perplexity_api_key: str | None = Field(
|
|
default=None,
|
|
alias="PERPLEXITY_API_KEY",
|
|
repr=False,
|
|
)
|
|
exa_api_key: str | None = Field(
|
|
default=None,
|
|
alias="EXA_API_KEY",
|
|
repr=False,
|
|
)
|
|
web_search_provider: WebSearchProvider = Field(
|
|
default="auto",
|
|
alias="STRIX_WEB_SEARCH_PROVIDER",
|
|
)
|
|
exa_search_type: ExaSearchType = Field(
|
|
default="auto",
|
|
alias="STRIX_EXA_SEARCH_TYPE",
|
|
)
|
|
exa_num_results: int = Field(
|
|
default=5,
|
|
ge=1,
|
|
le=100,
|
|
alias="STRIX_EXA_NUM_RESULTS",
|
|
)
|
|
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 AnthropicWifSettings(BaseSettings):
|
|
"""Anthropic Workload Identity Federation, under the Anthropic SDKs' own env names.
|
|
|
|
``ANTHROPIC_IDENTITY_TOKEN_COMMAND`` is Strix-only: a command whose stdout is
|
|
the JWT. The literal ``ANTHROPIC_IDENTITY_TOKEN`` JWT is read straight from
|
|
the environment instead, so it never lands in the persisted config file.
|
|
"""
|
|
|
|
model_config = _BASE_CONFIG
|
|
|
|
federation_rule_id: str | None = Field(default=None, alias="ANTHROPIC_FEDERATION_RULE_ID")
|
|
organization_id: str | None = Field(default=None, alias="ANTHROPIC_ORGANIZATION_ID")
|
|
service_account_id: str | None = Field(default=None, alias="ANTHROPIC_SERVICE_ACCOUNT_ID")
|
|
workspace_id: str | None = Field(default=None, alias="ANTHROPIC_WORKSPACE_ID")
|
|
identity_token_file: str | None = Field(default=None, alias="ANTHROPIC_IDENTITY_TOKEN_FILE")
|
|
identity_token_command: str | None = Field(
|
|
default=None, alias="ANTHROPIC_IDENTITY_TOKEN_COMMAND"
|
|
)
|
|
# Defaults for `strix auth login entra`: the Entra tenant, and the app registration
|
|
# that is both the OAuth client and the token audience.
|
|
entra_tenant_id: str | None = Field(default=None, alias="STRIX_ENTRA_TENANT_ID")
|
|
entra_app_id: str | None = Field(default=None, alias="STRIX_ENTRA_APP_ID")
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = _BASE_CONFIG
|
|
|
|
llm: LlmSettings = Field(default_factory=LlmSettings)
|
|
dedupe: DedupeSettings = Field(default_factory=DedupeSettings)
|
|
anthropic_wif: AnthropicWifSettings = Field(default_factory=AnthropicWifSettings)
|
|
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)
|