chore: import upstream snapshot with attribution
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:10:45 +08:00
commit 4b6817381b
3933 changed files with 525247 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(uv run *)",
"Bash(python *)",
"Bash(python3 *)",
"Bash(python3*)",
"PowerShell(uv run *)",
"PowerShell(python *)",
"PowerShell(python3 *)"
]
}
}
+45
View File
@@ -0,0 +1,45 @@
---
description: Code style and conventions
alwaysApply: true
---
# Code Style
## Formatting
- 100-character line length (enforced by ruff and `.editorconfig`)
- 4-space indentation for Python
- 2-space indentation for YAML, JSON, TOML
- Tabs for Makefiles
## Python Conventions
- Use `from __future__ import annotations` for forward references
- Absolute imports only: `from tools.base import BaseTool`
- Type hints on all function parameters and return types
- `TypedDict` for graph state, Pydantic `StrictConfigModel` for configs
- One clear purpose per file (separation of concerns)
## Tooling
- **Linter:** ruff (rules: E, W, F, I, B, C4, UP, ARG, SIM)
- **Type checker:** mypy (Python 3.13 target, `warn_return_any`)
- **Formatter:** ruff format (Black-compatible)
- **Python version:** >=3.12 (tooling targets 3.13)
## Quality Commands
Ruff and mypy run over the source packages (`config core gateway integrations
platform surfaces tools`) plus `tests/`; the Makefile keeps the canonical path
list in `PYTHON_SOURCE_PATHS`.
```bash
make lint # ruff check
make format-check # ruff format --check (read-only; CI enforces this)
make format # ruff format (write fixes locally)
make typecheck # mypy
make test-cov # pytest with coverage
make check # lint + format-check + typecheck + test-full
```
Before opening a PR, run `make format-check` (or `make check`) in addition to `make lint`.
+30
View File
@@ -0,0 +1,30 @@
---
description: Investigation pipeline architecture and stage development
globs:
- core/orchestration/**
- core/runtime/**
- core/domain/state/**
---
# Investigation pipeline
## Coordinator
`core/orchestration/pipeline.py` runs **resolve integrations → extract alert → investigation agent → deliver**.
`core/orchestration/entrypoints.py` exposes `run_investigation` and async streaming helpers.
## Key packages
- `core/orchestration/node/` for pipeline stages (extract, investigate, diagnose, publish findings)
- `core/runtime/` for shared LLM tool-calling runtime primitives
- `interactive_shell/chat/` for live REPL conversational assistant
- `core/domain/state/agent_state.py` for `AgentState` / `InvestigationState`
- `core/domain/state/runtime_slices.py` for investigation slice TypedDicts and stage ownership
## Conventions
- Prefer `@traceable` from `platform.observability.tracing` on externally-visible orchestration helpers.
- Stages read full state and return **partial dict** updates.
- Use `get_tracker()` for CLI progress when appropriate.
- Keep new persisted keys in the state `TypedDict` and any matching validators.
+43
View File
@@ -0,0 +1,43 @@
---
description: Integration client patterns
globs:
- integrations/**
---
# Integration Clients
## Directory Structure
All vendor integrations live under `integrations/<vendor>/`. There is no
`services/` directory — it was removed in the V0.2 Phase 1 refactor.
```
integrations/
├── grafana/
│ └── tools/ # Vendor tools (moved from tools/ in T-3)
├── datadog/
│ ├── tools/
│ └── correlation/ # Vendor-specific sub-packages where needed
├── <vendor>/ # One directory per vendor (60+ vendors)
│ └── tools/ # Vendor tools live here
├── opensre_mcp.py # MCP server implementation
└── ...
```
## Adding a New Integration
1. Create a directory `integrations/<vendor>/`
2. Add client code (connection, auth, API methods) directly in the vendor dir
3. Create a tool under `integrations/<vendor>/tools/<tool_name>/` that uses the client
4. Wire availability into `resolve_integrations` node if the integration requires
runtime discovery
5. Add the vendor to `EvidenceSource` literal in `core/domain/types/evidence.py`
if it is a new source type
## Conventions
- Clients should be stateless or use context managers for connection lifecycle
- Return `{"success": bool, "data": ..., "error": ...}` from client methods
- Keep API-specific logic in the client; the tool handles param extraction and
result formatting
- MCP tools are registered via `opensre-mcp` entry point in `pyproject.toml`
- All directory names use **snake_case** — no PascalCase
+20
View File
@@ -0,0 +1,20 @@
---
description: Always include live turn tests
alwaysApply: true
---
# Live Turn Test Policy
- When validating interactive-shell turn scenarios, run the live suite by default.
- Do not exclude live turn tests with deselection filters like `-k "not live_llm"`.
- Never "fix" turn scenario failures by forcing deterministic command paths, bypassing live
planner decisions, or disabling fail-closed/live checks.
- Scenario failures should be resolved by correcting planner/tool behavior or by intentional
fixture updates approved as behavior changes.
- Prefer full live turn commands such as:
- `uv run python -m pytest interactive_shell/harness/tests/test_turn_scenarios.py`
- For fast local iteration you MAY narrow the suite with the opt-in `--turn-select`
flag / `TURN_SELECT` env var (`complex:N` for the most complex scenarios, `sample:N`
for a random sample). This still runs live — it is not a `-k "not live_llm"`-style
bypass. The full suite remains the default and the required validation pass; never
set the selection in CI, and never use it to skip a scenario that is failing.
+43
View File
@@ -0,0 +1,43 @@
---
description: Pre-commit quality checklist and PR standards
alwaysApply: true
---
# Quality Standards
## Before Every PR
All three must pass — CI blocks merging if any fail:
```bash
make lint # ruff: style and import checking
make typecheck # mypy: type annotation checking
make test-cov # pytest: tests with coverage report
```
## PR Checklist
- Link to the relevant GitHub issue (`Fixes #123`)
- All local checks pass
- Tests added for bug fixes and new features
- Documentation updated if behavior changed
- Self-reviewed your own code
- Edge cases considered
- Breaking changes called out explicitly
## AI-Assisted Code
If AI tools were used to generate code:
- Review every line (not just skim)
- Understand the logic and can explain it
- Test edge cases
- Match project conventions
- Verify tests pass
## Code Quality Principles
- Clarity over cleverness
- DRY: extract common patterns
- Strong typing on all function signatures
- One responsibility per function/class
- Comments explain "why", not "what"
+40
View File
@@ -0,0 +1,40 @@
---
description: Testing conventions and patterns
globs:
- tests/**
- gateway/tests/**
- "**/*_test.py"
- "**/test_*.py"
---
# Testing Conventions
## Running Tests
- `make test-cov` — standard test suite with coverage (excludes synthetic + k8s simulation)
- `make test-rca` — real alert end-to-end tests
- `make test-full` — full pytest run
- `make test-synthetic` — synthetic LLM-based RCA tests (separate CI workflow)
## File Placement
Both patterns are valid and auto-discovered by pytest:
- `tools/registry_test.py` — co-located next to source
- `tests/tools/test_registry.py` — under `tests/` directory
## Markers
Apply markers to categorize tests:
- `@pytest.mark.integration` — requires external services
- `@pytest.mark.synthetic` — LLM-based RCA tests (excluded from `make test-cov`)
- `@pytest.mark.e2e` — end-to-end scenarios
- `@pytest.mark.axis2` — axis2 tests
## Conventions
- Bug fixes must include a test that would have caught the bug
- New features must have corresponding tests
- Aim for >80% code coverage
- Root `conftest.py` loads `.env` and disables keyring for isolation
- `tests/e2e/` contains scenario-specific folders (Prefect, Lambda, Kubernetes, etc.)
- Tests calling `get_registered_tools()` should call `clear_tool_registry_cache()` in fixtures
+89
View File
@@ -0,0 +1,89 @@
---
description: Tool development patterns for investigation and chat tools
globs:
- tools/**
- integrations/**/tools/**
---
# Tool Development
## Where tools live
After the V0.2 Phase 1 refactor there are two locations:
- `integrations/<vendor>/tools/` — vendor-specific tools (one folder per vendor)
Example: `integrations/grafana/tools/`
- `tools/` — non-vendor tools only (fleet monitoring, SRE guidance, investigation
helpers, etc.)
Do NOT create new vendor tool directories directly under `tools/`.
## Two Registration Patterns
### 1. Function + `@tool` decorator (preferred for simple tools, required for vendor tools)
- Vendor tools: add `@tool`-decorated functions in `integrations/<vendor>/tools/__init__.py`
- Non-vendor tools: single file under `tools/`
```python
from tools.tool_decorator import tool
@tool(
name="my_tool_name",
source="grafana", # Required: EvidenceSource literal
description="What this tool does.",
use_cases=["When to use it"],
requires=["api_key"],
input_schema={...}, # Optional: inferred from signature if omitted
is_available=my_check_fn, # (sources: dict) -> bool
extract_params=my_extract, # (sources: dict) -> dict
surfaces=("investigation",), # Default; add "chat" to expose in chat mode
)
def my_tool_name(param: str) -> dict:
...
```
### 2. `BaseTool` subclass (for complex non-vendor tools with helpers)
Package directory: `tools/<tool_name>/__init__.py`. Instantiate at module level.
```python
from tools.base import BaseTool
class MyToolName(BaseTool):
name = "my_tool_name"
source = "grafana"
description = "..."
input_schema = {
"type": "object",
"properties": {...},
"required": [...],
}
use_cases = [...]
requires = [...]
outputs = {"field": "description"}
def is_available(self, sources: dict) -> bool: ...
def extract_params(self, sources: dict) -> dict: ...
def run(self, param: str, **_kwargs) -> dict: ...
my_tool_name = MyToolName()
```
## Rules
- `source` is **required** — must be a valid `EvidenceSource` literal from
`core/domain/types/evidence.py`
- Tool packages under `tools/` use **snake_case** directory names — no PascalCase, no `Tool` suffix in the dir name
- The registry auto-discovers tools via `pkgutil.iter_modules`
- Do NOT add your module to `_SKIP_MODULE_NAMES` in `registry.py`
- Module names ending in `_test` are auto-skipped by the registry
- `surfaces` defaults to `("investigation",)` — add `"chat"` explicitly if needed
- For `BaseTool`: instantiate the class at module level so the registry can find it
- Return `dict` from `run()` with an `"error"` key on failure, structured data on success
## Valid `EvidenceSource` Values
`storage`, `batch`, `tracer_web`, `cloudwatch`, `aws_sdk`, `knowledge`, `grafana`,
`datadog`, `honeycomb`, `coralogix`, `eks`, `github`, `sentry`, `google_docs`,
`vercel`, `opsgenie`, `elasticsearch`
+7
View File
@@ -0,0 +1,7 @@
{
"plugins": {
"posthog": {
"enabled": true
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"setup-worktree": [
"make install"
]
}
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.13-bookworm
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git make sudo \
&& groupadd --gid "${USER_GID}" "${USERNAME}" \
&& useradd --uid "${USER_UID}" --gid "${USER_GID}" -m "${USERNAME}" --shell /bin/bash \
&& echo "${USERNAME} ALL=(root) NOPASSWD:ALL" > "/etc/sudoers.d/${USERNAME}" \
&& chmod 0440 "/etc/sudoers.d/${USERNAME}" \
&& rm -rf /var/lib/apt/lists/*
+51
View File
@@ -0,0 +1,51 @@
{
"name": "OpenSRE",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}",
"PATH": "${containerWorkspaceFolder}/.venv-devcontainer/bin:${containerEnv:PATH}"
},
"containerEnv": {
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONUTF8": "1"
},
"forwardPorts": [
8000
],
"portsAttributes": {
"8000": {
"label": "OpenSRE health app",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"ms-azuretools.vscode-docker",
"editorconfig.editorconfig"
],
"settings": {
"python.defaultInterpreterPath": "${containerWorkspaceFolder}/.venv-devcontainer/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": [
"tests"
]
}
}
},
"postCreateCommand": "python -m venv --clear .venv-devcontainer && .venv-devcontainer/bin/python -m pip install -e '.[dev]'",
"remoteUser": "vscode"
}
+42
View File
@@ -0,0 +1,42 @@
# Ignore node_modules and other dependency directories
node_modules
bower_components
vendor
# Ignore logs and temporary files
*.log
*.tmp
*.swp
# Ignore .env files and other environment files
.env
.env.*
*.local
# Ignore git-related files
.git
.gitignore
# Ignore Docker-related files and configs
.dockerignore
docker-compose.yml
# Ignore build and cache directories
dist
build
.cache
__pycache__
# Ignore IDE and editor configurations
.vscode
.idea
*.sublime-project
*.sublime-workspace
.DS_Store # macOS-specific
# Ignore test and coverage files
coverage
*.coverage
*.test.js
*.spec.js
tests
+21
View File
@@ -0,0 +1,21 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
max_line_length = 100
[*.{yml,yaml}]
indent_size = 2
[*.{json,toml}]
indent_size = 2
[Makefile]
indent_style = tab
+66
View File
@@ -0,0 +1,66 @@
# Minimal .env for EC2 deployment.
# Copy to .env in repo root (or export vars) before running:
# make deploy
# make destroy
#
# Provisions one EC2 instance with opensre-web and opensre-gateway containers.
# NEVER commit a file with real credentials.
# See also: .env.example for the full application env reference.
# ─── AWS SDK (EC2 / ECR / SSM provisioning) ──────────────────────────────────
# Use static keys or a cross-account role (not both).
# Option A: static credentials
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
# AWS_SESSION_TOKEN=optional_session_token
# Option B: cross-account role
# AWS_REGION=us-east-1
# AWS_ROLE_ARN=arn:aws:iam::123456789012:role/YourDeployRole
# AWS_EXTERNAL_ID=optional_external_id
# ─── Gateway deploy (make bake-gateway / make deploy-gateway) ───────────────
# Optional: git ref (commit SHA, tag, or branch) to bake into the gateway AMI.
# Defaults to the local git HEAD commit SHA at bake time.
# The ref must exist on GitHub — push your branch/commit before baking.
# OPENSRE_GATEWAY_GIT_REF=main
# Optional: skip baking by providing an existing AMI id directly.
# OPENSRE_GATEWAY_AMI_ID=ami-0abc123456789def0
# Optional: set to 1 to also deregister the AMI when running make destroy-gateway.
# OPENSRE_GATEWAY_DESTROY_PURGE_AMI=1
# Optional: personal suffix to isolate your resources from teammates in a shared AWS account.
# Set to your username or initials (e.g. joe, alice). All AWS resources (EC2 instance,
# IAM role/profile, ECR repo, container names) will be named with this suffix.
# Without this, all developers share the same resource names and will conflict.
# OPENSRE_STACK_SUFFIX=joe
# Optional: fail instead of auto-destroying when a prior deploy is still recorded.
# OPENSRE_DEPLOY_ABORT_IF_EXISTS=1
# Optional: override which key pair is attached to the instance for SSH debug access.
# Leave unset — SSM Run Command is used for gateway checks (no key pair required).
# EC2_KEY_NAME=your-key-pair-name
# ─── Telegram (gateway container) ─────────────────────────────────────────────
TELEGRAM_BOT_TOKEN=your_bot_token_here
# Comma-separated Telegram user IDs permitted to DM the bot.
# Get your ID from @userinfobot.
TELEGRAM_ALLOWED_USERS=123456789
# ─── LLM provider ─────────────────────────────────────────────────────────────
# Set one of: openai | anthropic | bedrock
LLM_PROVIDER=openai
# OpenAI
OPENAI_API_KEY=sk-...
# Anthropic (alternative)
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# ANTHROPIC_MODEL=claude-sonnet-4-5
+629
View File
@@ -0,0 +1,629 @@
# OpenSRE example environment
#
# Fast path:
# 1. Set `LLM_PROVIDER` and the matching API key below.
# 2. Run `opensre onboard` for guided local setup.
# 3. Configure one integration with `opensre integrations setup <service>`.
# 4. Verify with `opensre health` and `opensre integrations verify <service>`.
#
# `~/.config/opensre/integrations.json` is the preferred local integration store.
# The env vars below are still supported as fallback/direct configuration.
# --- Most important ---------------------------------------------------------
# Provider used for LLM calls. Common values: anthropic, openai, openrouter,
# deepseek, gemini, nvidia, minimax, bedrock, ollama, codex, claude-code,
# opencode, kimi, copilot, antigravity-cli.
LLM_PROVIDER=anthropic
# Codex CLI works for `opensre investigate` after `codex login`.
# Leave CODEX_MODEL empty to use the CLI's currently configured model.
CODEX_MODEL=
CODEX_BIN=
# Claude Code CLI works for `opensre investigate` after `claude login` or setting ANTHROPIC_API_KEY.
# Install: npm i -g @anthropic-ai/claude-code
# Leave CLAUDE_CODE_MODEL empty to use the CLI's currently configured model.
CLAUDE_CODE_MODEL=
CLAUDE_CODE_BIN=
# Gemini CLI works for `opensre investigate` after `gemini` auth setup.
# Install: npm i -g @google/gemini-cli
# Leave GEMINI_CLI_MODEL empty to use the CLI's configured default model.
GEMINI_CLI_MODEL=
GEMINI_CLI_BIN=
# Antigravity CLI (agy) works for `opensre investigate` after running `agy`
# (browser OAuth on first run; token cached by OS keyring).
# Install: curl -fsSL https://antigravity.google/cli/install.sh | bash
# Leave ANTIGRAVITY_CLI_MODEL empty to use agy's currently configured model.
# Note: ANTIGRAVITY_CLI_MODEL is forward-compat; agy v1.0.2 does not yet expose
# --model in headless mode. Switch models interactively via `/models` inside agy.
ANTIGRAVITY_CLI_MODEL=
ANTIGRAVITY_CLI_BIN=
# ANTIGRAVITY_CLI_TIMEOUT_SECONDS=
# OpenCode CLI works for `opensre investigate` after `opencode auth login`.
# Leave OPENCODE_MODEL empty to use the CLI's currently configured model
OPENCODE_MODEL=
OPENCODE_BIN=
# Cursor Agent CLI
# Leave CURSOR_MODEL empty to use the CLI's currently configured model.
CURSOR_MODEL=
CURSOR_BIN=
# Kimi Code CLI works for `opensre investigate` after `kimi login`.
# Leave KIMI_MODEL empty to use the CLI's currently configured model.
KIMI_MODEL=
KIMI_BIN=
KIMI_API_KEY=
# KIMI_SHARE_DIR=~/.kimi
# GitHub Copilot CLI works for `opensre investigate` after running `copilot`
# and authenticating with the interactive `/login` slash command.
# Install: npm i -g @github/copilot
# Leave COPILOT_MODEL empty to use the CLI's currently configured model.
COPILOT_MODEL=
COPILOT_BIN=
# Optional auth fallbacks (only used when no stored Copilot CLI login exists):
# COPILOT_GITHUB_TOKEN=
# GH_TOKEN=
# GITHUB_TOKEN=
# Optional config dir override (default: ~/.copilot):
# COPILOT_HOME=
# xAI Grok Build CLI works for `opensre investigate` after `grok login`,
# or set XAI_API_KEY for headless/CI use.
# Install: curl -fsSL https://x.ai/cli/install.sh | bash
# Leave GROK_CLI_MODEL empty to use the CLI's currently configured model.
GROK_CLI_MODEL=
GROK_CLI_BIN=
XAI_API_KEY=
# Optional xAI API base URL override.
# XAI_BASE_URL=
# Optional exec timeout in seconds (clamped 30-600; default 300).
# GROK_CLI_TIMEOUT_SECONDS=
# --- Pi coding tool (lets OpenSRE submit coding tasks to the Pi agent) ---
# Separate from using Pi as an LLM provider. This tool edits the working tree
# and returns a diff; it never commits or pushes. Mutating + approval-required,
# so it is OFF unless PI_CODING_ENABLED is truthy. Install: npm i -g @earendil-works/pi-coding-agent
# PI_CODING_ENABLED=1
# PI_CODING_MODEL=anthropic/claude-haiku-4-5
# PI_CODING_WORKSPACE=
# Optional per-task timeout in seconds (clamped 60-1800; default 600).
# PI_CODING_TIMEOUT_SECONDS=
# --- Fix a Sentry issue (paste a Sentry issue URL, Pi proposes the fix) ---
# Resolves the issue from Sentry (needs SENTRY_ORG_SLUG + SENTRY_AUTH_TOKEN below)
# and runs the Pi coding agent in the current repo. Returns a diff for review.
# OFF unless PI_ISSUE_FIX_ENABLED is truthy. Reuses the PI_CODING_MODEL /
# PI_CODING_WORKSPACE / PI_CODING_TIMEOUT_SECONDS settings.
# PI_ISSUE_FIX_ENABLED=1
# Maximum tokens for LLM responses (default: 4096).
LLM_MAX_TOKENS=4096
# LLM transport: unset or "sdk" for native vendor SDKs; "litellm" routes all API providers
# through LiteLLM (anthropic, openai, bedrock, and OpenAI-compatible endpoints).
# CLI-backed providers (codex, claude-code, etc.) are unaffected.
# OPENSRE_LLM_TRANSPORT=litellm
# Set the key for the provider you choose above.
ANTHROPIC_API_KEY=
ANTHROPIC_REASONING_MODEL=
ANTHROPIC_CLASSIFICATION_MODEL=
ANTHROPIC_TOOLCALL_MODEL=
OPENAI_API_KEY=
OPENAI_REASONING_MODEL=
OPENAI_CLASSIFICATION_MODEL=
OPENAI_TOOLCALL_MODEL=
# OpenRouter supports many hosted models behind one API key.
OPENROUTER_API_KEY=
OPENROUTER_MODEL=
OPENROUTER_REASONING_MODEL=
OPENROUTER_CLASSIFICATION_MODEL=
OPENROUTER_TOOLCALL_MODEL=
# DeepSeek official API (OpenAI-compatible endpoint).
DEEPSEEK_API_KEY=
DEEPSEEK_MODEL=
DEEPSEEK_REASONING_MODEL=
DEEPSEEK_CLASSIFICATION_MODEL=
DEEPSEEK_TOOLCALL_MODEL=
# Gemini uses the OpenAI-compatible endpoint in this project.
GEMINI_API_KEY=
GEMINI_MODEL=
GEMINI_REASONING_MODEL=
GEMINI_CLASSIFICATION_MODEL=
GEMINI_TOOLCALL_MODEL=
# NVIDIA NIM / hosted inference.
NVIDIA_API_KEY=
NVIDIA_MODEL=
NVIDIA_REASONING_MODEL=
NVIDIA_CLASSIFICATION_MODEL=
NVIDIA_TOOLCALL_MODEL=
# Amazon Bedrock — set `LLM_PROVIDER=bedrock` above. Uses the same AWS credential
# chain as the AWS integration block below (region, keys, or IAM role). No LLM API key.
BEDROCK_REASONING_MODEL=
BEDROCK_CLASSIFICATION_MODEL=
BEDROCK_TOOLCALL_MODEL=
# Ollama (local)
OLLAMA_MODEL=
OLLAMA_HOST=http://localhost:11434
# MiniMax
MINIMAX_API_KEY=
MINIMAX_MODEL=
MINIMAX_REASONING_MODEL=
MINIMAX_CLASSIFICATION_MODEL=
MINIMAX_TOOLCALL_MODEL=
# Groq
GROQ_API_KEY=
GROQ_MODEL=
GROQ_REASONING_MODEL=
GROQ_CLASSIFICATION_MODEL=
GROQ_TOOLCALL_MODEL=
# Azure OpenAI (routes via LiteLLM; model values are deployment names in your resource)
AZURE_OPENAI_BASE_URL=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_API_VERSION=2024-10-21
AZURE_OPENAI_REASONING_MODEL=
AZURE_OPENAI_CLASSIFICATION_MODEL=
AZURE_OPENAI_TOOLCALL_MODEL=
# --- First integrations to set up ------------------------------------------
# For a first real RCA run, one of Grafana / Datadog / Honeycomb / Coralogix
# is usually enough. Prefer `opensre integrations setup <service>` when local.
# Grafana
# For `make grafana-local-up`, use `http://localhost:3000` and any placeholder
# token such as `local`.
GRAFANA_READ_TOKEN=
GRAFANA_INSTANCE_URL=
GRAFANA_LOKI_DATASOURCE_UID=
GRAFANA_TEMPO_DATASOURCE_UID=
# Optional multi-instance override. When set, this takes precedence over the
# single-instance Grafana vars above.
# GRAFANA_INSTANCES='[
# {"name":"prod","tags":{"env":"prod"},"endpoint":"https://prod.grafana.net","api_key":"..."}
# ]'
GRAFANA_INSTANCES=
# Argo CD (read-only REST API integration)
# Use exactly one auth method: ARGOCD_AUTH_TOKEN/ARGOCD_TOKEN or username/password.
ARGOCD_BASE_URL=
ARGOCD_AUTH_TOKEN=
ARGOCD_TOKEN=
ARGOCD_USERNAME=
ARGOCD_PASSWORD=
ARGOCD_PROJECT=
ARGOCD_APP_NAMESPACE=
ARGOCD_VERIFY_SSL=true
# Multi-instance Argo CD (optional). When set, takes precedence over the
# single-instance ARGOCD_* vars above. See docs/multi-instance-integrations.mdx.
# ARGOCD_INSTANCES='[{"name":"prod","base_url":"https://argocd.example.com","bearer_token":"***","project":"default"}]'
ARGOCD_INSTANCES=
# Helm 3 (read-only CLI — list/status/history/get values/get manifest)
# Requires OSRE_HELM_INTEGRATION=1 (or true/yes) to activate from env.
OSRE_HELM_INTEGRATION=
HELM_PATH=helm
HELM_KUBE_CONTEXT=
HELM_KUBECONFIG=
HELM_NAMESPACE=
# Optional: cap manifest size from helm get manifest (integer, min 1024; default 600000).
# HELM_MANIFEST_MAX_CHARS=
# Datadog
DD_API_KEY=
DD_APP_KEY=
DD_SITE=datadoghq.com
# Optional multi-instance override.
# DD_INSTANCES='[{"name":"prod","api_key":"...","app_key":"...","site":"datadoghq.com"}]'
DD_INSTANCES=
# groundcover
# Read-only service-account token (Bearer). GROUNDCOVER_MCP_TOKEN is accepted as an alias.
GROUNDCOVER_API_KEY=
GROUNDCOVER_MCP_URL=https://mcp.groundcover.com/api/mcp
# Optional routing — only needed for multi-workspace / multi-backend accounts.
GROUNDCOVER_TENANT_UUID=
GROUNDCOVER_BACKEND_ID=
GROUNDCOVER_TIMEZONE=UTC
# Optional multi-instance override.
# GROUNDCOVER_INSTANCES='[{"name":"prod","api_key":"...","tenant_uuid":"...","backend_id":"..."}]'
GROUNDCOVER_INSTANCES=
# Honeycomb
# `HONEYCOMB_DATASET` can be a dataset slug or `__all__`.
HONEYCOMB_API_KEY=
HONEYCOMB_DATASET=__all__
HONEYCOMB_API_URL=https://api.honeycomb.io
# Optional multi-instance override.
# HONEYCOMB_INSTANCES='[{"name":"prod","api_key":"...","dataset":"__all__"}]'
HONEYCOMB_INSTANCES=
# Coralogix
CORALOGIX_API_KEY=
CORALOGIX_API_URL=https://api.coralogix.com
CORALOGIX_APPLICATION_NAME=
CORALOGIX_SUBSYSTEM_NAME=
# Optional multi-instance override.
# CORALOGIX_INSTANCES='[{"name":"prod","api_key":"...","base_url":"https://api.coralogix.com"}]'
CORALOGIX_INSTANCES=
# SigNoz (Query API — logs, metrics, traces)
# Local Docker stack: http://localhost:8080 (see docs/signoz.mdx)
# API key: Settings → Service Accounts → Keys
SIGNOZ_URL=
SIGNOZ_API_KEY=
# Grafana Tempo (standalone — direct HTTP API, no Grafana datasource proxy)
# Default local Tempo HTTP port is 3200. Auth is optional: provide either
# TEMPO_API_KEY (bearer token) OR TEMPO_USERNAME/TEMPO_PASSWORD (basic auth),
# not both. TEMPO_ORG_ID is only needed for multi-tenant deployments.
TEMPO_URL=
TEMPO_API_KEY=
TEMPO_USERNAME=
TEMPO_PASSWORD=
TEMPO_ORG_ID=
# Temporal (self-hosted only — talks to the Temporal HTTP API, not gRPC or Cloud)
# Point TEMPORAL_API_URL at the frontend HTTP port (default 7243), not the gRPC
# port (7233) or Web UI (8233). TEMPORAL_NAMESPACE defaults to "default".
# TEMPORAL_API_KEY is optional — leave empty for unauthenticated self-hosted clusters.
TEMPORAL_API_URL=
TEMPORAL_NAMESPACE=default
TEMPORAL_API_KEY=
# AWS
AWS_REGION=us-east-1
AWS_ROLE_ARN=
AWS_EXTERNAL_ID=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
# Optional multi-instance override.
# AWS_INSTANCES='[
# {"name":"prod","tags":{"account":"prod"},"role_arn":"arn:aws:iam::111:role/opensre"}
# ]'
AWS_INSTANCES=
# GitHub MCP
GITHUB_MCP_URL=
GITHUB_MCP_MODE=streamable-http
GITHUB_MCP_COMMAND=
GITHUB_MCP_ARGS=
GITHUB_MCP_AUTH_TOKEN=
GITHUB_MCP_TOOLSETS=repos,issues,pull_requests,actions,search
# Optional: 5-500, default 50.
# OPENSRE_GITHUB_MCP_REPO_PROBE_LIMIT=
# Sentry
# Runtime error monitoring for OpenSRE itself uses the project Sentry DSN constant.
# Optional: override for operator-side DSN rotation without rebuilding.
# OPENSRE_SENTRY_DSN=
SENTRY_ERROR_SAMPLE_RATE=1.0
SENTRY_TRACES_SAMPLE_RATE=1.0
OPENSRE_SENTRY_DISABLED=0
OPENSRE_SENTRY_LOGGING_DISABLED=0
# Tag value attached to Sentry events to identify how this process is deployed.
# Common values: railway, ec2, vercel, local. Defaults to "local" when unset.
# OPENSRE_DEPLOYMENT_METHOD=local
# Interactive-shell prompt logging (for local eval mining + PostHog LLM analytics).
# Set OPENSRE_PROMPT_LOG_DISABLED=1 to disable prompt/response logging entirely.
OPENSRE_PROMPT_LOG_DISABLED=0
# Disable only local JSONL prompt log file.
OPENSRE_PROMPT_LOG_LOCAL_DISABLED=0
# Redact prompt/response content (known token shapes) before local or PostHog
# sinks. On by default — set to 0 to log/send raw, unredacted content.
OPENSRE_PROMPT_LOG_REDACT=1
# Optional override for local prompt log path (default ~/.config/opensre/prompt_log.jsonl).
# OPENSRE_PROMPT_LOG_PATH=
# Sentry investigation integration
SENTRY_URL=https://sentry.io
SENTRY_ORG_SLUG=
SENTRY_PROJECT_SLUG=
SENTRY_AUTH_TOKEN=
# Slack delivery and Slack-thread investigation context
SLACK_WEBHOOK_URL=
SLACK_BOT_TOKEN=
# SLACK_ACCESS_TOKEN=
# --- Additional RCA integrations -------------------------------------------
# Alertmanager
ALERTMANAGER_URL=
ALERTMANAGER_BEARER_TOKEN=
ALERTMANAGER_USERNAME=
ALERTMANAGER_PASSWORD=
# MongoDB
MONGODB_CONNECTION_STRING=
MONGODB_DATABASE=
MONGODB_AUTH_SOURCE=admin
MONGODB_TLS=true
# Redis
REDIS_HOST=
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_DATABASE=0
REDIS_SSL=false
# MongoDB Atlas
MONGODB_ATLAS_PUBLIC_KEY=
MONGODB_ATLAS_PRIVATE_KEY=
MONGODB_ATLAS_PROJECT_ID=
MONGODB_ATLAS_BASE_URL=https://cloud.mongodb.com/api/atlas/v2
# PostgreSQL
POSTGRESQL_HOST=
POSTGRESQL_PORT=5432
POSTGRESQL_DATABASE=
POSTGRESQL_USERNAME=postgres
POSTGRESQL_PASSWORD=
POSTGRESQL_SSL_MODE=prefer
# MySQL
MYSQL_HOST=
MYSQL_PORT=3306
MYSQL_DATABASE=
MYSQL_USERNAME=root
MYSQL_PASSWORD=
MYSQL_SSL_MODE=preferred
# MariaDB
MARIADB_HOST=
MARIADB_PORT=3306
MARIADB_DATABASE=
MARIADB_USERNAME=
MARIADB_PASSWORD=
MARIADB_SSL=true
# Dagster workflow orchestration
DAGSTER_ENDPOINT=
DAGSTER_API_TOKEN=
# RabbitMQ management API
RABBITMQ_HOST=
RABBITMQ_MANAGEMENT_PORT=15672
RABBITMQ_USERNAME=
RABBITMQ_PASSWORD=
RABBITMQ_VHOST=/
RABBITMQ_SSL=false
RABBITMQ_VERIFY_SSL=true
# Better Stack Telemetry
BETTERSTACK_QUERY_ENDPOINT=
BETTERSTACK_USERNAME=
BETTERSTACK_PASSWORD=
BETTERSTACK_SOURCES=
# Kafka
KAFKA_BOOTSTRAP_SERVERS=
KAFKA_SECURITY_PROTOCOL=PLAINTEXT
KAFKA_SASL_MECHANISM=
KAFKA_SASL_USERNAME=
KAFKA_SASL_PASSWORD=
# ClickHouse
CLICKHOUSE_HOST=
CLICKHOUSE_PORT=8123
CLICKHOUSE_DATABASE=default
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=
CLICKHOUSE_SECURE=false
# Bitbucket
BITBUCKET_WORKSPACE=
BITBUCKET_USERNAME=
BITBUCKET_APP_PASSWORD=
# GitLab
GITLAB_BASE_URL=https://gitlab.com/api/v4
GITLAB_MR_WRITEBACK=false
GITLAB_MR_IID=
GITLAB_ACCESS_TOKEN=
GITLAB_PROJECT_ID=
GITLAB_REPO_URL=
# Jira
JIRA_BASE_URL=
JIRA_EMAIL=
JIRA_API_TOKEN=
JIRA_PROJECT_KEY=
# OpsGenie
OPSGENIE_API_KEY=
OPSGENIE_REGION=us
# incident.io
INCIDENT_IO_API_KEY=
INCIDENT_IO_BASE_URL=
# Splunk
SPLUNK_URL=
SPLUNK_TOKEN=
SPLUNK_INDEX=main
SPLUNK_VERIFY_SSL=true
# Hermes
HERMES_API_URL=
HERMES_API_KEY=
# OpenObserve
OPENOBSERVE_URL=
OPENOBSERVE_TOKEN=
OPENOBSERVE_USERNAME=
OPENOBSERVE_PASSWORD=
OPENOBSERVE_ORG=default
OPENOBSERVE_STREAM=
OPENOBSERVE_MAX_RESULTS=100
# Azure Monitor (Log Analytics)
AZURE_LOG_ANALYTICS_WORKSPACE_ID=
AZURE_LOG_ANALYTICS_TOKEN=
AZURE_TENANT_ID=
AZURE_SUBSCRIPTION_ID=
AZURE_MAX_RESULTS=100
# VictoriaLogs
VICTORIA_LOGS_URL=
VICTORIA_LOGS_TENANT_ID=
# Vercel
VERCEL_API_TOKEN=
VERCEL_TEAM_ID=
# PostHog (REST bounce-rate alerting)
POSTHOG_PROJECT_ID=
POSTHOG_PERSONAL_API_KEY=
POSTHOG_BASE_URL=https://us.i.posthog.com
POSTHOG_TIMEOUT_SECONDS=15.0
POSTHOG_BOUNCE_THRESHOLD=0.6
POSTHOG_BOUNCE_WINDOW=24h
# PostHog MCP (analytics, feature flags, error tracking, HogQL via the hosted MCP server)
POSTHOG_MCP_MODE=streamable-http
POSTHOG_MCP_URL=https://mcp.posthog.com/mcp
POSTHOG_MCP_AUTH_TOKEN=
POSTHOG_MCP_ORGANIZATION_ID=
POSTHOG_MCP_PROJECT_ID=
POSTHOG_MCP_FEATURES=
POSTHOG_MCP_READ_ONLY=true
POSTHOG_MCP_COMMAND=
POSTHOG_MCP_ARGS=
# Sentry MCP (issues, events, traces, releases, Seer root-cause via the hosted MCP server)
SENTRY_MCP_MODE=streamable-http
SENTRY_MCP_URL=https://mcp.sentry.dev/mcp
SENTRY_MCP_AUTH_TOKEN=
SENTRY_MCP_HOST=
SENTRY_MCP_ORGANIZATION_SLUG=
SENTRY_MCP_PROJECT_SLUG=
SENTRY_MCP_SKILLS=
SENTRY_MCP_COMMAND=
SENTRY_MCP_ARGS=
# X (Twitter) MCP (post, search, timelines, likes via https://github.com/xdevplatform/xmcp)
# XMCP runs locally by default; point X_MCP_URL at your own instance
# (optionally tunneled) or launch it directly with X_MCP_COMMAND/X_MCP_ARGS.
X_MCP_MODE=streamable-http
X_MCP_URL=http://127.0.0.1:8000/mcp
X_MCP_AUTH_TOKEN=
X_MCP_COMMAND=
X_MCP_ARGS=
# X API bearer token forwarded to the local xmcp server (its own X API auth,
# not the MCP transport auth above).
X_BEARER_TOKEN=
# Google Docs export
GOOGLE_CREDENTIALS_FILE=
GOOGLE_DRIVE_FOLDER_ID=
# OpenClaw MCP
OPENCLAW_MCP_MODE=stdio
OPENCLAW_MCP_URL=
OPENCLAW_MCP_COMMAND=openclaw
OPENCLAW_MCP_ARGS=mcp serve
OPENCLAW_MCP_AUTH_TOKEN=
# Splunk
SPLUNK_URL=
SPLUNK_TOKEN=
SPLUNK_INDEX=main
SPLUNK_VERIFY_SSL=true
# Hermes
HERMES_API_URL=
HERMES_API_KEY=
# OpenObserve
OPENOBSERVE_URL=
OPENOBSERVE_TOKEN=
OPENOBSERVE_USERNAME=
OPENOBSERVE_PASSWORD=
OPENOBSERVE_ORG=default
OPENOBSERVE_STREAM=
OPENOBSERVE_MAX_RESULTS=100
# Azure Monitor (Log Analytics)
AZURE_LOG_ANALYTICS_WORKSPACE_ID=
AZURE_LOG_ANALYTICS_TOKEN=
AZURE_TENANT_ID=
AZURE_SUBSCRIPTION_ID=
AZURE_MAX_RESULTS=100
# VictoriaLogs
VICTORIA_LOGS_URL=
VICTORIA_LOGS_TENANT_ID=
# Discord
DISCORD_BOT_TOKEN=
DISCORD_APPLICATION_ID=
DISCORD_PUBLIC_KEY=
DISCORD_DEFAULT_CHANNEL_ID=
# Telegram
TELEGRAM_BOT_TOKEN=
TELEGRAM_DEFAULT_CHAT_ID=
# WhatsApp (Twilio)
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_WHATSAPP_FROM=
WHATSAPP_DEFAULT_TO=
# Twilio SMS — shares TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN from the block
# above (do NOT re-declare them; dotenv keeps only the first occurrence).
# Set either TWILIO_SMS_FROM (E.164 number) or TWILIO_SMS_MESSAGING_SERVICE_SID.
TWILIO_SMS_FROM=
# TWILIO_SMS_MESSAGING_SERVICE_SID=
TWILIO_SMS_DEFAULT_TO=
# --- Web app / hosted runtime only -----------------------------------------
# Required only when using the Tracer web app / hosted integration path.
JWT_TOKEN=
TRACER_API_URL=
# Remote server API key for `opensre remote ...` and hosted agent access.
OPENSRE_API_KEY=
# --- Deployment / runtime ---------------------------------------------------
# Required for hosted OpenSRE runtimes that need persistent storage.
DATABASE_URI=
REDIS_URI=
ENV=development
# Reversible masking before external LLM calls. Off by default.
OPENSRE_MASK_ENABLED=false
# Comma-separated kinds to mask. Empty = project defaults.
OPENSRE_MASK_KINDS=
# Example: '{"jira_key": "\\b[A-Z]+-\\d+\\b"}'
OPENSRE_MASK_EXTRA_REGEX=
+8
View File
@@ -0,0 +1,8 @@
# Normalize line endings for shell scripts so they stay LF on all platforms.
*.sh text eol=lf
# Mark vendored third-party code so GitHub's analysis tools skip it.
# This suppresses false-positive code scanning alerts in code we don't own.
tests/e2e/upstream_lambda/pipeline_code/** linguist-vendored=true
tests/e2e/upstream_apache_flink_ecs/pipeline_code/** linguist-vendored=true
tests/e2e/upstream_prefect_ecs_fargate/pipeline_code/** linguist-vendored=true
+92
View File
@@ -0,0 +1,92 @@
name: Bug Report
description: Report something that isn't working
title: "[BUG] "
labels:
- bug
body:
- type: markdown
attributes:
value: |
> **Security notice:** Never paste real API keys, tokens, or passwords in this form. Replace them with placeholders like `glsa_***`, `sk-***`, or `your-token-here` before submitting. Tokens posted here are publicly visible and cannot be fully deleted from GitHub's history.
Thanks for the report. Please include **observed** behavior and repro steps — guesses make triage slower.
- type: textarea
id: summary
attributes:
label: Summary
description: One-line description of the bug. Be specific (e.g. agent fails when `.env` contains `=` in values). Avoid vague phrases like "something doesn't work."
placeholder: "Example: Agent fails to parse config when .env vars contain special characters"
validations:
required: true
- type: textarea
id: expected_actual
attributes:
label: Expected vs actual behavior
description: What should happen, and what happens instead? Include exact errors, exit codes, or unexpected output where relevant.
placeholder: |
**Expected:** …
**Actual:** …
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Minimal steps that consistently trigger the bug (commands, config snippets — redact secrets).
placeholder: |
1.
2.
3.
validations:
required: true
- type: dropdown
id: reproducible
attributes:
label: Can you reproduce it consistently?
options:
- "Yes"
- "No"
- "Sometimes"
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: How often does it occur?
options:
- Every time
- Intermittent
- Under specific conditions
validations:
required: true
- type: dropdown
id: os
attributes:
label: Operating system
description: Pick the closest match.
options:
- macOS
- Linux
- Windows
- Other
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs and error output
description: Full error message and relevant logs. Redact API keys, tokens, and passwords.
placeholder: |
```
[paste error / logs]
```
validations:
required: false
- type: textarea
id: context
attributes:
label: Additional context
description: Screenshots, related issues (`#123`), what you were trying to do when this happened.
validations:
required: false
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: "Questions or Support"
url: https://github.com/Tracer-Cloud/open-sre-agent/discussions
about: "Ask questions or get help in Discussions"
- name: "Security Issue"
url: https://github.com/Tracer-Cloud/open-sre-agent/security/policy
about: "Report a security vulnerability safely"
@@ -0,0 +1,22 @@
name: Feature Request
description: Suggest a new feature or capability
title: "[FEATURE] "
labels:
- enhancement
- pending triage
body:
- type: textarea
id: problem
attributes:
label: Problem statement
description: Why is this needed? User pain, limitation, or gap today (not the solution yet).
placeholder: "Example: Users can't export agent logs to external systems without custom scripts."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed solution
description: How should it work? Commands, APIs, UX, inputs/outputs, example snippets.
validations:
required: true
+20
View File
@@ -0,0 +1,20 @@
name: Improvement
description: Suggest a refactor, optimization, or quality improvement
title: "[IMPROVEMENT] "
labels:
- enhancement
body:
- type: textarea
id: current
attributes:
label: Current state
description: How does it work now? Link to code (`file:line`), snippets, or behavior/architecture.
validations:
required: true
- type: textarea
id: desired
attributes:
label: Desired state
description: What should change, why it is better (metrics if possible), and any architectural notes.
validations:
required: true
+51
View File
@@ -0,0 +1,51 @@
Fixes #
<!-- Add issue number above -->
#### Describe the changes you have made in this PR -
### Demo/Screenshot for feature changes and bug fixes -
<!-- Include at least one proof of the change: UI screenshot, terminal screenshot/log snippet, short video/GIF, or equivalent demo output. -->
<!-- Do not add code diff here -->
---
## Code Understanding and AI Usage
**Did you use AI assistance (ChatGPT, Claude, Copilot, etc.) to write any part of this code?**
- [ ] No, I wrote all the code myself
- [ ] Yes, I used AI assistance (continue below)
**If you used AI assistance:**
- [ ] I have reviewed every single line of the AI-generated code
- [ ] I can explain the purpose and logic of each function/component I added
- [ ] I have tested edge cases and understand how the code handles them
- [ ] I have modified the AI output to follow this project's coding standards and conventions
**Explain your implementation approach:**
<!--
Describe in your own words:
- What problem does your code solve?
- What alternative approaches did you consider?
- Why did you choose this specific implementation?
- What are the key functions/components and what do they do?
This helps reviewers understand your thought process and ensures you understand the code.
-->
---
## Checklist before requesting a review
- [ ] I have added proper PR title and linked to the issue
- [ ] I have performed a self-review of my code
- [ ] **I can explain the purpose of every function, class, and logic block I added**
- [ ] I understand why my changes work and have tested them thoroughly
- [ ] I have considered potential edge cases and how my code handles them
- [ ] If it is a core feature, I have added thorough tests
- [ ] My code follows the project's style guidelines and conventions
---
Note: Please check **Allow edits from maintainers** if you would like us to assist in the PR.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 B

+237
View File
@@ -0,0 +1,237 @@
"""Enforce forbidden *direct* import edges between top-level packages.
Unlike import-linter (which flags transitive chains), this checker looks at
**module-top-level** and **nested** (function/class-body) ``import`` /
``from … import`` statements. Module-top-level uses the same AST walk as
``check_import_cycles``; nested imports close the loophole where lazy
``surfaces.*`` imports bypass the module-level graph.
Used by ``make check-imports`` (and ``check_imports``) alongside
import-linter's config contract.
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
_CI_DIR = Path(__file__).resolve().parent
if str(_CI_DIR) not in sys.path:
sys.path.insert(0, str(_CI_DIR))
from check_import_cycles import ( # noqa: E402
_build_graph,
_nested_imports,
discover_first_party_roots,
module_from_path,
)
_REPO_ROOT = Path(__file__).resolve().parents[2]
# ``source_prefix -> forbidden destination roots`` for direct imports only.
# Enforces the layering contract documented in ``surfaces/__init__.py``:
# "Nothing first-party may import from surfaces/". Adds an explicit bound
# on ``platform``, ``core``, ``gateway`` so the surfaces ban
# is CI-enforced, not just doc-described.
_FORBIDDEN_DIRECT: dict[str, frozenset[str]] = {
"platform": frozenset({"surfaces"}),
"core": frozenset({"surfaces"}),
"gateway": frozenset({"surfaces"}),
"integrations": frozenset({"tools", "surfaces"}),
"tools": frozenset({"surfaces"}),
}
# Known direct violations being burned down — remove entries as fixes land.
# Format: ``"source.module -> dest.module"`` (exact modules from the graph).
_BASELINE_IGNORES: frozenset[str] = frozenset(
{
# Gateway hosts the interactive_shell runtime — pre-existing reuse
# to be burned down by extracting shared runtime primitives out of
# ``surfaces/interactive_shell/`` and into a layer below ``surfaces``.
"gateway.storage.session.resolver -> surfaces.interactive_shell.runtime.context",
# tools/interactive_shell action tools reach UP into surfaces/interactive_shell
# for runtime / command_registry / UI primitives. Clears when the action
# tools themselves are refactored to be UI-agnostic (e.g. return
# "approval-required" sentinels instead of calling execution_confirm
# directly) so the surface owns its own confirmation UX.
"tools.interactive_shell.actions.cli_command -> surfaces.interactive_shell.runtime.subprocess_runner",
# These shell action tools type against the shell ``Session`` (investigation_launch
# reads session.terminal.background_mode_enabled). Clears when they are made
# session-core-agnostic so they no longer import the shell session type.
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.session",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.session",
"tools.interactive_shell.shared.investigation_launch -> surfaces.interactive_shell.session",
"tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry.slash_catalog",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.actions.slash -> surfaces.interactive_shell.utils.telemetry.turn_outcome",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.command_registry",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.ui",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.implementation.claude_code_executor -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.runtime",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.runtime.subprocess_runner.task_streaming",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.ui",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.ui.execution_confirm",
"tools.interactive_shell.synthetic.runner -> surfaces.interactive_shell.utils.error_handling.exception_reporting",
}
)
# Function/class-body lazy imports that bypass the module-level graph.
# Format matches ``_BASELINE_IGNORES``; burn down by moving shared code
# below ``surfaces/`` or making tools UI-agnostic.
_NESTED_BASELINE_IGNORES: frozenset[str] = frozenset(
{
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.background.runner",
"tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.investigation_adapter",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.background.runner",
"tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.investigation_adapter",
"tools.interactive_shell.actions.llm_provider -> surfaces.cli.wizard.config",
# Nested only: runner.py also has a module-level ui import (see _BASELINE_IGNORES).
"tools.interactive_shell.shell.runner -> surfaces.interactive_shell.ui",
}
)
@dataclass(frozen=True)
class DirectViolation:
source: str
target: str
@property
def edge(self) -> str:
return f"{self.source} -> {self.target}"
@dataclass(frozen=True)
class NestedViolation:
source: str
target: str
lineno: int
@property
def edge(self) -> str:
return f"{self.source} -> {self.target}"
def _source_root(module: str) -> str:
return module.split(".", 1)[0]
def find_direct_violations(
graph: dict[str, set[str]],
*,
forbidden: dict[str, frozenset[str]] | None = None,
baseline_ignores: frozenset[str] | None = None,
) -> list[DirectViolation]:
rules = forbidden or _FORBIDDEN_DIRECT
ignores = baseline_ignores if baseline_ignores is not None else _BASELINE_IGNORES
violations: list[DirectViolation] = []
for source_module, targets in sorted(graph.items()):
source_root = _source_root(source_module)
forbidden_roots = rules.get(source_root)
if not forbidden_roots:
continue
for target_module in sorted(targets):
target_root = _source_root(target_module)
if target_root not in forbidden_roots:
continue
edge = DirectViolation(source_module, target_module)
if edge.edge in ignores:
continue
violations.append(edge)
return violations
def find_nested_direct_violations(
root: Path,
first_party_roots: tuple[str, ...],
*,
forbidden: dict[str, frozenset[str]] | None = None,
baseline_ignores: frozenset[str] | None = None,
) -> list[NestedViolation]:
rules = forbidden or _FORBIDDEN_DIRECT
ignores = baseline_ignores if baseline_ignores is not None else _NESTED_BASELINE_IGNORES
roots = frozenset(first_party_roots)
violations: list[NestedViolation] = []
for pkg in first_party_roots:
if pkg not in rules:
continue
pkg_path = root / pkg
if not pkg_path.exists():
continue
for py in pkg_path.rglob("*.py"):
if "__pycache__" in py.parts:
continue
source_module = module_from_path(root, py)
source_root = _source_root(source_module)
forbidden_roots = rules.get(source_root)
if not forbidden_roots:
continue
source = py.read_text(encoding="utf-8")
for target_module, lineno in _nested_imports(source, first_party_roots=roots):
target_root = _source_root(target_module)
if target_root not in forbidden_roots:
continue
violation = NestedViolation(source_module, target_module, lineno)
if violation.edge in ignores:
continue
violations.append(violation)
return sorted(violations, key=lambda item: (item.source, item.lineno, item.target))
def main(argv: list[str] | None = None) -> int:
del argv
root = _REPO_ROOT
first_party_roots = discover_first_party_roots(root)
graph = _build_graph(root, first_party_roots)
module_violations = find_direct_violations(graph)
nested_violations = find_nested_direct_violations(root, first_party_roots)
if not module_violations and not nested_violations:
print(
"No forbidden direct import edges found "
f"(module baseline: {len(_BASELINE_IGNORES)}, "
f"nested baseline: {len(_NESTED_BASELINE_IGNORES)})."
)
return 0
if module_violations:
print(f"FAIL: {len(module_violations)} forbidden module-level direct import edge(s):")
for violation in module_violations:
print(f" {violation.edge}")
if nested_violations:
if module_violations:
print()
print(f"FAIL: {len(nested_violations)} forbidden nested direct import edge(s):")
for violation in nested_violations:
print(f" {violation.edge} (line {violation.lineno})")
print(
"\nFix by moving shared code to a lower layer (platform/common, core/contracts) "
"or add a temporary baseline entry in .github/ci/check_direct_imports.py "
"with a linked issue — do not use function-level lazy imports to hide "
"new direct edges."
)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+342
View File
@@ -0,0 +1,342 @@
"""Detect first-party module-load import cycles via Tarjan's SCC.
Walks every first-party Python module in the repo, builds an import
graph from **top-level** ``import`` / ``from ... import`` statements
only (function-level lazy imports are intentional runtime breaks and
not counted), then reports any strongly-connected component of size
> 1, plus any single-module self-loop.
Used by ``make check-imports`` (via ``check_imports``) locally and by CI.
Exit codes:
0 — zero cycles found
1 — at least one cycle found (output lists every SCC + its edges)
How to break a cycle
====================
Two patterns work. Pick by what consumers do with the name.
1. ``import pkg.sub as sub`` (preferred when consumers monkeypatch)
Switch ::
from pkg.sub import name # binds ``name`` at import time
...
name(args) # uses the bound reference
to ::
import pkg.sub as sub # imports the submodule directly
...
sub.name(args) # attribute lookup at call time
Both break the static cycle (no edge from the consumer back to the
``pkg`` package). The second form keeps attribute-lookup semantics,
which matters whenever consumers monkeypatch ``pkg.sub.name`` in
tests — the patched attribute IS looked up at each call. The first
form binds the name at import time and ignores later patching.
2. Port / Protocol (when the cycle crosses architectural layers)
When the cycle is ``layerA <-> layerB`` (e.g. analytics ↔ sentry,
integrations ↔ services), neither side should depend on the other
directly. Extract a third module — a ``Protocol`` or a small
abstract dataclass — that both depend on, and inject the concrete
implementation at startup. See the verifier plugin-registry
refactor (``integrations/verification/registry.py``) for the
canonical example.
Avoid ``from pkg import sub`` (where ``sub`` is a submodule of ``pkg``)
inside that ``pkg`` itself or any of its children — that's the form
this script flags. It triggers ``pkg``'s ``__init__`` even when you
just want the submodule, and re-export patterns in ``__init__`` close
the loop.
Function-local imports are NOT flagged. They're a legitimate Python
pattern for startup-cost deferral (heavy modules in click subcommand
bodies), optional dependencies, and conditional / platform-specific
code paths. Use sparingly — keep top-level the default — and comment
the *why* so future readers don't mistake them for cycle workarounds.
"""
from __future__ import annotations
import ast
import sys
from collections import defaultdict
from collections.abc import Iterable
from functools import lru_cache
from pathlib import Path
# Directories at the repo root that are never first-party import roots.
_SKIP_ROOT_DIRS = frozenset(
{
".git",
".github",
".pytest_cache",
".ruff_cache",
".venv",
"__pycache__",
"docs",
"opensre.egg-info",
"packaging",
"tests",
"venv",
}
)
@lru_cache(maxsize=1)
def discover_first_party_roots(repo_root: Path | None = None) -> tuple[str, ...]:
"""Return top-level package names that contain importable Python code."""
root = repo_root or Path(__file__).resolve().parents[2]
names: list[str] = []
for child in sorted(root.iterdir()):
if not child.is_dir() or child.name.startswith("."):
continue
if child.name in _SKIP_ROOT_DIRS:
continue
if not any(child.rglob("*.py")):
continue
names.append(child.name)
return tuple(names)
def _top_level_imports(source: str, *, first_party_roots: frozenset[str]) -> set[str]:
"""Return first-party module paths imported at the module top level.
Function-bodies, class-bodies, conditional / try-except wrappers all
count as top-level if they are direct module statements — the only
imports skipped are those nested **inside a function or class body**.
A lazy ``from X import Y`` inside a function does not deadlock at
module load, so it should not be flagged as a cycle.
"""
try:
tree = ast.parse(source)
except SyntaxError:
return set()
names: set[str] = set()
def _add(module_path: str) -> None:
top = module_path.split(".", 1)[0]
if top in first_party_roots:
names.add(module_path)
def _walk_top(body: Iterable[ast.stmt]) -> None:
for node in body:
if isinstance(node, ast.Import):
for alias in node.names:
_add(alias.name)
elif isinstance(node, ast.ImportFrom):
if node.level or not node.module:
continue
_add(node.module)
elif isinstance(node, ast.If):
_walk_top(node.body)
_walk_top(node.orelse)
elif isinstance(node, ast.Try | ast.TryStar):
# ``ast.TryStar`` (Python 3.11+, ``try/except*``) shares
# the same handler/orelse/finalbody shape as ``ast.Try``.
_walk_top(node.body)
for handler in node.handlers:
_walk_top(handler.body)
_walk_top(node.orelse)
_walk_top(node.finalbody)
elif isinstance(node, ast.With | ast.AsyncWith):
_walk_top(node.body)
_walk_top(tree.body)
return names
def _nested_imports(source: str, *, first_party_roots: frozenset[str]) -> list[tuple[str, int]]:
"""Return ``(target_module, lineno)`` for imports inside function/class bodies.
Complements ``_top_level_imports``: catches lazy imports that bypass the
module-level direct-edge checker while still being layering violations.
"""
try:
tree = ast.parse(source)
except SyntaxError:
return []
results: list[tuple[str, int]] = []
def _add(module_path: str, lineno: int) -> None:
top = module_path.split(".", 1)[0]
if top in first_party_roots:
results.append((module_path, lineno))
def _walk_nested(body: Iterable[ast.stmt], *, nested: bool) -> None:
for node in body:
if isinstance(node, ast.Import):
if nested:
for alias in node.names:
_add(alias.name, node.lineno)
elif isinstance(node, ast.ImportFrom):
if nested and not node.level and node.module:
_add(node.module, node.lineno)
elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
_walk_nested(node.body, nested=True)
elif isinstance(node, ast.If):
_walk_nested(node.body, nested=nested)
_walk_nested(node.orelse, nested=nested)
elif isinstance(node, ast.Try | ast.TryStar):
_walk_nested(node.body, nested=nested)
for handler in node.handlers:
_walk_nested(handler.body, nested=nested)
_walk_nested(node.orelse, nested=nested)
_walk_nested(node.finalbody, nested=nested)
elif isinstance(node, ast.With | ast.AsyncWith):
_walk_nested(node.body, nested=nested)
elif isinstance(node, ast.For | ast.AsyncFor | ast.While):
_walk_nested(node.body, nested=nested)
_walk_nested(node.orelse, nested=nested)
elif isinstance(node, ast.Match):
for case in node.cases:
_walk_nested(case.body, nested=nested)
_walk_nested(tree.body, nested=False)
return results
def module_from_path(root: Path, py: Path) -> str:
"""Resolve a repo-relative ``.py`` path to its dotted module name."""
module = ".".join(py.with_suffix("").relative_to(root).parts)
return module.removesuffix(".__init__")
def _build_graph(root: Path, first_party_roots: tuple[str, ...]) -> dict[str, set[str]]:
"""Build the first-party module-level import graph rooted at ``root``."""
roots = frozenset(first_party_roots)
graph: dict[str, set[str]] = defaultdict(set)
for pkg in first_party_roots:
pkg_path = root / pkg
if not pkg_path.exists():
continue
for py in pkg_path.rglob("*.py"):
if "__pycache__" in py.parts:
continue
module = module_from_path(root, py)
source = py.read_text(encoding="utf-8")
graph[module].update(_top_level_imports(source, first_party_roots=roots))
return graph
def _tarjan_sccs(graph: dict[str, set[str]]) -> list[list[str]]:
"""Return every strongly-connected component of size > 1, plus any
single-module self-loop."""
index: dict[str, int] = {}
lowlink: dict[str, int] = {}
on_stack: dict[str, bool] = {}
stack: list[str] = []
sccs: list[list[str]] = []
counter = [0]
def strongconnect(v: str) -> None:
index[v] = counter[0]
lowlink[v] = counter[0]
counter[0] += 1
stack.append(v)
on_stack[v] = True
for w in graph.get(v, ()):
if w not in index:
strongconnect(w)
lowlink[v] = min(lowlink[v], lowlink[w])
elif on_stack.get(w):
lowlink[v] = min(lowlink[v], index[w])
if lowlink[v] == index[v]:
component: list[str] = []
while True:
w = stack.pop()
on_stack[w] = False
component.append(w)
if w == v:
break
# ``while True`` above guarantees ``component`` is non-empty here.
if len(component) > 1 or component[0] in graph.get(component[0], ()):
sccs.append(component)
sys.setrecursionlimit(10000)
for vertex in list(graph.keys()):
if vertex not in index:
strongconnect(vertex)
return sccs
def _format_scc(scc: list[str], graph: dict[str, set[str]]) -> str:
"""Format an SCC for human-readable output: members + edges within."""
members = sorted(scc)
in_scc = set(scc)
is_self_loop = len(scc) == 1
edges: list[str] = []
for module in members:
for target in sorted(graph.get(module, ())):
if target not in in_scc:
continue
# Single-module self-loops have only the self-edge; show it
# explicitly so the developer knows which import closes the
# loop. Multi-module SCCs hide self-edges to keep the diff
# focused on the cross-module edges that close the cycle.
if target == module and not is_self_loop:
continue
edges.append(f" {module} -> {target}")
lines = [f" Modules ({len(scc)}):"]
lines.extend(f" - {m}" for m in members)
if is_self_loop and not edges:
lines.append(" Self-import detected (module imports itself at top level).")
elif edges:
lines.append(" Edges within SCC:")
lines.extend(edges)
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
del argv
root = Path(__file__).resolve().parents[2]
first_party_roots = discover_first_party_roots(root)
graph = _build_graph(root, first_party_roots)
sccs = _tarjan_sccs(graph)
if not sccs:
print(
f"No import cycles found across {len(graph)} first-party modules "
f"({len(first_party_roots)} roots)."
)
return 0
print(f"FAIL: {len(sccs)} import cycle(s) found across {len(graph)} first-party modules.")
for i, scc in enumerate(sorted(sccs, key=lambda s: -len(s)), 1):
print(f"\n## SCC #{i} ({len(scc)} module{'s' if len(scc) > 1 else ''}):")
print(_format_scc(scc, graph))
print(
"\nTo break a cycle, prefer:\n"
" import pkg.sub as sub\n"
" ...\n"
" sub.name(args)\n"
"over:\n"
" from pkg.sub import name\n"
" ...\n"
" name(args)\n"
"\n"
"Both fix the static cycle; only the first keeps attribute-lookup\n"
"semantics so tests that monkeypatch ``pkg.sub.name`` still work.\n"
"\n"
"For cross-layer cycles, introduce a Protocol/port both sides\n"
"depend on (see ``integrations/verification/registry.py`` for the\n"
"canonical example).\n"
"\n"
"Full pattern reference: docstring at the top of this script."
)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+99
View File
@@ -0,0 +1,99 @@
"""Run all import-graph quality checks in one command.
Orchestrates, in order:
1. **Cycles** — module-load SCCs (``check_import_cycles``)
2. **Layers** — ``config`` independence via import-linter (``.importlinter``)
3. **Direct edges** — forbidden top-level imports (``check_direct_imports``)
Used by ``make check-imports``, ``make check``, and CI.
Exit codes:
0 — all checks passed
1 — one or more checks failed (each section prints its own detail)
"""
from __future__ import annotations
import subprocess
import sys
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from pathlib import Path
_CI_DIR = Path(__file__).resolve().parent
if str(_CI_DIR) not in sys.path:
sys.path.insert(0, str(_CI_DIR))
from check_direct_imports import main as check_direct_imports # noqa: E402
from check_import_cycles import main as check_import_cycles # noqa: E402
_REPO_ROOT = Path(__file__).resolve().parents[2]
@dataclass(frozen=True)
class ImportCheck:
name: str
run: Callable[[], int]
def _run_importlinter(*, config: Path | None = None) -> int:
lint_imports = Path(sys.executable).with_name("lint-imports")
if not lint_imports.is_file():
print(
"lint-imports not found — install dev deps (import-linter package).",
file=sys.stderr,
)
return 1
command = [str(lint_imports)]
if config is not None:
command.extend(["--config", str(config)])
completed = subprocess.run(command, cwd=_REPO_ROOT, check=False)
return int(completed.returncode)
def import_checks(*, strict_layers: bool = False) -> Sequence[ImportCheck]:
layer_config = _REPO_ROOT / ".importlinter.strict" if strict_layers else None
return (
ImportCheck("Import cycles (Tarjan SCC)", check_import_cycles),
ImportCheck(
"Import layers (import-linter)",
lambda: _run_importlinter(config=layer_config),
),
ImportCheck("Forbidden direct import edges (module + nested)", check_direct_imports),
)
def main(argv: list[str] | None = None) -> int:
args = list(argv or [])
strict_layers = False
if args == ["--strict"]:
strict_layers = True
args = []
if args:
print(f"Unknown arguments: {' '.join(args)}", file=sys.stderr)
print("Usage: check_imports.py [--strict]", file=sys.stderr)
return 2
checks = import_checks(strict_layers=strict_layers)
failures: list[str] = []
for index, check in enumerate(checks, start=1):
print(f"=== [{index}/{len(checks)}] {check.name} ===")
exit_code = check.run()
if exit_code != 0:
failures.append(check.name)
print()
if not failures:
print(f"All {len(checks)} import checks passed.")
return 0
print(f"FAIL: {len(failures)} import check(s) failed:")
for name in failures:
print(f" - {name}")
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+194
View File
@@ -0,0 +1,194 @@
"""Run the live-LLM turn scenario suite sharded across local processes.
This mirrors the CI ``turn-live`` job
(``.github/workflows/interactive-shell-live.yml``): each shard sets
``TURN_SHARD_TOTAL`` / ``TURN_SHARD_INDEX`` and runs the live pytest selection
``-m live_llm -k "test_live_turn_execution_oracle or test_live_action_planning"``
against ``tests/core/agent/test_turn_scenarios.py``.
The suite is IO-bound (it waits on real LLM API calls), so running all shards
concurrently finishes in roughly one shard's wall time instead of the serial
total. Each shard runs as its own pytest process; per-shard output is streamed
to a log file and the exit codes are aggregated into a final summary.
Usage:
uv run python .github/ci/run_live_turn_shards.py # all 8 shards
uv run python .github/ci/run_live_turn_shards.py --shards 4
uv run python .github/ci/run_live_turn_shards.py --indexes 0,3
uv run python .github/ci/run_live_turn_shards.py -- -x # extra pytest args
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
import time
from dataclasses import dataclass
from pathlib import Path
from typing import IO
_TARGET = "tests/core/agent/test_turn_scenarios.py"
_K_EXPR = "test_live_turn_execution_oracle or test_live_action_planning"
_LOG_DIR = Path(".turn-shard-logs")
@dataclass(frozen=True)
class ShardResult:
index: int
exit_code: int
duration_s: float
log_path: Path
def _parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--shards",
type=int,
default=int(os.getenv("TURN_SHARD_TOTAL", "8")),
help="Total number of shards to split the suite into (default: 8).",
)
parser.add_argument(
"--indexes",
type=str,
default="",
help="Comma-separated subset of shard indexes to run (default: all).",
)
parser.add_argument(
"--workers-per-shard",
type=str,
default="auto",
help="pytest-xdist worker count per shard, passed to -n (default: auto).",
)
parser.add_argument(
"--provider",
type=str,
default=os.getenv("LLM_PROVIDER", "openai"),
help="LLM provider for the live run (default: $LLM_PROVIDER or openai).",
)
parser.add_argument(
"pytest_args",
nargs="*",
help="Extra args forwarded to each pytest shard (after a -- separator).",
)
return parser.parse_args(argv)
def _resolve_indexes(shards: int, indexes: str) -> list[int]:
if shards < 1:
raise SystemExit("--shards must be >= 1")
if not indexes.strip():
return list(range(shards))
selected = sorted({int(part) for part in indexes.split(",") if part.strip()})
out_of_range = [i for i in selected if i < 0 or i >= shards]
if out_of_range:
raise SystemExit(f"shard indexes out of range for --shards {shards}: {out_of_range}")
return selected
def _build_command(workers: str, pytest_args: list[str]) -> list[str]:
return [
sys.executable,
"-m",
"pytest",
"-n",
workers,
"-v",
"-m",
"live_llm",
_TARGET,
"-k",
_K_EXPR,
*pytest_args,
]
def _shard_env(*, shard_total: int, shard_index: int, provider: str) -> dict[str, str]:
env = os.environ.copy()
env["TURN_SHARD_TOTAL"] = str(shard_total)
env["TURN_SHARD_INDEX"] = str(shard_index)
env["LLM_PROVIDER"] = provider
env.setdefault("OPENSRE_DISABLE_KEYRING", "1")
env.setdefault("PYTHONUTF8", "1")
return env
def _run_shards(
*, shard_total: int, indexes: list[int], workers: str, provider: str, pytest_args: list[str]
) -> list[ShardResult]:
_LOG_DIR.mkdir(parents=True, exist_ok=True)
command = _build_command(workers, pytest_args)
print(f"Launching {len(indexes)} shard(s) of {shard_total} (provider={provider}):")
print(f" {' '.join(command)}\n")
started: dict[int, tuple[subprocess.Popen[bytes], float, Path, IO[bytes]]] = {}
for shard_index in indexes:
log_path = _LOG_DIR / f"shard-{shard_index}.log"
log_handle: IO[bytes] = log_path.open("wb")
process = subprocess.Popen(
command,
env=_shard_env(shard_total=shard_total, shard_index=shard_index, provider=provider),
stdout=log_handle,
stderr=subprocess.STDOUT,
)
started[shard_index] = (process, time.monotonic(), log_path, log_handle)
print(f" shard {shard_index} -> pid {process.pid}, log {log_path}")
print("\nWaiting for shards to finish...\n")
results: list[ShardResult] = []
pending = set(started)
while pending:
for shard_index in sorted(pending):
process, start_time, log_path, log_handle = started[shard_index]
code = process.poll()
if code is None:
continue
log_handle.close()
duration = time.monotonic() - start_time
status = "PASS" if code == 0 else f"FAIL (exit {code})"
print(f" shard {shard_index} {status} in {duration:.0f}s")
results.append(
ShardResult(
index=shard_index,
exit_code=code,
duration_s=duration,
log_path=log_path,
)
)
pending.discard(shard_index)
if pending:
time.sleep(2.0)
return sorted(results, key=lambda r: r.index)
def _print_summary(results: list[ShardResult]) -> int:
failures = [r for r in results if r.exit_code != 0]
print("\n==================== live turn shard summary ====================")
for result in results:
status = "PASS" if result.exit_code == 0 else f"FAIL (exit {result.exit_code})"
print(f" shard {result.index}: {status} [{result.duration_s:.0f}s] {result.log_path}")
if failures:
print(f"\n{len(failures)} shard(s) failed. Inspect the logs above.")
return 1
print("\nAll shards passed.")
return 0
def main(argv: list[str] | None = None) -> int:
args = _parse_args(sys.argv[1:] if argv is None else argv)
indexes = _resolve_indexes(args.shards, args.indexes)
results = _run_shards(
shard_total=args.shards,
indexes=indexes,
workers=args.workers_per_shard,
provider=args.provider,
pytest_args=list(args.pytest_args),
)
return _print_summary(results)
if __name__ == "__main__":
raise SystemExit(main())
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Run pytest targets relevant to files changed on this branch.
Usage
-----
make test-scope
make test-scope ARGS=--dry-run
python .github/ci/run_test_scope.py [--dry-run] [--base <ref>]
Exit codes mirror pytest: 0 = all pass, non-zero = failure or config error.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
_CI_DIR = Path(__file__).resolve().parent
if str(_CI_DIR) not in sys.path:
sys.path.insert(0, str(_CI_DIR))
from test_scope_rules import classify # noqa: E402
def _commit_ref_exists(ref: str) -> bool:
return (
subprocess.run(
["git", "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
def _resolve_base_ref(base: str) -> str:
"""Prefer branch refs for unqualified bases so same-named tags do not win."""
if "/" in base or base.startswith("refs/"):
return base
for ref in (
f"refs/heads/{base}",
f"refs/remotes/origin/{base}",
f"refs/remotes/upstream/{base}",
):
if _commit_ref_exists(ref):
return ref
return base
def _git_changed_files(base: str) -> list[str]:
resolved_base = _resolve_base_ref(base)
try:
merge_base = subprocess.check_output(
["git", "merge-base", "HEAD", resolved_base],
text=True,
stderr=subprocess.DEVNULL,
).strip()
except subprocess.CalledProcessError:
merge_base = "HEAD~1"
result = subprocess.check_output(
["git", "diff", "--name-only", merge_base],
text=True,
)
return [f.strip() for f in result.splitlines() if f.strip()]
def _run(cmd: list[str], *, dry_run: bool) -> int:
print(f"\n $ {' '.join(cmd)}\n", flush=True)
if dry_run:
return 0
return subprocess.run(cmd, check=False).returncode
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dry-run", action="store_true", help="Print command without running.")
parser.add_argument(
"--base",
default="main",
help="Ref to diff against (default: main). Falls back to HEAD~1 if unavailable.",
)
args = parser.parse_args(argv)
try:
changed = _git_changed_files(args.base)
except subprocess.CalledProcessError as exc:
print(f"error: could not determine changed files: {exc}", file=sys.stderr)
return 1
if not changed:
print("No changed files detected — nothing to test.")
return 0
print(f"Changed files ({len(changed)}):")
for path in changed:
print(f" {path}")
escalate, targets, areas = classify(changed)
if escalate:
print("\nEscalating to full unit suite (core/shared code or 3+ areas touched).")
return _run(["make", "test-cov"], dry_run=args.dry_run)
if not targets:
print("\nNo test targets matched — running full unit suite as fallback.")
return _run(["make", "test-cov"], dry_run=args.dry_run)
print(f"\nAreas touched: {', '.join(areas)}")
print(f"Running scoped tests: {' '.join(targets)}")
return _run(
[sys.executable, "-m", "pytest", *targets, "-v"],
dry_run=args.dry_run,
)
if __name__ == "__main__":
sys.exit(main())
+608
View File
@@ -0,0 +1,608 @@
"""Path → pytest target mapping for branch-scoped test runs (CI.md §2).
This module is the single source of truth for ``make test-scope``. Edit rules
here only — do not duplicate the mapping table in CI.md.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
# Distinct app areas in one diff that trigger escalation to ``make test-cov``.
ESCALATION_AREA_THRESHOLD = 3
@dataclass(frozen=True, slots=True)
class PathRule:
"""Map changed paths under ``path_prefix`` to pytest targets."""
path_prefix: str
test_targets: tuple[str, ...]
always_escalate: bool = False
# Matched in list order — more specific prefixes must appear before parents.
RULES: tuple[PathRule, ...] = (
# Shared core (always escalate)
PathRule("core/domain/", (), always_escalate=True),
PathRule("core/", ("tests/core/",)),
PathRule("tools/investigation/reporting/", ("tests/delivery/",)),
PathRule("tools/investigation/", (), always_escalate=True),
PathRule("utils/", (), always_escalate=True),
# Specific sub-packages before their parent
PathRule("integrations/llm_cli/", ("tests/integrations/llm_cli/",)),
PathRule("integrations/opensre/", ("tests/integrations/opensre/",)),
PathRule("integrations/hermes/", ("tests/hermes/",)),
PathRule(
"integrations/alertmanager/",
("tests/integrations/alertmanager/", "tests/e2e/alertmanager/"),
),
PathRule(
"integrations/dagster/",
("tests/integrations/dagster/", "tests/synthetic/test_dagster_scenario.py"),
),
PathRule(
"integrations/eks/",
(
"tests/integrations/eks/",
"tests/tools/test_eks_deployment_status_tool.py",
"tests/tools/test_eks_describe_addon_tool.py",
"tests/tools/test_eks_describe_cluster_tool.py",
"tests/tools/test_eks_events_tool.py",
"tests/tools/test_eks_list_clusters_tool.py",
"tests/tools/test_eks_list_deployments_tool.py",
"tests/tools/test_eks_list_namespaces_tool.py",
"tests/tools/test_eks_list_pods_tool.py",
"tests/tools/test_eks_node_health_tool.py",
"tests/tools/test_eks_nodegroup_health_tool.py",
"tests/tools/test_eks_pod_logs_tool.py",
"tests/tools/test_telemetry.py",
"tests/benchmarks/cloudopsbench/tests/test_bench_agent.py",
),
),
PathRule(
"integrations/elasticsearch/",
(
"tests/integrations/elasticsearch/",
"tests/tools/test_elasticsearch_logs_tool.py",
),
),
PathRule(
"integrations/google_docs/",
(
"tests/integrations/google_docs/",
"tests/tools/test_google_docs_create_report_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/groundcover/",
("tests/integrations/groundcover/", "tests/tools/test_groundcover_tools.py"),
),
PathRule(
"integrations/helm/",
("tests/integrations/helm/", "tests/tools/test_helm_tools.py"),
),
PathRule(
"integrations/incident_io/",
("tests/integrations/incident_io/", "tests/tools/test_incident_io_tool.py"),
),
PathRule(
"integrations/jira/",
(
"tests/integrations/jira/",
"tests/tools/test_jira_add_comment_tool.py",
"tests/tools/test_jira_create_issue_tool.py",
"tests/tools/test_jira_issue_detail_tool.py",
"tests/tools/test_jira_search_issues_tool.py",
),
),
PathRule(
"integrations/clickhouse/",
(
"tests/integrations/clickhouse/",
"tests/tools/test_clickhouse_query_activity_tool.py",
"tests/tools/test_clickhouse_system_health_tool.py",
),
),
PathRule(
"integrations/mariadb/",
(
"tests/integrations/mariadb/",
"tests/tools/test_mariadb_innodb_status_tool.py",
"tests/tools/test_mariadb_process_list_tool.py",
"tests/tools/test_mariadb_replication_tool.py",
"tests/tools/test_mariadb_slow_queries_tool.py",
"tests/tools/test_mariadb_status_tool.py",
"tests/e2e/mariadb/",
),
),
PathRule(
"integrations/mongodb_atlas/",
(
"tests/integrations/mongodb_atlas/",
"tests/tools/test_mongodb_atlas_alerts_tool.py",
"tests/tools/test_mongodb_atlas_clusters_tool.py",
"tests/tools/test_mongodb_atlas_events_tool.py",
"tests/tools/test_mongodb_atlas_metrics_tool.py",
"tests/tools/test_mongodb_atlas_performance_advisor_tool.py",
),
),
PathRule(
"integrations/mongodb/",
(
"tests/integrations/mongodb/",
"tests/tools/test_mongodb_collection_stats_tool.py",
"tests/tools/test_mongodb_current_ops_tool.py",
"tests/tools/test_mongodb_profiler_tool.py",
"tests/tools/test_mongodb_replica_status_tool.py",
"tests/tools/test_mongodb_server_status_tool.py",
"tests/e2e/mongodb/",
),
),
PathRule(
"integrations/mysql/",
(
"tests/integrations/mysql/",
"tests/tools/test_mysql_current_processes_tool.py",
"tests/tools/test_mysql_replication_status_tool.py",
"tests/tools/test_mysql_server_status_tool.py",
"tests/tools/test_mysql_slow_queries_tool.py",
"tests/tools/test_mysql_table_stats_tool.py",
"tests/e2e/mysql/",
),
),
PathRule(
"integrations/postgresql/",
(
"tests/integrations/postgresql/",
"tests/tools/test_postgresql_current_queries_tool.py",
"tests/tools/test_postgresql_locks_tool.py",
"tests/tools/test_postgresql_replication_status_tool.py",
"tests/tools/test_postgresql_server_status_tool.py",
"tests/tools/test_postgresql_slow_queries_tool.py",
"tests/tools/test_postgresql_table_stats_tool.py",
"tests/e2e/postgresql/",
),
),
PathRule(
"integrations/redis/",
(
"tests/integrations/redis/",
"tests/tools/test_redis_client_list_tool.py",
"tests/tools/test_redis_key_scan_tool.py",
"tests/tools/test_redis_latency_doctor_tool.py",
"tests/tools/test_redis_list_depth_tool.py",
"tests/tools/test_redis_replication_tool.py",
"tests/tools/test_redis_server_info_tool.py",
"tests/tools/test_redis_slowlog_tool.py",
"tests/e2e/redis/",
),
),
PathRule(
"integrations/snowflake/",
(
"tests/integrations/snowflake/",
"tests/tools/test_snowflake_query_history_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/azure/",
(
"tests/tools/test_azure_monitor_logs_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/azure_sql/",
(
"tests/integrations/test_azure_sql.py",
"tests/tools/test_azure_sql_current_queries_tool.py",
"tests/tools/test_azure_sql_resource_stats_tool.py",
"tests/tools/test_azure_sql_server_status_tool.py",
"tests/tools/test_azure_sql_slow_queries_tool.py",
"tests/tools/test_azure_sql_wait_stats_tool.py",
),
),
PathRule(
"integrations/betterstack/",
(
"tests/integrations/test_betterstack.py",
"tests/tools/test_betterstack_logs_tool.py",
),
),
PathRule(
"integrations/hermes/tools/",
(
"tests/tools/test_hermes_logs_tool.py",
"tests/tools/test_hermes_session_evidence_tool.py",
),
),
PathRule(
"integrations/kafka/",
(
"tests/integrations/test_kafka.py",
"tests/tools/test_kafka_consumer_group_tool.py",
"tests/tools/test_kafka_topic_health_tool.py",
),
),
PathRule(
"integrations/openclaw/",
(
"tests/tools/test_openclaw_mcp_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/openobserve/",
(
"tests/tools/test_openobserve_logs_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/opensearch/",
(
"tests/integrations/test_opensearch_catalog.py",
"tests/tools/test_opensearch_analytics_tool.py",
),
),
PathRule(
"integrations/posthog_mcp/",
(
"tests/integrations/test_posthog_mcp.py",
"tests/tools/test_posthog_mcp_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/rabbitmq/",
(
"tests/integrations/test_rabbitmq.py",
"tests/tools/test_rabbitmq_broker_overview_tool.py",
"tests/tools/test_rabbitmq_connection_stats_tool.py",
"tests/tools/test_rabbitmq_consumer_health_tool.py",
"tests/tools/test_rabbitmq_node_health_tool.py",
"tests/tools/test_rabbitmq_queue_backlog_tool.py",
),
),
PathRule(
"integrations/sentry_mcp/",
(
"tests/integrations/test_sentry_mcp.py",
"tests/tools/test_sentry_mcp_tool.py",
"tests/tools/test_telemetry.py",
),
),
PathRule(
"integrations/sentry/",
(
"tests/tools/test_sentry_issue_details_tool.py",
"tests/tools/test_sentry_issue_events_tool.py",
"tests/tools/test_sentry_search_issues_tool.py",
),
),
PathRule(
"integrations/supabase/",
(
"tests/integrations/test_supabase.py",
"tests/tools/test_supabase_health_tool.py",
"tests/tools/test_supabase_storage_tool.py",
),
),
PathRule(
"integrations/bitbucket/",
(
"tests/integrations/test_bitbucket.py",
"tests/tools/test_bitbucket_commits_tool.py",
"tests/tools/test_bitbucket_file_contents_tool.py",
"tests/tools/test_bitbucket_search_code_tool.py",
),
),
PathRule(
"integrations/telegram/tools/",
("tests/tools/test_telegram_send_message_tool.py",),
),
PathRule(
"integrations/tracer/tools/",
(
"tests/tools/test_tracer_airflow_metrics_tool.py",
"tests/tools/test_tracer_batch_statistics_tool.py",
"tests/tools/test_tracer_error_logs_tool.py",
"tests/tools/test_tracer_failed_jobs_tool.py",
"tests/tools/test_tracer_failed_run_tool.py",
"tests/tools/test_tracer_failed_tools_tool.py",
"tests/tools/test_tracer_host_metrics_tool.py",
"tests/tools/test_tracer_run_tool.py",
"tests/tools/test_tracer_tasks_tool.py",
),
),
PathRule(
"integrations/twilio/",
(
"tests/integrations/test_twilio.py",
"tests/tools/test_twilio_notify_tool.py",
),
),
PathRule(
"integrations/github/tools/",
(
"tests/tools/test_github_actions_tool.py",
"tests/tools/test_github_commits_tool.py",
"tests/tools/test_github_file_contents_tool.py",
"tests/tools/test_github_helpers.py",
"tests/tools/test_github_issues_tool.py",
"tests/tools/test_github_repo_scope.py",
"tests/tools/test_github_repository_tool.py",
"tests/tools/test_github_repository_tree_tool.py",
"tests/tools/test_github_search_code_tool.py",
"tests/tools/test_github_workflow_tools.py",
),
),
PathRule(
"integrations/gitlab/",
(
"tests/integrations/test_gitlab.py",
"tests/tools/test_gitlab_commits_tool.py",
"tests/tools/test_gitlab_file_tool.py",
"tests/tools/test_gitlab_mrs_tool.py",
"tests/tools/test_gitlab_pipelines_tool.py",
"tests/e2e/gitlab/",
),
),
PathRule(
"integrations/aws/tools/",
("tests/tools/test_aws_operation_tool.py",),
),
PathRule(
"integrations/aws_lambda/",
(
"tests/integrations/aws/test_lambda_client.py",
"tests/tools/test_lambda_config_tool.py",
"tests/tools/test_lambda_errors_tool.py",
"tests/tools/test_lambda_inspect_tool.py",
"tests/tools/test_lambda_invocation_logs_tool.py",
),
),
PathRule(
"integrations/cloudtrail/",
("tests/tools/test_cloudtrail_events.py",),
),
PathRule(
"integrations/cloudwatch/",
(
"tests/integrations/aws/test_cloudwatch_client.py",
"tests/tools/test_cloudwatch_batch_metrics_tool.py",
"tests/tools/test_cloudwatch_logs_tool.py",
),
),
PathRule(
"integrations/ec2/",
("tests/tools/test_ec2_instances_by_tag_tool.py",),
),
PathRule(
"integrations/elb/",
("tests/tools/test_elb_target_health_tool.py",),
),
PathRule(
"integrations/rds/",
(
"tests/integrations/test_rds.py",
"tests/tools/test_rds_tools.py",
),
),
PathRule(
"integrations/s3/",
(
"tests/integrations/aws/test_s3_client.py",
"tests/tools/test_s3_get_object_tool.py",
"tests/tools/test_s3_inspect_tool.py",
"tests/tools/test_s3_list_tool.py",
"tests/tools/test_s3_marker_tool.py",
),
),
PathRule(
"integrations/opsgenie/",
(
"tests/integrations/opsgenie/",
"tests/tools/test_opsgenie_alert_detail_tool.py",
"tests/tools/test_opsgenie_alerts_tool.py",
),
),
PathRule(
"integrations/pagerduty/",
(
"tests/integrations/pagerduty/",
"tests/tools/test_pagerduty_incident_detail_tool.py",
"tests/tools/test_pagerduty_incidents_tool.py",
"tests/tools/test_pagerduty_oncall_tool.py",
"tests/tools/test_pagerduty_services_tool.py",
),
),
PathRule(
"integrations/prefect/",
(
"tests/integrations/prefect/",
"tests/tools/test_prefect_flow_runs_tool.py",
"tests/tools/test_prefect_worker_health_tool.py",
),
),
PathRule(
"integrations/signoz/",
(
"tests/integrations/signoz/",
"tests/tools/test_signoz_tools.py",
"tests/synthetic/test_signoz_scenario.py",
),
),
PathRule(
"integrations/splunk/",
("tests/integrations/splunk/", "tests/tools/test_splunk_search_tool.py"),
),
PathRule(
"integrations/tempo/",
(
"tests/integrations/tempo/",
"tests/tools/test_tempo_tools.py",
"tests/synthetic/test_tempo_scenario.py",
),
),
PathRule(
"integrations/temporal/",
(
"tests/integrations/temporal/",
"tests/integrations/test_temporal_catalog.py",
"tests/synthetic/test_temporal_scenario.py",
"tests/tools/test_temporal_namespace_info_tool.py",
"tests/tools/test_temporal_task_queue_tool.py",
"tests/tools/test_temporal_workflow_history_tool.py",
"tests/tools/test_temporal_workflows_tool.py",
),
),
PathRule(
"integrations/vercel/",
(
"tests/integrations/vercel/",
"tests/tools/test_vercel_deployment_status_tool.py",
"tests/tools/test_vercel_logs_tool.py",
),
),
PathRule(
"integrations/victoria_logs/",
(
"tests/integrations/victoria_logs/",
"tests/tools/test_victoria_logs_tool.py",
"tests/e2e/victoria_logs/",
),
),
PathRule(
"integrations/x_mcp/",
(
"tests/integrations/test_x_mcp.py",
"tests/tools/test_x_mcp_tool.py",
),
),
PathRule(
"integrations/argocd/",
(
"tests/integrations/argocd/",
"tests/tools/test_argocd_tools.py",
),
),
PathRule(
"integrations/coralogix/",
(
"tests/integrations/coralogix/",
"tests/tools/test_coralogix_logs_tool.py",
),
),
PathRule(
"integrations/honeycomb/",
(
"tests/integrations/honeycomb/",
"tests/tools/test_honeycomb_traces_tool.py",
),
),
PathRule(
"integrations/jenkins/",
("tests/integrations/test_jenkins.py", "tests/synthetic/test_jenkins_scenario.py"),
),
PathRule(
"integrations/datadog/",
(
"tests/integrations/datadog/",
"tests/tools/test_datadog_context_tool.py",
"tests/tools/test_datadog_events_tool.py",
"tests/tools/test_datadog_logs_tool.py",
"tests/tools/test_datadog_metrics_tool.py",
"tests/tools/test_datadog_monitors_tool.py",
"tests/tools/test_datadog_node_pods_tool.py",
),
),
PathRule(
"integrations/grafana/",
(
"tests/integrations/grafana/",
"tests/tools/test_grafana_alert_rules_tool.py",
"tests/tools/test_grafana_annotations_tool.py",
"tests/tools/test_grafana_logs_tool.py",
"tests/tools/test_grafana_metrics_tool.py",
"tests/tools/test_grafana_service_names_tool.py",
"tests/tools/test_grafana_traces_tool.py",
"tests/e2e/grafana_validation/",
),
),
PathRule("integrations/", ("tests/integrations/",)),
PathRule("tools/system/fleet_monitoring/", ("tests/agent/", "tests/fleet_monitoring/")),
PathRule("surfaces/cli/", ("tests/cli/",)),
PathRule("surfaces/interactive_shell/", ("tests/interactive_shell/",)),
PathRule("gateway/", ("gateway/tests/",)),
PathRule("tools/system/watch_dog/", ("tests/watch_dog/",)),
PathRule("tools/", ("tests/tools/",)),
PathRule("platform/analytics/", ("tests/analytics/",)),
PathRule("platform/guardrails/", ("tests/platform/guardrails/",)),
PathRule("platform/masking/", ("tests/masking/",)),
PathRule("platform/packaging/", ("tests/packaging/",)),
PathRule("platform/sandbox/", ("tests/sandbox/",)),
PathRule(
"platform/deployment/",
("tests/deployment/", "tests/platform/deployment/test_deployment_health.py"),
),
PathRule("platform/auth/", ("tests/platform/auth/",)),
PathRule("gateway/webapp.py", ("gateway/tests/test_webapp.py",)),
# Repo-wide config
PathRule("pyproject.toml", (), always_escalate=True),
PathRule("uv.lock", (), always_escalate=True),
PathRule("pytest.ini", (), always_escalate=True),
PathRule("Makefile", (), always_escalate=True),
PathRule(".github/ci/", ("tests/github_ci/",)),
)
def _matches(path: str, prefix: str) -> bool:
return path.startswith(prefix) or path == prefix.rstrip("/")
def _area_key(prefix: str) -> str:
parts = prefix.split("/")
if parts[0] == "deployment" or parts[:2] == ["platform", "deployment"]:
return "deployment"
return prefix
def classify(changed: list[str]) -> tuple[bool, list[str], list[str]]:
"""Return ``(should_escalate, test_targets, matched_areas)``."""
escalate = False
targets: list[str] = []
areas: list[str] = []
for path in changed:
matched = False
for rule in RULES:
if not _matches(path, rule.path_prefix):
continue
matched = True
if rule.always_escalate:
escalate = True
else:
area = _area_key(rule.path_prefix)
if area not in areas:
areas.append(area)
for target in rule.test_targets:
if target not in targets:
targets.append(target)
break
if not matched and path.startswith("tests/") and path not in targets:
targets.append(path)
if len(areas) >= ESCALATION_AREA_THRESHOLD:
escalate = True
existing = [t for t in targets if Path(t).exists()]
dropped = [t for t in targets if t not in existing]
if dropped:
print(f" (skipping non-existent targets: {', '.join(dropped)})", flush=True)
return escalate, existing, areas
+7
View File
@@ -0,0 +1,7 @@
name: "CodeQL config"
paths-ignore:
# Vendored third-party pipeline code in test fixtures — not owned by this project.
# Covers every tests/e2e/<case>/pipeline_code/ fixture (lambda, flink, prefect,
# datadog, kubernetes, …) so new cases are ignored automatically.
- tests/e2e/**/pipeline_code/**
+15
View File
@@ -0,0 +1,15 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: "daily"
open-pull-requests-limit: 10
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Build the Discord release announcement JSON payload.
Reads from environment variables set by the GitHub Actions step:
RELEASE_TAG, RELEASE_URL,
DISCORD_RELEASES_ROLE_ID, DISCORD_RELEASE_LOGO_EMOJI, DISCORD_RELEASE_LOGO_URL,
CHANGELOG_FILE — path to DISCORD_NARRATIVE.md or GENERATED_CHANGELOG.md
"""
from __future__ import annotations
import json
import os
import sys
MAX_CHANGELOG_CHARS = 1400
tag = os.environ["RELEASE_TAG"]
url = os.environ["RELEASE_URL"]
role_id = os.environ.get("DISCORD_RELEASES_ROLE_ID", "")
logo_emoji = os.environ.get("DISCORD_RELEASE_LOGO_EMOJI", "")
logo_url = os.environ.get("DISCORD_RELEASE_LOGO_URL", "")
changelog_file = os.environ.get("CHANGELOG_FILE", "")
if changelog_file and os.path.isfile(changelog_file):
with open(changelog_file) as f:
changelog = f.read().replace("\r", "").strip()
else:
changelog = "No changelog available."
if len(changelog) > MAX_CHANGELOG_CHARS:
changelog = changelog[: MAX_CHANGELOG_CHARS - 3] + "..."
mention = f"<@&{role_id}>\n" if role_id else ""
logo_prefix = f"{logo_emoji} " if logo_emoji else ""
bt = "`"
content = (
mention + logo_prefix + f"🚀 **opensre {bt}{tag}{bt} is live**\n" + f"🔗 {url}\n\n" + changelog
)
allowed_mentions: dict = {"parse": []}
if role_id:
allowed_mentions["roles"] = [role_id]
payload: dict = {"content": content, "allowed_mentions": allowed_mentions}
if logo_url:
payload["username"] = "OpenSRE"
payload["avatar_url"] = logo_url
json.dump(payload, sys.stdout)
+223
View File
@@ -0,0 +1,223 @@
"""Assign and notify **new** contributors on `good first issue` threads.
Here *new* means the commenter has **no merged PRs and no open PRs** in this repo where
they are the PR author (GitHub Search API). **One or more merged PRs** means they are not
treated as a first-time contributor for this automation (GitHub's ``FIRST_TIME_CONTRIBUTOR``
/ ``FIRST_TIMER`` flags are not used).
Also skips repo insiders (OWNER / MEMBER / COLLABORATOR), bots, closed issues,
comments on **pull request** threads (``issue_comment`` fires for PRs too; those
use ``issue.pull_request``), and commenters already listed as assignees.
**One open assignment per eligible new contributor** in this repo: if they already
have another **open** issue assigned (Search API), they cannot be auto-assigned here.
At most **one** auto-assignment per issue: if anyone else is already an assignee,
further eligible commenters are skipped (manual pre-assignments count).
"""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
GOOD_FIRST_LABEL = "good first issue"
# Do not auto-assign maintainers/collaborators;
# eligibility is 0 merged + 0 open PRs as author + not insider.
EXCLUDED_COMMENTER_ASSOCIATIONS = frozenset({"OWNER", "MEMBER", "COLLABORATOR"})
GITHUB_API = "https://api.github.com"
def _github_api_url(path: str, query: dict[str, str]) -> str:
encoded = urllib.parse.urlencode(query)
return f"{GITHUB_API}{path}?{encoded}"
def screen_event_without_api(event: dict[str, Any]) -> str | None:
"""Return a skip reason before calling the GitHub API, or None if checks should continue."""
issue = event.get("issue") or {}
comment = event.get("comment") or {}
if issue.get("pull_request") is not None:
return "comment_on_pull_request"
if issue.get("state") != "open":
return "issue_not_open"
labels = issue.get("labels") or []
if not isinstance(labels, list):
return "invalid_labels"
names = {item.get("name") for item in labels if isinstance(item, dict)}
if GOOD_FIRST_LABEL not in names:
return "not_good_first_issue"
c_user = comment.get("user") or {}
if c_user.get("type") == "Bot":
return "bot_commenter"
c_login = c_user.get("login") or ""
if not c_login:
return "missing_commenter_login"
c_assoc = comment.get("author_association") or ""
if c_assoc in EXCLUDED_COMMENTER_ASSOCIATIONS:
return "commenter_repo_insider"
assignees = issue.get("assignees") or []
if isinstance(assignees, list):
assigned_logins = {
a.get("login") for a in assignees if isinstance(a, dict) and a.get("login")
}
if c_login in assigned_logins:
return "already_assignee"
if assigned_logins:
return "issue_already_claimed"
return None
def assign_decision(
*,
skip_reason_pre_api: str | None,
merged_pr_count_for_commenter: int,
open_pr_count_for_commenter: int,
open_assigned_issue_count_for_commenter: int,
) -> tuple[bool, str]:
"""Return (should_assign_and_comment, skip_reason_or_empty).
Eligible "new contributor" means ``merged_pr_count_for_commenter == 0`` and
``open_pr_count_for_commenter == 0`` (PR author in this repo, via Search API).
They must also have no other open issues assigned in this repo
(``open_assigned_issue_count_for_commenter == 0``).
"""
if skip_reason_pre_api is not None:
return False, skip_reason_pre_api
if open_pr_count_for_commenter > 0:
return False, "has_open_prs"
if merged_pr_count_for_commenter > 0:
return False, "has_merged_prs"
if open_assigned_issue_count_for_commenter > 0:
return False, "already_has_open_assigned_issue"
return True, ""
def _request_json(url: str, token: str) -> Any:
parsed = urllib.parse.urlparse(url)
if parsed.scheme != "https" or parsed.netloc != "api.github.com":
raise ValueError("GitHub API URL must target https://api.github.com")
req = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
method="GET",
)
with (
urllib.request.urlopen(req, timeout=60) as resp
): # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
return json.loads(resp.read().decode("utf-8"))
def _search_issue_total_count(query: str, token: str) -> int:
url = _github_api_url("/search/issues", {"q": query})
try:
data = _request_json(url, token)
except urllib.error.URLError as exc:
print(f"GitHub search failed: {exc}", file=sys.stderr)
raise
total = data.get("total_count")
if not isinstance(total, int):
return 0
return total
def fetch_merged_pr_count(owner: str, repo: str, login: str, token: str) -> int:
q = f"repo:{owner}/{repo} is:pr is:merged author:{login}"
return _search_issue_total_count(q, token)
def fetch_open_pr_count(owner: str, repo: str, login: str, token: str) -> int:
q = f"repo:{owner}/{repo} is:pr is:open author:{login}"
return _search_issue_total_count(q, token)
def fetch_open_assigned_issue_count(owner: str, repo: str, login: str, token: str) -> int:
"""Count open issues in this repo where ``login`` is an assignee."""
q = f"repo:{owner}/{repo} is:issue is:open assignee:{login}"
return _search_issue_total_count(q, token)
def build_assign_notice_body(*, assignee_login: str) -> str:
return f"@{assignee_login} You've been **assigned** to this issue. Thanks for picking it up."
def set_github_output(name: str, value: str) -> None:
path = os.environ.get("GITHUB_OUTPUT")
if not path:
return
with open(path, "a", encoding="utf-8") as fh:
fh.write(f"{name}={value}\n")
def main() -> int:
event_path = os.environ.get("GITHUB_EVENT_PATH")
repository = os.environ.get("GITHUB_REPOSITORY")
token = os.environ.get("GITHUB_TOKEN")
if not event_path or not repository or not token:
print("Missing GITHUB_EVENT_PATH, GITHUB_REPOSITORY, or GITHUB_TOKEN.", file=sys.stderr)
return 1
raw = Path(event_path).read_text(encoding="utf-8")
event = json.loads(raw)
pre = screen_event_without_api(event)
merged_count = 0
open_count = 0
open_assigned_issue_count = 0
if pre is None:
owner, _, repo = repository.partition("/")
if not owner or not repo:
print("Invalid GITHUB_REPOSITORY.", file=sys.stderr)
return 1
comment = event.get("comment") or {}
c_login = (comment.get("user") or {}).get("login") or ""
try:
merged_count = fetch_merged_pr_count(owner, repo, c_login, token)
open_count = fetch_open_pr_count(owner, repo, c_login, token)
open_assigned_issue_count = fetch_open_assigned_issue_count(owner, repo, c_login, token)
except urllib.error.URLError as exc:
print(f"GitHub API request failed: {exc}", file=sys.stderr)
return 1
should, reason = assign_decision(
skip_reason_pre_api=pre,
merged_pr_count_for_commenter=merged_count,
open_pr_count_for_commenter=open_count,
open_assigned_issue_count_for_commenter=open_assigned_issue_count,
)
if not should:
print(f"Skip: {reason}")
set_github_output("should_assign", "false")
return 0
comment_user = (event.get("comment") or {}).get("user") or {}
login = comment_user.get("login") if isinstance(comment_user, dict) else ""
if not isinstance(login, str) or not login:
print("Missing commenter login.", file=sys.stderr)
return 1
body = build_assign_notice_body(assignee_login=login)
Path("assign_comment.md").write_text(body, encoding="utf-8")
set_github_output("should_assign", "true")
print("Wrote assign_comment.md; assignment will be applied in workflow.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,124 @@
"""Write celebrate-merge PR comment body to comment.md (run from Actions after merge)."""
from __future__ import annotations
import os
import random
discord = os.environ["DISCORD_INVITE_URL"]
contributor = os.environ["CONTRIBUTOR_LOGIN"]
templates: list[str] = [
(f"🎉 **MERGED!** @{contributor} just shipped something. The diff gods are pleased. 🙌"),
(
f"🚀 **Houston, we have a merge.** @{contributor} your PR is in orbit. "
"Thanks for launching this one!"
),
(
f"💜 **One more reason the project grows.** Thanks @{contributor}"
"your contribution just landed!"
),
(
f"🎊 **Achievement unlocked: PR Merged.** @{contributor} passed code review, "
"survived CI, and shipped. Respect. 🤝"
),
(
f'🔥 **Another one.** @{contributor} said "here\'s a PR" and maintainers said '
"\"ship it\". That's how it's done."
),
(
f"🧑‍💻 **@{contributor} has entered the contributor hall of fame.** "
"Merged. Done. Shipped. Go touch grass (then come back with another PR). 🌱"
),
(
f"🎯 **Bullseye.** @{contributor} opened a PR, kept the vibes clean, "
"and got it merged. Absolute cinema. 🎬"
),
(
f"⚡ **LGTM → Merged.** @{contributor}, your work is in. "
"Every commit counts — thank you for this one."
),
# new additions
(
f'😤 **@{contributor} said "I will fix this" and then actually fixed it.** '
"Legendary behavior."
),
(
f"🍕 **@{contributor}'s PR:** crispy edges, no unnecessary toppings, delivered on time. "
"Understood the assignment. 🔥"
),
(f"🌊 **Merged.** @{contributor} is now permanently woven into git history. No take-backs. 😄"),
(
f"🤖 **CI passed. Linter didn't scream. Reviewer typed LGTM.** "
f"@{contributor}, every machine in this pipeline just slow-clapped. 🖥️✨"
),
(f"🧠 **@{contributor} opened a PR.** Maintainers feared them. CI genuflected. It merged. 🚨"),
(
f"😭 **Clear commit message. Green tests. Kind review.** "
f"@{contributor}, stop making the rest of us look bad."
),
(
f"🐸 **Rebase? Handled. Conflicts? Squashed. CI? Vibing.** "
f"@{contributor} touched the untouchable and lived. 🫡"
),
(
f"🏆 **@{contributor} did not come to play.** "
"PR opened. Review survived. Merged clean. Retire the jersey. 🎽"
),
(
f"🎲 **Researchers are baffled.** @{contributor} opened a PR, got it reviewed without drama, "
"and merged clean. This violates known laws of open source. 🔬"
),
(
f"🌮 **@{contributor}'s PR:** showed up unannounced, improved everything, left zero bugs. "
"Just like a perfect taco. 🌮"
),
(
f"🐉 **Legend says** enough merged PRs and you ascend. "
f"@{contributor} is dangerously close. 🌤️"
),
(
f"🛸 **Aliens watching our repo** just upgraded @{contributor}'s threat level to: "
"*do not engage — too competent*. 👽"
),
(
f'🎻 **"The diff was clean, the tests did pass, the reviewer wept."** '
f"That poem was about @{contributor}'s PR. 🥹"
),
(f"🍵 **@{contributor} made tea, opened a PR, and merged before it cooled.** No notes. ☕"),
(
f"🏄 **Some PRs rot in review for six weeks.** "
f'@{contributor}\'s said "not today" and merged like it owned the place. 🌊'
),
(
f"💼 **Interviewer:** describe a time you shipped something impactful.\n\n"
f"**@{contributor}:** *points at this PR*\n\n"
"**Interviewer:** you're hired. 🤝"
),
]
# GIFs are repo-hosted under .github/assets/celebrations/ so GitHub's own CDN serves them.
_base = "https://raw.githubusercontent.com/Tracer-Cloud/opensre/main/.github/assets/celebrations"
gif_blocks: list[str] = [
f"\n\n![]({_base}/party.gif)",
f"\n\n![]({_base}/celebrate.gif)",
f"\n\n![]({_base}/ship.gif)",
f"\n\n![]({_base}/shipped.gif)",
f"\n\n![]({_base}/fireworks.gif)",
f"\n\n![]({_base}/woohoo.gif)",
f"\n\n![]({_base}/office-celebrate.gif)",
f"\n\n![]({_base}/merge-celebrate-1.gif)",
f"\n\n![]({_base}/merge-celebrate-2.gif)",
f"\n\n![]({_base}/merge-celebrate-3.gif)",
]
head = random.choice(templates) + random.choice(gif_blocks)
footer = (
"---\n\n"
f"👋 **Join us on [Discord - OpenSRE]({discord})** : hang out, contribute, "
"or hunt for features and issues. Everyone's welcome."
)
body = f"{head}\n\n{footer}"
with open("comment.md", "w", encoding="utf-8") as fh:
fh.write(body)
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Sync Tracer-Cloud/homebrew-tap Formula/opensre.rb version and archive SHAs.
#
# Usage (CI):
# HOMEBREW_TAP_GITHUB_TOKEN=... VERSION=2026.5.29 ASSET_DIR=release-assets ./sync-homebrew-tap-formula.sh
#
# Local dry-run (no push):
# DRY_RUN=1 VERSION=2026.5.29 ASSET_DIR=/path/to/sha256-files ./sync-homebrew-tap-formula.sh
set -euo pipefail
VERSION="${VERSION:-}"
ASSET_DIR="${ASSET_DIR:-release-assets}"
DRY_RUN="${DRY_RUN:-0}"
if [ -z "$VERSION" ]; then
echo "VERSION is required (e.g. 2026.5.29)" >&2
exit 1
fi
for platform in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do
sha_file="${ASSET_DIR}/opensre_${VERSION}_${platform}.tar.gz.sha256"
if [ ! -f "$sha_file" ]; then
echo "Missing SHA256 file: $sha_file" >&2
exit 1
fi
done
linux_x64_sha="$(awk '{print $1}' "${ASSET_DIR}/opensre_${VERSION}_linux-x64.tar.gz.sha256")"
linux_arm64_sha="$(awk '{print $1}' "${ASSET_DIR}/opensre_${VERSION}_linux-arm64.tar.gz.sha256")"
darwin_x64_sha="$(awk '{print $1}' "${ASSET_DIR}/opensre_${VERSION}_darwin-x64.tar.gz.sha256")"
darwin_arm64_sha="$(awk '{print $1}' "${ASSET_DIR}/opensre_${VERSION}_darwin-arm64.tar.gz.sha256")"
tap_dir="$(mktemp -d)/homebrew-tap"
if [ -n "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]; then
git clone "https://x-access-token:${HOMEBREW_TAP_GITHUB_TOKEN}@github.com/Tracer-Cloud/homebrew-tap.git" "$tap_dir"
else
git clone "https://github.com/Tracer-Cloud/homebrew-tap.git" "$tap_dir"
fi
python3 - "$tap_dir/Formula/opensre.rb" "$VERSION" "$darwin_arm64_sha" "$darwin_x64_sha" "$linux_arm64_sha" "$linux_x64_sha" <<'PY'
from __future__ import annotations
import re
import sys
from pathlib import Path
formula_path = Path(sys.argv[1])
version = sys.argv[2]
darwin_arm64_sha = sys.argv[3]
darwin_x64_sha = sys.argv[4]
linux_arm64_sha = sys.argv[5]
linux_x64_sha = sys.argv[6]
text = formula_path.read_text()
text = re.sub(r'version "[^"]+"', f'version "{version}"', text, count=1)
replacements = {
r'(opensre_#\{version\}_darwin-arm64\.tar\.gz"\n\s*sha256 ")[^"]+(")': darwin_arm64_sha,
r'(opensre_#\{version\}_darwin-x64\.tar\.gz"\n\s*sha256 ")[^"]+(")': darwin_x64_sha,
r'(opensre_#\{version\}_linux-arm64\.tar\.gz"\n\s*sha256 ")[^"]+(")': linux_arm64_sha,
r'(opensre_#\{version\}_linux-x64\.tar\.gz"\n\s*sha256 ")[^"]+(")': linux_x64_sha,
}
for pattern, sha in replacements.items():
text, count = re.subn(pattern, rf'\g<1>{sha}\g<2>', text, count=1)
if count != 1:
raise SystemExit(f"Failed to update checksum with pattern: {pattern}")
formula_path.write_text(text)
PY
cd "$tap_dir"
if git diff --quiet -- Formula/opensre.rb; then
echo "Homebrew tap formula already up to date for version ${VERSION}."
exit 0
fi
echo "=== Formula diff ==="
git diff Formula/opensre.rb
echo "===================="
if [ "$DRY_RUN" = "1" ]; then
echo "DRY_RUN=1 — not committing or pushing."
exit 0
fi
if [ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]; then
echo "HOMEBREW_TAP_GITHUB_TOKEN is required to push." >&2
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/opensre.rb
git commit -m "chore: update opensre formula to ${VERSION}"
git push origin HEAD:main
echo "Pushed homebrew-tap update for version ${VERSION}."
+17
View File
@@ -0,0 +1,17 @@
# GitHub Actions (maintainer reference)
Internal notes for repository automation under `.github/workflows/`. Not published on the docs site.
## Workflows
| Workflow | Purpose |
| -------- | ------- |
| [`ci.yml`](ci.yml) | PR/push quality gates and sharded pytest |
| [`ci-labels-windows.yml`](ci-labels-windows.yml) | Optional Windows CI (`ci:windows` label) |
| [`codeql.yml`](codeql.yml) | CodeQL security analysis |
| [`greptile-pr-reminder.yml`](greptile-pr-reminder.yml) | Greptile review nudge on PR open |
| [`celebrate-merged-pr.yml`](celebrate-merged-pr.yml) | Post-merge celebration comment |
| [`good-first-issue-assign.yml`](good-first-issue-assign.yml) | Auto-assign good first issues |
| [`release.yml`](release.yml) | Release builds and artifacts |
See [CI.md](../../CI.md) for local parity commands before push.
+176
View File
@@ -0,0 +1,176 @@
name: Benchmark image — build + push to ECR (any adapter)
# Adapter-agnostic image build. The bench image carries the full
# ``tests/benchmarks/`` tree, so every registered adapter ships in the
# same image. ``benchmark-run.yml`` selects which adapter actually runs
# via its ``config`` input. See ``tests/benchmarks/_framework/registry.py``
# for the registration contract.
# Builds tests/benchmarks/cloudopsbench/infra/Dockerfile.bench and pushes the resulting image to the
# opensre-bench ECR repository. The bench container is what
# `Benchmark run (manual)` invokes on AWS Fargate, so this workflow's
# output is the input to that one.
#
# Triggered automatically on changes to the bench code (or the Dockerfile
# itself) and manually via workflow_dispatch.
#
# After the image is pushed, update the task definition by re-applying the
# Terraform with the new tag:
#
# cd tests/benchmarks/cloudopsbench/infra
# terraform apply -var="image_tag=<TAG>"
#
# (Or wire a follow-up step into this workflow to update task definition
# automatically — out of scope for v1.)
#
# Tag format: short git SHA (`git rev-parse --short HEAD`). Stable, unique
# per commit, recognizable in `aws ecr describe-images` output. ECR is
# IMMUTABLE — a tag pushed once cannot be overwritten, so re-running the
# workflow on the same commit just re-tags (no-op layer push).
on:
workflow_dispatch:
inputs:
tag:
description: Image tag to push (default = short git SHA)
required: false
type: string
push:
branches:
- main
paths:
- "tests/benchmarks/cloudopsbench/infra/Dockerfile.bench"
- "tests/benchmarks/cloudopsbench/infra/Dockerfile.bench.dockerignore"
- "pyproject.toml"
- "uv.lock"
- "surfaces/**"
- "config/**"
- "core/**"
- "platform/deployment/**"
- "integrations/**"
- "platform/**"
- "tools/**"
- "tests/benchmarks/**"
- ".github/workflows/benchmark-image.yml"
permissions:
contents: read
id-token: write # required for AWS OIDC role assumption
concurrency:
# Allow one in-flight build per ref so two pushes in quick succession
# don't race ECR. The newer push cancels the older.
group: bench-image-${{ github.ref }}
cancel-in-progress: true
env:
AWS_REGION: us-east-1
# Account ID is repo-level configuration, not a secret. Set as a GitHub
# repository Variable (Settings > Secrets and variables > Actions >
# Variables) so it doesn't need to be edited in every workflow file
# when the bench moves accounts.
ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
ECR_REPOSITORY: opensre-bench
jobs:
build-and-push:
if: github.repository == 'Tracer-Cloud/opensre'
name: build + push
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
# Fetch enough history that `git rev-parse --short HEAD` is stable
fetch-depth: 1
- name: Resolve image tag
id: tag
env:
# Route `inputs.tag` through env so the shell treats it as DATA,
# not code. Without this, a workflow_dispatch caller could inject
# shell commands via a crafted `tag` value containing $() or
# backticks. See:
# https://securitylab.github.com/research/github-actions-untrusted-input/
INPUT_TAG: ${{ inputs.tag }}
run: |
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
else
TAG="$(git rev-parse --short HEAD)"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Will push: $ECR_REPOSITORY:$TAG"
- name: Configure AWS credentials (OIDC role assumption)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/opensre-bench-github-actions
role-session-name: github-actions-bench-image-${{ github.run_id }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Set up Docker Buildx
# Buildx adds multi-platform support + better cache control. Even
# for single-platform builds, it's the modern default.
uses: docker/setup-buildx-action@v3
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: tests/benchmarks/cloudopsbench/infra/Dockerfile.bench
platforms: linux/amd64
push: true
tags: |
${{ steps.ecr-login.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ steps.tag.outputs.tag }}
# Stamp the COMMIT SHA into the image so the runtime can read it
# via the OPENSRE_SHA env var. We use github.sha (the full 40-char
# commit being built), NOT steps.tag.outputs.tag (which resolves
# to the user-supplied inputs.tag like ``hotfix-june`` on
# workflow_dispatch and would stamp an unverifiable string as if
# it were a SHA). The image tag (for ECR naming) and OPENSRE_SHA
# (for provenance) are intentionally decoupled — the tag is for
# humans/operators; the SHA is for reproducibility. The runtime
# gate also validates SHA shape (7-40 lowercase hex chars), so
# even a manually-built image with a bad OPENSRE_SHA fails loudly.
build-args: |
OPENSRE_SHA=${{ github.sha }}
# GitHub Actions cache for Docker layers - speeds up rebuilds
# when only the source changes (deps stay in the cached layer).
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false # smaller manifest; matches ECR's tolerance for OCI
- name: Summarise
# Route step outputs through env so the shell sees them as DATA,
# not code. `steps.tag.outputs.tag` is derived verbatim from the
# `inputs.tag` user input — without env scoping, a workflow_dispatch
# caller could inject shell commands here even though the
# "Resolve image tag" step is hardened. Same risk applies to any
# tag-derived chain.
env:
REGISTRY: ${{ steps.ecr-login.outputs.registry }}
TAG: ${{ steps.tag.outputs.tag }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
echo "## Image pushed" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| field | value |" >> "$GITHUB_STEP_SUMMARY"
echo "| --- | --- |" >> "$GITHUB_STEP_SUMMARY"
echo "| Registry | \`$REGISTRY\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Repository | \`$ECR_REPOSITORY\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Tag | \`$TAG\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Digest | \`$DIGEST\` |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "To deploy this image:" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY"
echo "cd tests/benchmarks/cloudopsbench/infra" >> "$GITHUB_STEP_SUMMARY"
echo "terraform apply -var=\"image_tag=$TAG\"" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,135 @@
name: Benchmark image — promote tag to task definition (any adapter)
# Adapter-agnostic image promotion. Rebinds the ECS task definition to
# a specific image tag. The image carries every registered adapter; the
# config supplied to ``benchmark-run.yml`` picks which one runs.
#
# Manually-triggered workflow that runs `terraform apply -var=image_tag=<TAG>`
# in tests/benchmarks/cloudopsbench/infra/ to register a new ECS task definition revision pointing at
# the chosen ECR image. This is the privileged "deploy" step that comes
# between an image push (automatic) and a bench run (manual).
#
# Why not auto-promote on every image push? An image build is a code-change
# event. A task-def update is a deploy event. Decoupling them lets you
# stage many images and choose deliberately which one production runs.
#
# Trigger from the GitHub UI:
# Actions → "Benchmark image — promote tag to task definition" → Run
#
# Pre-reqs (one-time):
# - tests/benchmarks/cloudopsbench/infra/ Terraform applied at least once. The opensre-bench-github-actions
# OIDC role's permissions (ecs:RegisterTaskDefinition, state-bucket read/write,
# lock-table read/write, iam:PassRole) are granted by the github_actions_run_bench
# inline policy in tests/benchmarks/cloudopsbench/infra/iam_oidc.tf — any apply of that module attaches them.
# - Repo secrets seeded into AWS Secrets Manager
# - Repo vars set (AWS_ACCOUNT_ID etc., see tests/benchmarks/cloudopsbench/infra/AWS_BENCH_SETUP.md step 4)
on:
workflow_dispatch:
inputs:
image_tag:
description: 'ECR image tag to promote (e.g. 3792493)'
required: true
permissions:
contents: read
id-token: write # required for AWS OIDC role assumption
concurrency:
group: benchmark-promote-image
cancel-in-progress: false
jobs:
promote:
name: terraform apply image_tag=${{ inputs.image_tag }}
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
AWS_REGION: us-east-1
steps:
- name: Verify required repo variables
env:
AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
run: |
if [ -z "${AWS_ACCOUNT_ID:-}" ]; then
echo "::error::Missing repo variable AWS_ACCOUNT_ID. See tests/benchmarks/cloudopsbench/infra/AWS_BENCH_SETUP.md."
exit 1
fi
- uses: actions/checkout@v5
- name: Configure AWS credentials (OIDC role assumption)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/opensre-bench-github-actions
role-session-name: bench-promote-${{ github.run_id }}
aws-region: us-east-1
- name: Verify image tag exists in ECR
# Fail loudly if the operator typos the tag, before Terraform tries
# to register a task definition pointing at a missing image.
env:
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
if ! aws ecr describe-images \
--repository-name opensre-bench \
--image-ids imageTag="$IMAGE_TAG" \
>/dev/null 2>&1; then
echo "::error::Image tag $IMAGE_TAG not found in ECR repo opensre-bench."
echo "::error::Push it first via 'Benchmark image — build + push to ECR'."
exit 1
fi
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.5
- name: Terraform init
working-directory: tests/benchmarks/cloudopsbench/infra
run: terraform init -input=false
- name: Terraform apply
# Plan is captured in the workflow log; review it in the run page.
# -auto-approve is intentional — this workflow IS the human approval
# (the operator triggered it manually with a specific tag).
#
# -target=aws_ecs_task_definition.bench scopes apply to the task
# definition (and its data-source / role dependencies) only. Without
# this, every dispatch tries to reconcile every resource in the
# module — IAM, S3, ECR, etc. — against whatever ref the workflow
# was dispatched from. Out-of-band local applies cause that
# reconciliation to attempt rollbacks the workflow role isn't
# permitted to perform (iam:DetachRolePolicy, iam:PutRolePolicy),
# failing the run even when the task-def update itself succeeded.
working-directory: tests/benchmarks/cloudopsbench/infra
env:
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
terraform apply -input=false -auto-approve \
-target=aws_ecs_task_definition.bench \
-var="image_tag=$IMAGE_TAG"
- name: Surface the new task definition revision in the job summary
working-directory: tests/benchmarks/cloudopsbench/infra
env:
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
TASK_DEF_ARN=$(terraform output -raw task_definition_arn)
IMAGE_URI=$(aws ecs describe-task-definition \
--task-definition "$TASK_DEF_ARN" \
--query 'taskDefinition.containerDefinitions[0].image' \
--output text)
{
echo "## Image promoted"
echo ""
echo "- Promoted tag: \`$IMAGE_TAG\`"
echo "- New task definition ARN: \`$TASK_DEF_ARN\`"
echo "- Image now in task definition: \`$IMAGE_URI\`"
echo ""
echo "### Next step"
echo ""
echo "Trigger **Benchmark — run on Fargate** to launch a run against this image."
} >> "$GITHUB_STEP_SUMMARY"
+45
View File
@@ -0,0 +1,45 @@
name: Update Benchmark README Section
on:
push:
branches:
- main
paths:
- "docs/benchmarks/results.md"
workflow_dispatch:
jobs:
update-benchmark-readme:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Update README benchmark section
run: uv run python -m tests.benchmarks.toolcall_model_benchmark.readme_updater
- name: Commit changes
run: |
git config user.name "benchmark-readme-action"
git config user.email "benchmark-readme-action@noreply.github.com"
git add README.md
git diff --staged --quiet || git commit -m "docs(benchmark): update README with latest benchmark results"
git pull --rebase origin main
git push
+191
View File
@@ -0,0 +1,191 @@
name: Benchmark — run on Fargate (any adapter)
# Manually-triggered benchmark run on AWS Fargate.
#
# Adapter-agnostic. This workflow does NOT know or care which benchmark
# is running. The ``config`` input names a YAML file; the YAML names an
# adapter (``benchmark: <name>``); the framework's registry resolves it
# to a registered ``BenchmarkAdapter`` subclass. The same workflow runs
# any adapter packaged into the bench image — CloudOpsBench today,
# ToolCallBench / future adapters tomorrow, with zero changes
# here. See ``tests/benchmarks/_framework/registry.py``.
#
# Why ECS RunTask instead of a GitHub-hosted ubuntu runner: a full bench
# grid runs for hours and writes hundreds of MB of artifacts — Fargate
# handles it cleanly, has AWS Secrets Manager wired in via tests/benchmarks/cloudopsbench/infra/,
# and writes to the per-run S3 bucket. The workflow's job is just to
# launch the task and print where to watch.
#
# Trigger from the GitHub UI:
# Actions → "Benchmark run (manual)" → Run workflow → fill inputs
#
# Pre-reqs (one-time, see tests/benchmarks/cloudopsbench/infra/README.md):
# - tests/benchmarks/cloudopsbench/infra/ Terraform applied
# - Bench image pushed to ECR via benchmark-image.yml
# - Repo secrets seeded into AWS Secrets Manager via benchmark-seed-secret.yml
# - Repo variables set:
# AWS_ACCOUNT_ID, BENCH_ECS_CLUSTER, BENCH_TASK_DEFINITION_FAMILY,
# BENCH_SUBNET_IDS (comma-separated), BENCH_SECURITY_GROUP_ID
on:
workflow_dispatch:
inputs:
config:
# No adapter-specific default — the operator must point at a
# specific config. Leaving the field empty surfaces the choice
# rather than silently dispatching the CloudOpsBench smoke. The
# path is relative to the container's repo root. Examples:
# tests/benchmarks/cloudopsbench/configs/cloudopsbench_smoke.yml
# tests/benchmarks/<adapter>/configs/<config>.yml
description: 'Path to YAML config inside the container (e.g. tests/benchmarks/<adapter>/configs/<config>.yml)'
required: true
dev_mode:
description: 'Dev mode (skip integrity gates, no pre-reg needed)'
type: boolean
default: true
# Note: there is intentionally no `image_tag` input here. ECS RunTask
# container overrides do NOT support overriding the image URI — that lives
# on the task definition itself. The image actually pulled is whatever
# was registered the last time `terraform apply -var=image_tag=<tag>` ran
# in tests/benchmarks/cloudopsbench/infra/. Adding a workflow input here would silently mislead
# operators into thinking they control the image per run. To change the
# image, push it via `Benchmark image — build + push to ECR`, then re-apply
# Terraform with the new tag.
permissions:
contents: read
id-token: write # required for AWS OIDC role assumption
concurrency:
group: benchmark-run
cancel-in-progress: false
jobs:
launch:
name: launch ECS task
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
timeout-minutes: 15 # we only LAUNCH the task; we don't wait for it
env:
AWS_REGION: us-east-1
steps:
- name: Verify required repo variables
# Fail loudly BEFORE the AWS auth step if any required repo variable
# is missing. Without this, an unset var would surface downstream as
# an opaque error like "Task Definition can not be blank" from
# describe-task-definition. See tests/benchmarks/cloudopsbench/infra/AWS_BENCH_SETUP.md step 4 for how
# to set these.
env:
AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
BENCH_ECS_CLUSTER: ${{ vars.BENCH_ECS_CLUSTER }}
BENCH_TASK_DEFINITION_FAMILY: ${{ vars.BENCH_TASK_DEFINITION_FAMILY }}
BENCH_SUBNET_IDS: ${{ vars.BENCH_SUBNET_IDS }}
BENCH_SECURITY_GROUP_ID: ${{ vars.BENCH_SECURITY_GROUP_ID }}
run: |
missing=()
for var in AWS_ACCOUNT_ID BENCH_ECS_CLUSTER BENCH_TASK_DEFINITION_FAMILY \
BENCH_SUBNET_IDS BENCH_SECURITY_GROUP_ID; do
if [ -z "${!var:-}" ]; then
missing+=("$var")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
echo "::error::Missing repo variable(s): ${missing[*]}"
echo "::error::Set them under Settings → Secrets and variables → Actions → Variables."
echo "::error::Values come from \`cd tests/benchmarks/cloudopsbench/infra && terraform output\` — see tests/benchmarks/cloudopsbench/infra/AWS_BENCH_SETUP.md step 4."
exit 1
fi
echo "All 5 required repo variables are set."
- name: Configure AWS credentials (OIDC role assumption)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/opensre-bench-github-actions
role-session-name: bench-run-${{ github.run_id }}
aws-region: us-east-1
- name: Launch ECS RunTask
# Inputs bound to env vars so the shell does the interpolation —
# GitHub Actions template syntax (${{ ... }}) is expanded before
# the shell sees it, which would let a crafted input inject shell
# metacharacters. Env vars are quoted at use-site.
env:
BENCH_CONFIG: ${{ inputs.config }}
BENCH_DEV_FLAG: ${{ inputs.dev_mode == true && '--dev' || '' }}
CLUSTER: ${{ vars.BENCH_ECS_CLUSTER }}
TASK_FAMILY: ${{ vars.BENCH_TASK_DEFINITION_FAMILY }}
SUBNETS: ${{ vars.BENCH_SUBNET_IDS }}
SECURITY_GROUP: ${{ vars.BENCH_SECURITY_GROUP_ID }}
run: |
set -euo pipefail
# Surface the image the task definition will actually pull, so
# operators see the truth (not a workflow input that ECS ignores).
IMAGE_URI=$(aws ecs describe-task-definition \
--task-definition "$TASK_FAMILY" \
--query 'taskDefinition.containerDefinitions[0].image' \
--output text)
# Overrides JSON: container env vars passed at runtime. The
# container's entrypoint reads BENCH_CONFIG + BENCH_DEV_FLAG
# and invokes:
# uv run python -m tests.benchmarks._framework.cli run \
# "$BENCH_CONFIG" $BENCH_DEV_FLAG
OVERRIDES=$(jq -nc \
--arg config "$BENCH_CONFIG" \
--arg dev_flag "$BENCH_DEV_FLAG" \
'{
containerOverrides: [{
name: "bench",
environment: [
{name: "BENCH_CONFIG", value: $config},
{name: "BENCH_DEV_FLAG", value: $dev_flag}
]
}]
}')
TASK_ARN=$(aws ecs run-task \
--cluster "$CLUSTER" \
--task-definition "$TASK_FAMILY" \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SECURITY_GROUP],assignPublicIp=ENABLED}" \
--overrides "$OVERRIDES" \
--query 'tasks[0].taskArn' \
--output text)
if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" = "None" ]; then
echo "::error::run-task returned no taskArn"
exit 1
fi
TASK_ID="${TASK_ARN##*/}"
echo "task_arn=$TASK_ARN" >> "$GITHUB_OUTPUT"
echo "task_id=$TASK_ID" >> "$GITHUB_OUTPUT"
# Job summary — visible in the workflow run page
{
echo "## Bench run launched"
echo ""
echo "- Image (from task definition): \`$IMAGE_URI\`"
echo "- Config: \`$BENCH_CONFIG\`"
echo "- Dev mode: \`${BENCH_DEV_FLAG:-(off)}\`"
echo "- Task ARN: \`$TASK_ARN\`"
echo ""
echo "### Watch it"
echo ""
echo "Stream logs locally:"
echo ""
echo '```bash'
echo "aws logs tail /ecs/opensre-bench --follow"
echo '```'
echo ""
echo "Or via AWS Console:"
echo "https://us-east-1.console.aws.amazon.com/ecs/v2/clusters/$CLUSTER/tasks/$TASK_ID"
echo ""
echo "Artifacts land in the bench S3 bucket under \`runs/\` when the task completes."
echo ""
echo "_To change the running image: push a new tag via \`Benchmark image — build + push to ECR\`, then \`cd tests/benchmarks/cloudopsbench/infra && terraform apply -var=image_tag=<tag>\`._"
} >> "$GITHUB_STEP_SUMMARY"
+102
View File
@@ -0,0 +1,102 @@
name: Benchmark secret — seed GitHub repo secret into AWS Secrets Manager
# Manually-triggered workflow that copies a GitHub repo secret into AWS
# Secrets Manager at opensre-bench/llm/<secret>. The bench container reads
# its LLM API keys from Secrets Manager at runtime; this workflow is how
# you put them there without touching a developer laptop.
#
# The `secret` dropdown enforces which target is valid — must match one
# of the four IAM-granted secret ARNs in tests/benchmarks/cloudopsbench/infra/iam_oidc.tf
# (anthropic / openai / deepseek / hf_token).
#
# Why a workflow instead of a developer running `aws secretsmanager
# put-secret-value` locally: keeps keys off developer laptops + out of
# shell history, and centralizes rotation — rotate the value in the GH
# repo secret and re-run this workflow to propagate.
#
# Pre-reqs:
# - tests/benchmarks/cloudopsbench/infra/ Terraform has been applied (the secret resource exists)
# - Corresponding GitHub repo secret is set:
# ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY / HF_TOKEN
#
# Order of operations:
# 1. terraform-bench.yml (plan) on PR
# 2. terraform apply locally
# 3. this workflow (workflow_dispatch) — pick the secret to seed
# from the dropdown, repeat once per LLM provider key
on:
workflow_dispatch:
inputs:
secret:
description: Which secret to seed (must match the AWS resource name)
required: true
type: choice
options:
- anthropic_api_key
- openai_api_key
- deepseek_api_key
- hf_token
permissions:
contents: read
id-token: write # required for AWS OIDC role assumption
concurrency:
group: bench-seed-${{ inputs.secret }}
cancel-in-progress: false
jobs:
seed:
name: seed ${{ inputs.secret }}
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
env:
AWS_REGION: us-east-1
SECRET_ID: opensre-bench/llm/${{ inputs.secret }}
steps:
- name: Configure AWS credentials (OIDC role assumption)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/opensre-bench-github-actions
role-session-name: github-actions-seed-${{ inputs.secret }}-${{ github.run_id }}
aws-region: us-east-1
- name: Verify secret resource exists
run: |
if ! aws secretsmanager describe-secret --secret-id "$SECRET_ID" >/dev/null 2>&1; then
echo "::error::Secret $SECRET_ID not found. Run \`terraform apply\` in tests/benchmarks/cloudopsbench/infra/ first."
exit 1
fi
- name: Put secret value
# All four candidate values are bound at workflow level. The case
# statement picks the one matching the chosen target — unused
# values stay as masked env vars (GH redacts secret refs in logs).
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
HF_TOKEN: ${{ secrets.HF_TOKEN }}
TARGET: ${{ inputs.secret }}
run: |
case "$TARGET" in
anthropic_api_key) V="$ANTHROPIC_API_KEY" ;;
openai_api_key) V="$OPENAI_API_KEY" ;;
deepseek_api_key) V="$DEEPSEEK_API_KEY" ;;
hf_token) V="$HF_TOKEN" ;;
*)
echo "::error::Unknown target: $TARGET (dropdown should have prevented this)"
exit 1
;;
esac
if [ -z "$V" ]; then
echo "::error::GitHub repo secret for $TARGET is unset. Configure it under Settings > Secrets and variables > Actions."
exit 1
fi
aws secretsmanager put-secret-value \
--secret-id "$SECRET_ID" \
--secret-string "$V" >/dev/null
echo "Seeded $SECRET_ID (length=${#V})."
+37
View File
@@ -0,0 +1,37 @@
name: Celebrate merged pull requests
# Runs after a PR lands (merged into the target branch). Uses pull_request_target so the
# token can comment on merged PRs from forks; checkout is base/default only — no PR head.
on:
pull_request_target:
types: [closed]
permissions:
contents: read
pull-requests: write
jobs:
celebrate-merge:
name: Post-merge celebration comment
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.user.type != 'Bot' &&
github.event.pull_request.user.login != 'dependabot[bot]'
steps:
- uses: actions/checkout@v5
- name: Build celebration comment
id: celebrate
env:
DISCORD_INVITE_URL: https://discord.com/invite/opensre
CONTRIBUTOR_LOGIN: ${{ github.event.pull_request.user.login }}
run: python3 .github/scripts/merged_pr_celebration_message.py
- name: Comment on merged PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: >
gh pr comment "${{ github.event.pull_request.number }}"
--repo "${{ github.repository }}"
--body-file comment.md
+109
View File
@@ -0,0 +1,109 @@
# Optional Windows CI when PR label `ci:windows` is present.
# Separate workflow + concurrency group so Windows label runs are isolated.
name: CI Labels (Windows)
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Keep in sync with ci.yml — single source of truth for lint/typecheck/coverage targets.
PYTHON_SOURCE_PATHS: "config core integrations platform surfaces tools"
concurrency:
group: ci-labels-windows-${{ github.ref }}
cancel-in-progress: true
jobs:
windows-quality:
if: contains(github.event.pull_request.labels.*.name, 'ci:windows')
name: windows quality
runs-on: windows-latest
timeout-minutes: 15
continue-on-error: true
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Lint
run: uv run python -m ruff check ${{ env.PYTHON_SOURCE_PATHS }} tests/
- name: Format check
run: uv run python -m ruff format --check ${{ env.PYTHON_SOURCE_PATHS }} tests/
- name: Typecheck
run: uv run python -m mypy ${{ env.PYTHON_SOURCE_PATHS }}
windows-test:
if: contains(github.event.pull_request.labels.*.name, 'ci:windows')
name: windows test
needs: [windows-quality]
runs-on: windows-latest
timeout-minutes: 25
continue-on-error: true
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
TRACER_ORG_ID: ${{ secrets.TRACER_ORG_ID }}
TRACER_WEB_APP_URL: ${{ secrets.TRACER_WEB_APP_URL }}
GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }}
GCLOUD_HOSTED_METRICS_ID: ${{ secrets.GCLOUD_HOSTED_METRICS_ID }}
GCLOUD_HOSTED_METRICS_URL: ${{ secrets.GCLOUD_HOSTED_METRICS_URL }}
GCLOUD_HOSTED_LOGS_ID: ${{ secrets.GCLOUD_HOSTED_LOGS_ID }}
GCLOUD_HOSTED_LOGS_URL: ${{ secrets.GCLOUD_HOSTED_LOGS_URL }}
GCLOUD_RW_API_KEY: ${{ secrets.GCLOUD_RW_API_KEY }}
GCLOUD_OTLP_ENDPOINT: ${{ secrets.GCLOUD_OTLP_ENDPOINT }}
GCLOUD_OTLP_AUTH_HEADER: ${{ secrets.GCLOUD_OTLP_AUTH_HEADER }}
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run full tests
run: >-
uv run python -m pytest -n auto -v
--cov=config --cov=core --cov=platform.deployment.aws --cov=platform.deployment --cov=integrations --cov=platform --cov=surfaces --cov=tools
--cov-report=term-missing
--cov-report=html
--ignore=tests/e2e/kubernetes_local_alert_simulation
--ignore=tests/synthetic
-m "not synthetic and not integration"
+468
View File
@@ -0,0 +1,468 @@
name: CI
on:
push:
branches: [main]
paths:
- "surfaces/**"
- "config/**"
- "core/**"
- "platform/deployment/**"
- "integrations/**"
- "platform/**"
- "tools/**"
- "tests/**"
- "pyproject.toml"
- "uv.lock"
- "pytest.ini"
- "ruff.toml"
- "mypy.ini"
- "Makefile"
- ".importlinter"
- ".importlinter.strict"
- ".github/ci/**"
- ".github/workflows/ci.yml"
pull_request:
branches: [main]
paths:
- "surfaces/**"
- "config/**"
- "core/**"
- "platform/deployment/**"
- "integrations/**"
- "platform/**"
- "tools/**"
- "tests/**"
- "pyproject.toml"
- "uv.lock"
- "pytest.ini"
- "ruff.toml"
- "mypy.ini"
- "Makefile"
- ".importlinter"
- ".importlinter.strict"
- ".github/ci/**"
- ".github/workflows/ci.yml"
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PYTHON_SOURCE_PATHS: "config core integrations platform surfaces tools"
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: quality (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
continue-on-error: ${{ matrix.os == 'windows-latest' }}
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Lint
run: uv run python -m ruff check $PYTHON_SOURCE_PATHS tests/
- name: Format check
run: uv run python -m ruff format --check $PYTHON_SOURCE_PATHS tests/
- name: Typecheck
run: uv run python -m mypy $PYTHON_SOURCE_PATHS
- name: Import graph
run: uv run python .github/ci/check_imports.py --strict
# Full layered contracts (.importlinter.strict), not the minimal default
# .importlinter config used by ``make check-imports``.
- name: Import boundaries
run: uv run lint-imports --config .importlinter.strict
test:
name: test (${{ matrix.shard }})
strategy:
fail-fast: false
max-parallel: 5
matrix:
include:
- os: ubuntu-latest
shard: tools-runtime
# ``tests/core/agent`` is excluded here; it runs in cli-runtime,
# which pins openai for its live action-planning oracles.
extra_pytest_args: >-
--ignore=tests/core/agent
pytest_paths: >-
tests/tools
tests/core
tests/platform
tests/utils
tests/masking
tests/watch_dog
- os: ubuntu-latest
shard: cli-runtime
# Live shell action-agent contracts are validated on openai,
# matching interactive-shell-live.yml. The
# default anthropic tool-call model (haiku) cannot reliably perform
# compound action planning, so pin this shard to openai.
# ``tests/core/agent`` runs here because it contains the live
# action-planning oracles (``test_live_action_planning`` and
# ``test_live_turn_execution_oracle``) that need the openai pin —
# plus the static ``test_import_boundaries`` guard the surfaces
# restructure (#3299) added.
llm_provider: openai
pytest_paths: >-
tests/cli
tests/interactive_shell
tests/agent
tests/core/agent
tests/fleet_monitoring
tests/analytics
tests/tools/investigation
tests/delivery
tests/sandbox
tests/hermes
- os: ubuntu-latest
shard: integrations-and-misc
pytest_paths: >-
tests/integrations
tests/deployment
tests/benchmarks
tests/chaos_engineering
tests/shared
tests/config
tests/github_ci
tests/packaging
tests/scheduler
gateway/tests
- os: ubuntu-latest
shard: e2e-general
pytest_paths: >-
tests/e2e
extra_pytest_args: >-
--ignore=tests/e2e/kubernetes
--ignore=tests/e2e/openclaw
--ignore=tests/e2e/upstream_lambda
--ignore=tests/e2e/upstream_prefect_ecs_fargate
--ignore=tests/e2e/upstream_apache_flink_ecs
- os: ubuntu-latest
shard: e2e-provider-and-openclaw
pytest_paths: >-
tests/e2e/openclaw
tests/e2e/upstream_lambda
tests/e2e/upstream_prefect_ecs_fargate
tests/e2e/upstream_apache_flink_ecs
tests/e2e/kubernetes
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
# Per-shard LLM provider for live_llm contracts; defaults to anthropic.
LLM_PROVIDER: ${{ matrix.llm_provider || 'anthropic' }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }}
GCLOUD_HOSTED_METRICS_ID: ${{ secrets.GCLOUD_HOSTED_METRICS_ID }}
GCLOUD_HOSTED_METRICS_URL: ${{ secrets.GCLOUD_HOSTED_METRICS_URL }}
GCLOUD_HOSTED_LOGS_ID: ${{ secrets.GCLOUD_HOSTED_LOGS_ID }}
GCLOUD_HOSTED_LOGS_URL: ${{ secrets.GCLOUD_HOSTED_LOGS_URL }}
GCLOUD_RW_API_KEY: ${{ secrets.GCLOUD_RW_API_KEY }}
GCLOUD_OTLP_ENDPOINT: ${{ secrets.GCLOUD_OTLP_ENDPOINT }}
GCLOUD_OTLP_AUTH_HEADER: ${{ secrets.GCLOUD_OTLP_AUTH_HEADER }}
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Validate pytest marker
env:
# Fork PRs do not receive repository secrets; skip live_llm contracts
# (see interactive-shell-live.yml). Index a JSON string array so the
# value is always a marker string — never a bare boolean from
# ``a && 'x' || 'y'`` (boolean false → ``-m false`` → xdist
# ``N workers [0 items]``, which can still exit 0).
PYTEST_MARKER_EXPR: ${{ fromJSON('["not synthetic", "not (synthetic or live_llm)"]')[github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true] }}
run: |
echo "PYTEST_MARKER_EXPR=${PYTEST_MARKER_EXPR}"
case "${PYTEST_MARKER_EXPR}" in
"not synthetic"|"not (synthetic or live_llm)") ;;
*)
echo "Refusing unexpected PYTEST_MARKER_EXPR: '${PYTEST_MARKER_EXPR}'" >&2
exit 1
;;
esac
- name: Run tests
env:
COVERAGE_FILE: .coverage.${{ matrix.shard }}
PYTEST_MARKER_EXPR: ${{ fromJSON('["not synthetic", "not (synthetic or live_llm)"]')[github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true] }}
# Keep ``run: >`` (folded) so ``matrix.pytest_paths`` stays one argv list.
# A ``run: |`` + ``\`` rewrite can drop/split those paths and yield
# ``N workers [0 items]`` even when the marker is correct.
run: >
uv run python -m pytest -n auto -v
${{ matrix.pytest_paths }}
--cov=config
--cov=core
--cov=platform.deployment.aws --cov=platform.deployment
--cov=integrations
--cov=platform
--cov=surfaces
--cov=tools
--cov-report=
--ignore=tests/e2e/kubernetes_local_alert_simulation
--ignore=tests/synthetic
${{ matrix.extra_pytest_args }}
-m "${PYTEST_MARKER_EXPR}"
- name: Upload shard coverage data
if: >-
always() &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: coverage-data-${{ matrix.shard }}
path: .coverage.${{ matrix.shard }}*
if-no-files-found: error
include-hidden-files: true
retention-days: 7
coverage-report:
name: coverage-report
runs-on: ubuntu-latest
needs: [test]
if: >-
always() &&
github.event_name == 'push' &&
github.ref == 'refs/heads/main'
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Download shard coverage artifacts
uses: actions/download-artifact@v4
with:
pattern: coverage-data-*
path: ./
merge-multiple: true
- name: Combine coverage and build report
run: |
uv run python -m coverage combine .
uv run python -m coverage html
- name: Upload combined coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report-ubuntu-latest
path: |
htmlcov/
.coverage
retention-days: 7
test-kubernetes:
runs-on: ubuntu-latest
continue-on-error: true
timeout-minutes: 15
if: >-
github.repository == 'Tracer-Cloud/opensre' &&
(github.event_name == 'push' ||
(
github.event_name == 'pull_request' &&
(
contains(github.event.pull_request.title, 'k8s') ||
contains(github.event.pull_request.title, 'kubernetes')
)
))
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run Kubernetes tests
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: uv run python -m tests.e2e.kubernetes.test_local
should-run-thorough:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'Tracer-Cloud/opensre'
outputs:
thorough: ${{ steps.filter.outputs.thorough }}
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
thorough:
- ".github/workflows/ci.yml"
- "pyproject.toml"
- "uv.lock"
- "platform/deployment/**"
- "integrations/**"
- "tools/investigation/**"
- "tools/**"
- "tests/e2e/cloudwatch_demo/**"
- "tests/e2e/upstream_lambda/**"
- "tests/e2e/upstream_prefect_ecs_fargate/**"
- "tests/e2e/upstream_apache_flink_ecs/**"
test-thorough:
runs-on: ubuntu-latest
needs: [test, should-run-thorough]
if: >-
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
github.repository == 'Tracer-Cloud/opensre' &&
needs.should-run-thorough.outputs.thorough == 'true'
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: cloudwatch-demo
command: python3 -m tests.e2e.cloudwatch_demo.test_aws
- name: upstream-lambda
command: python3 -m tests.e2e.upstream_lambda.test_agent_e2e
- name: prefect-ecs-fargate
command: python3 -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e
- name: flink-ecs
command: python3 -m tests.e2e.upstream_apache_flink_ecs.test_agent_e2e
name: test-thorough (${{ matrix.name }})
env:
# Thorough e2e suites make live LLM calls; run them on openai because the
# default anthropic account is credit-exhausted. Requires a valid, funded
# OPENAI_API_KEY secret.
LLM_PROVIDER: openai
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
TRACER_API_URL: ${{ secrets.TRACER_API_URL }}
TRACER_INGEST_TOKEN: ${{ secrets.TRACER_INGEST_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
AWS_DEFAULT_REGION: us-east-1
GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }}
GCLOUD_HOSTED_METRICS_ID: ${{ secrets.GCLOUD_HOSTED_METRICS_ID }}
GCLOUD_HOSTED_METRICS_URL: ${{ secrets.GCLOUD_HOSTED_METRICS_URL }}
GCLOUD_HOSTED_LOGS_ID: ${{ secrets.GCLOUD_HOSTED_LOGS_ID }}
GCLOUD_HOSTED_LOGS_URL: ${{ secrets.GCLOUD_HOSTED_LOGS_URL }}
GCLOUD_RW_API_KEY: ${{ secrets.GCLOUD_RW_API_KEY }}
CLOUDWATCH_VERIFY_LOGS: ${{ matrix.name == 'cloudwatch-demo' && '0' || '1' }}
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Install tracer CLI
run: |
curl -sSL https://install.tracer.cloud | sh -s user_36fbN6K6FwEgJFHsQv1pUByo8K6
- name: Initialize tracer (optional)
run: |
sudo tracer init --token ${{ secrets.JWT_TOKEN }}
continue-on-error: true
timeout-minutes: 2
- name: Run test
run: uv run ${{ matrix.command }}
timeout-minutes: 30
+118
View File
@@ -0,0 +1,118 @@
name: Closed-loop learning (weekly miss triage)
# Weekly reminder workflow for the closed-loop learning process documented
# in docs/closed-loop-learning.mdx. Runs at 09:00 UTC every Monday and
# also supports manual triggering.
#
# The workflow does not consume the per-user ``~/.opensre/misses.jsonl``
# store directly (that lives on engineer machines, not on the runner).
# Its purpose is to:
# 1) Open or refresh a tracking issue that nudges the on-call engineer
# to run ``opensre misses stats --since 7d`` and then
# ``opensre misses export --since 7d --top 10 --out tests/benchmarks/production_misses/``.
# 2) Verify the ``misses_command`` CLI surface is still wired and exits
# cleanly, so the weekly process is not blocked by a CLI regression.
on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch: {}
permissions:
contents: read
issues: write
jobs:
smoke:
name: Verify opensre misses CLI
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Smoke test misses CLI
run: |
uv run opensre misses --help
uv run opensre misses list --json
uv run opensre misses stats --json
remind:
name: Open weekly triage reminder
needs: smoke
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- name: Open weekly tracking issue (close last week's first)
uses: actions/github-script@v7
with:
script: |
// One issue per week. Previous week's tracker is closed before
// opening this week's so triage history is per-week (GitHub's
// natural unit) rather than one ever-growing comment thread.
// Uses the existing "pending triage" label so we do not
// auto-create a new label on first run.
const week = new Date().toISOString().slice(0, 10);
const titlePrefix = "Weekly closed-loop learning triage";
const title = `${titlePrefix} — week of ${week}`;
const body = [
"Weekly nudge per docs/closed-loop-learning.mdx.",
"",
"Steps for this week's on-call:",
"- [ ] `opensre misses stats --since 7d`",
"- [ ] Review recurring `(alert, taxonomy)` pairs",
"- [ ] `opensre misses export --since 7d --top 10 --out tests/benchmarks/production_misses/`",
"- [ ] Open a PR labelled `benchmark` with the new scenarios",
"- [ ] Trigger the benchmark workflow on the PR branch",
].join("\n");
const { data: openIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
labels: "pending triage",
per_page: 100,
});
const stale = openIssues.filter(
(i) => i.title.startsWith(titlePrefix) && i.title !== title,
);
for (const issue of stale) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: "closed",
state_reason: "completed",
});
}
const existing = openIssues.find((i) => i.title === title);
if (existing) {
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ["pending triage"],
});
+43
View File
@@ -0,0 +1,43 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
# Skip CodeQL for docs-only PRs (Markdown/MDX and the docs site tree) to
# save CI minutes. Not a required check, so skipping cannot block merge.
# `push` to main and the weekly schedule keep full coverage regardless.
paths-ignore:
- '**/*.md'
- '**/*.mdx'
- 'docs/**'
schedule:
- cron: "0 6 * * 1"
permissions:
contents: read
security-events: write
actions: read
jobs:
analyze:
if: github.repository == 'Tracer-Cloud/opensre'
name: Analyze (python)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: python
build-mode: none
config-file: .github/codeql/codeql-config.yml
queries: security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: /language:python
+148
View File
@@ -0,0 +1,148 @@
name: Docker Publish
on:
release:
types: [published]
schedule:
- cron: "30 2 * * *"
workflow_dispatch:
inputs:
tag:
description: "Release tag to build (e.g. v0.1.2026.7.8). Defaults to the latest release."
required: false
type: string
permissions:
contents: read
packages: write
concurrency:
group: docker-publish
cancel-in-progress: false
jobs:
build-and-push:
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve release tag and image tags
id: meta
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
DISPATCH_TAG: ${{ inputs.tag }}
shell: bash
run: |
set -euo pipefail
image="ghcr.io/${GITHUB_REPOSITORY,,}"
latest_tag="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)"
case "$EVENT_NAME" in
release)
if [ "$RELEASE_PRERELEASE" = "true" ]; then
echo "Skipping prerelease $RELEASE_TAG (rolling main builds are not published to GHCR)."
echo "build=false" >> "$GITHUB_OUTPUT"
exit 0
fi
tag="$RELEASE_TAG"
;;
workflow_dispatch)
tag="${DISPATCH_TAG:-$latest_tag}"
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
;;
*)
tag="$latest_tag"
;;
esac
version="${tag#v}"
# The scheduled run rebuilds the newest release; skip it when that
# version is already in the registry (e.g. the release event or a
# dispatch already published it).
if [ "$EVENT_NAME" = "schedule" ] \
&& docker manifest inspect "${image}:${version}" >/dev/null 2>&1; then
echo "Image ${image}:${version} already published; nothing to do."
echo "build=false" >> "$GITHUB_OUTPUT"
exit 0
fi
tags="${image}:${version}"
if [ "$tag" = "$latest_tag" ]; then
tags="${tags}"$'\n'"${image}:latest"
else
echo "Tag $tag is older than latest release $latest_tag; not moving :latest."
fi
{
echo "build=true"
echo "tag=${tag}"
echo "version=${version}"
echo "image=${image}"
echo "tags<<EOF"
echo "$tags"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v5
if: steps.meta.outputs.build == 'true'
with:
ref: ${{ steps.meta.outputs.tag }}
- name: Sync release version into pyproject.toml
if: steps.meta.outputs.build == 'true'
shell: bash
run: python3 platform/packaging/sync_release_version.py --tag "${{ steps.meta.outputs.tag }}"
- name: Set up QEMU
if: steps.meta.outputs.build == 'true'
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
if: steps.meta.outputs.build == 'true'
uses: docker/setup-buildx-action@v3
- name: Build and push image
if: steps.meta.outputs.build == 'true'
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.title=opensre
org.opencontainers.image.description=OpenSRE unified image (MODE=web FastAPI health app, MODE=gateway Telegram gateway)
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.version=${{ steps.meta.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke test published image
if: steps.meta.outputs.build == 'true'
shell: bash
run: |
set -euo pipefail
image="${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.version }}"
docker pull "$image"
version_output="$(docker run --rm --entrypoint opensre "$image" --version)"
printf '%s\n' "$version_output"
case "$version_output" in
*"${{ steps.meta.outputs.version }}"*) ;;
*)
echo "Image version mismatch: expected ${{ steps.meta.outputs.version }}" >&2
exit 1
;;
esac
+186
View File
@@ -0,0 +1,186 @@
# OpenClaw end-to-end CI.
#
# Triggers, in order of intent:
# 1. PRs that touch OpenClaw-relevant paths (auto, primary signal)
# 2. Pushes to main that touch the same paths (post-merge regression catch)
# 3. Daily schedule at 06:00 UTC (catches environment drift —
# OpenClaw version bumps, MCP SDK
# breaks, npm install regressions)
# 4. Adding the `ci:openclaw` label to a PR (manual re-run on matching PRs)
# 5. Workflow_dispatch from the Actions UI (manual force-run on any ref,
# no PR needed)
#
# The path filter at the trigger level is the primary gate, so non-OpenClaw PRs
# pay no CI cost. The label is only useful as a re-trigger on PRs that already
# matched paths; for force-running against an unrelated branch, use the manual
# dispatch button in the Actions UI.
name: CI (OpenClaw E2E)
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- "integrations/openclaw.py"
- "tools/OpenClawMCPTool/**"
- "tests/e2e/openclaw/**"
- "tests/utils/alert_factory/**"
- ".github/workflows/e2e-openclaw.yml"
- "docs/openclaw.mdx"
- ".tool-versions"
push:
branches: [main]
paths:
- "integrations/openclaw.py"
- "tools/OpenClawMCPTool/**"
- "tests/e2e/openclaw/**"
- "tests/utils/alert_factory/**"
- ".github/workflows/e2e-openclaw.yml"
- "docs/openclaw.mdx"
- ".tool-versions"
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
concurrency:
group: ci-openclaw-${{ github.ref }}
cancel-in-progress: true
jobs:
# Quality gates (lint / format-check / mypy) are run by the main
# ``ci.yml`` workflow for every PR. We don't duplicate them here —
# this workflow's sole job is the OpenClaw e2e suite. If you reach
# this workflow, your PR has already cleared the quality gates.
openclaw-test:
if: github.repository == 'Tracer-Cloud/opensre'
name: openclaw test
runs-on: ubuntu-latest
timeout-minutes: 25
# No ``env:`` block by design. CI runs the boot + use-case
# scenarios only — the full-RCA sub-tests call an LLM, cost API
# credits, and take ~25s each, so we deliberately don't wire any
# LLM secret in. They auto-skip via
# ``LLM_CREDENTIAL_SKIP_REASON`` when no key is present in env.
# Contributors with ANTHROPIC_API_KEY / OPENAI_API_KEY /
# GEMINI_API_KEY set locally still run them via
# ``make test-openclaw``. Lang Smith env vars stay scoped to the
# local-RCA path for the same reason — no value tracing the
# use-case-only CI runs.
steps:
- uses: actions/checkout@v5
- name: Set up Node (OpenClaw requires >=22.12; version pinned in .tool-versions)
uses: actions/setup-node@v4
with:
node-version-file: ".tool-versions"
- name: Cache npm downloads
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-openclaw-npm-${{ hashFiles('.tool-versions') }}
restore-keys: |
${{ runner.os }}-openclaw-npm-
- name: Install OpenClaw CLI
# ``continue-on-error: true`` so an npm-registry hiccup, a package-name
# rename on OpenClaw's side, or a network blip does not kill the whole
# workflow. The "Probe OpenClaw availability" step below catches the
# failure and downgrades the run to "tests skipped" with a clear
# explanation rather than crashing.
continue-on-error: true
id: install_openclaw
run: npm install -g openclaw --prefer-offline --no-audit
- name: Probe OpenClaw availability
# Whether or not ``npm install`` succeeded, check if ``openclaw`` is
# actually runnable. Failure modes we surface explicitly:
# - CLI not installed (npm step failed silently)
# - CLI installed but Node version mismatch (older runner image)
# - CLI installed but the bundled JS errored on first run
# ``openclaw_runnable`` env var feeds the next step's gating.
run: |
set +e
if ! command -v openclaw >/dev/null 2>&1; then
echo "::warning::openclaw CLI is not on PATH (npm install may have failed); the e2e suite will be skipped this run."
echo "openclaw_runnable=false" >> "$GITHUB_ENV"
exit 0
fi
version_output="$(openclaw --version 2>&1)"
if [ $? -ne 0 ]; then
echo "::warning::openclaw is on PATH but errors when run:"
echo "$version_output"
echo "::warning::Skipping e2e suite this run."
echo "openclaw_runnable=false" >> "$GITHUB_ENV"
exit 0
fi
echo "✓ OpenClaw verified: $version_output"
echo "openclaw_runnable=true" >> "$GITHUB_ENV"
- name: Note LLM credential policy
# CI deliberately does not wire any LLM key, so the full-RCA
# sub-tests skip every run. This step is informational — it
# makes the policy obvious in the run log instead of leaving a
# silent skip that a reviewer has to dig for. Run RCA locally
# with ``make test-openclaw`` if you have a key configured.
run: |
echo "︎ CI policy: full-RCA sub-tests are skipped (LLM credentials intentionally not wired)."
echo " Boot + use-case sub-tests still run and gate the merge."
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run OpenClaw E2E suite
# Skips with a clear message if the OpenClaw probe failed earlier,
# so the workflow finishes green with a documented "couldn't run"
# status rather than a confusing red on the install step.
run: |
if [ "$openclaw_runnable" != "true" ]; then
echo "::warning::Skipping pytest invocation — openclaw CLI unavailable in this environment. See earlier warnings."
exit 0
fi
uv run python -m pytest -m e2e -v tests/e2e/openclaw/
- name: Upload gateway logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openclaw-gateway-logs
path: /tmp/openclaw-e2e-logs/
if-no-files-found: ignore
retention-days: 7
- name: Summary
# Always runs, regardless of earlier step outcomes. The probe
# step above downgrades a missing-CLI / wrong-Node environment
# to "tests skipped" rather than failing the workflow, so this
# summary is the one place a reviewer sees why fewer tests ran
# than expected. Real pytest failures still fail the job.
if: always()
run: |
echo "### OpenClaw E2E run summary"
echo " OpenClaw runnable: ${openclaw_runnable:-unknown}"
echo " Full-RCA sub-tests: skipped by CI policy (no LLM key wired)"
echo " Run RCA locally with: make test-openclaw"
@@ -0,0 +1,52 @@
# When someone with zero merged PRs in this repo (and not OWNER/MEMBER/COLLABORATOR) comments
# on an open issue labeled `good first issue`, assign them and reply — only if the issue has no
# assignees yet (first eligible commenter wins; includes when a human pre-assigned someone).
# issue_comment also fires for PR review threads; those payloads include issue.pull_request.
name: Good first issue — auto-assign
on:
issue_comment:
types: [created]
concurrency:
group: good-first-issue-assign-${{ github.repository }}-${{ github.event.issue.number }}
cancel-in-progress: false
permissions:
contents: read
issues: write
pull-requests: read
jobs:
assign:
name: Assign new contributor on good first issue
runs-on: ubuntu-latest
if: >-
github.event.issue.state == 'open' &&
github.event.comment.user.type != 'Bot' &&
!github.event.issue.pull_request
steps:
- uses: actions/checkout@v5
- name: Decide assign + notice body
id: decide
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 .github/scripts/good_first_issue_assign.py
- name: Add assignee and notify
if: steps.decide.outputs.should_assign == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO_FULL: ${{ github.repository }}
COMMENTER_LOGIN: ${{ github.event.comment.user.login }}
run: |
gh issue edit "${ISSUE_NUMBER}" \
--repo "${REPO_FULL}" \
--add-assignee "${COMMENTER_LOGIN}"
gh issue comment "${ISSUE_NUMBER}" \
--repo "${REPO_FULL}" \
--body-file assign_comment.md
@@ -0,0 +1,46 @@
name: Greptile PR reminder
# One-time nudge when a PR is opened or reopened. Uses pull_request_target so the token can
# comment on PRs from forks. Does not checkout the PR head — only posts a static reminder (see
# SECURITY: pull_request_target guidance in GitHub Actions docs).
on:
pull_request_target:
types: [opened, reopened]
permissions:
contents: read
pull-requests: write
jobs:
remind:
name: Post Greptile review reminder
runs-on: ubuntu-latest
if: github.event.pull_request.user.type != 'Bot'
steps:
- name: Comment with Greptile guidance
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
base_ref="${BASE_REF}"
contributing_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/blob/${base_ref}/CONTRIBUTING.md#greptile-code-review"
pr="${PR_NUMBER}"
repo="${GITHUB_REPOSITORY}"
{
echo '### Greptile code review'
echo ''
echo "This repo uses **Greptile** for automated review. Before merge, aim for **Confidence Score: 5/5** with **zero unresolved** review threads — see [CONTRIBUTING.md](${contributing_url})."
echo ''
echo '**Run a review** — add a PR comment with:'
echo ''
echo '```'
echo '@greptile review'
echo '```'
echo ''
echo 'Give it **~5-10 minutes** (sometimes longer) for results, then fix feedback and re-trigger until you reach **Confidence Score: 5/5**.'
echo ''
echo '**Optional:** automate with the [greploop skill](https://skills.sh/greptileai/skills/greploop).'
} > comment.md
gh pr comment "$pr" --repo "$repo" --body-file comment.md
+50
View File
@@ -0,0 +1,50 @@
name: Hermes Tests
on:
pull_request:
paths:
- "tests/synthetic/hermes_rca/**"
- "tests/e2e/hermes/**"
- "tests/synthetic/mock_hermes_backend/**"
- "tools/HermesSessionEvidenceTool/**"
- ".github/workflows/hermes-tests.yml"
- "Makefile"
- "pytest.ini"
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
jobs:
synthetic:
name: Hermes synthetic offline
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run Hermes synthetic tests
run: uv run pytest tests/synthetic/hermes_rca -q
- name: Run Hermes offline suite
run: uv run python -m tests.synthetic.hermes_rca.run_suite --offline-only
e2e:
name: Hermes e2e/meta
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run Hermes e2e/meta tests
run: uv run pytest tests/e2e/hermes -q
@@ -0,0 +1,160 @@
name: Interactive Shell Live (PR + post-merge)
on:
pull_request:
branches: [main]
paths:
- "core/agent/**"
- "core/agent_harness/**"
- "tests/core/agent/**"
- "surfaces/interactive_shell/**"
- "core/runtime/**"
- "tools/**"
- "integrations/**"
- "pytest.ini"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/interactive-shell-live.yml"
push:
branches: [main]
paths:
- "core/agent/**"
- "core/agent_harness/**"
- "tests/core/agent/**"
- "surfaces/interactive_shell/**"
- "core/runtime/**"
- "tools/**"
- "integrations/**"
- "pytest.ini"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/interactive-shell-live.yml"
workflow_dispatch:
inputs:
shard_indexes:
description: "Comma-separated shard indexes to run (0-7)"
required: false
default: "0,1,2,3,4,5,6,7"
permissions:
contents: read
concurrency:
group: interactive-shell-live-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
turn-checks:
name: turn-checks (no-LLM)
runs-on: ubuntu-latest
timeout-minutes: 10
env:
OPENSRE_DISABLE_KEYRING: "1"
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Deterministic turn + fixture integrity
run: >-
uv run python -m pytest -n auto -v
tests/core/agent/
-m "not live_llm"
turn-live:
# Run on push to main, manual dispatch, and same-repo PRs. Fork PRs have no
# secrets, so they skip the live job (the no-LLM turn-checks job above
# still gates them) instead of failing the credential-validation step.
if: >-
github.repository == 'Tracer-Cloud/opensre' &&
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
name: turn-live shard ${{ matrix.shard_index }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard_index: ${{ fromJSON(github.event_name == 'workflow_dispatch' && format('[{0}]', github.event.inputs.shard_indexes) || '[0, 1, 2, 3, 4, 5, 6, 7]') }}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
LLM_PROVIDER: openai
OPENSRE_DISABLE_KEYRING: "1"
TURN_SHARD_TOTAL: "8"
TURN_SHARD_INDEX: ${{ matrix.shard_index }}
PYTHONUTF8: "1"
# @live gather scenarios (316, 333335, 337) require these secrets in CI.
# Missing values fail the job in preflight and via skip_or_fail in tests.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG_SLUG: tracer-30
SENTRY_PROJECT_SLUG: python
SENTRY_URL: https://sentry.io
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_APP_KEY: ${{ secrets.DD_APP_KEY }}
DD_SITE: datadoghq.com
GRAFANA_READ_TOKEN: ${{ secrets.GRAFANA_READ_TOKEN }}
GRAFANA_INSTANCE_URL: ${{ secrets.GRAFANA_INSTANCE_URL }}
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Validate live turn credentials
run: |
missing=()
if [ -z "${OPENAI_API_KEY}" ]; then
missing+=(OPENAI_API_KEY)
fi
if [ -z "${SENTRY_AUTH_TOKEN}" ]; then
missing+=(SENTRY_AUTH_TOKEN)
fi
if [ -z "${DD_API_KEY}" ]; then
missing+=(DD_API_KEY)
fi
if [ -z "${DD_APP_KEY}" ]; then
missing+=(DD_APP_KEY)
fi
if [ ${#missing[@]} -gt 0 ]; then
echo "::error::Missing secrets for live turn tests: ${missing[*]}"
exit 1
fi
- name: Run sharded live turn suite
run: >-
uv run python -m pytest -n auto -v
-m live_llm
tests/core/agent/test_turn_scenarios.py
-k "test_live_turn_execution_oracle or test_live_action_planning"
+678
View File
@@ -0,0 +1,678 @@
name: Release
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "tests/benchmarks/cloudopsbench/infra/**"
- "platform/deployment/install-proxy/**"
- "tests/e2e/kubernetes/helm/scripts/**"
- "tests/**"
- "**/*.md"
- "**/*.mdx"
- ".claude/**"
schedule:
- cron: "30 0 * * *"
workflow_dispatch:
inputs:
channel:
description: "Release channel to publish."
required: false
default: release
type: choice
options:
- release
- main
tag:
description: "Optional tag to release (e.g. v0.1.2026.6.26 or v0.1). Ignored for the main channel."
required: false
type: string
permissions:
contents: write
actions: read
models: read
concurrency:
group: release-${{ github.event_name == 'push' && 'main' || (github.event_name == 'workflow_dispatch' && inputs.channel == 'main' && 'main') || 'stable' }}
cancel-in-progress: true
jobs:
prepare:
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
outputs:
channel: ${{ steps.meta.outputs.channel }}
tag_name: ${{ steps.meta.outputs.tag_name }}
version_name: ${{ steps.meta.outputs.version_name }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve release metadata
id: meta
env:
EVENT_NAME: ${{ github.event_name }}
DISPATCH_CHANNEL: ${{ inputs.channel }}
DISPATCH_TAG: ${{ inputs.tag }}
shell: bash
run: |
set -euo pipefail
channel="release"
if [ "$EVENT_NAME" = "push" ]; then
channel="main"
fi
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$DISPATCH_CHANNEL" = "main" ]; then
channel="main"
fi
if [ "$channel" = "main" ]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$DISPATCH_TAG" ]; then
echo "The main channel does not accept a custom tag." >&2
exit 1
fi
year="$(date -u +%Y)"
month="$(date -u +%-m)"
day="$(date -u +%-d)"
short_sha="${GITHUB_SHA:0:7}"
main_version="0.1.${year}.${month}.${day}+main.${short_sha}"
echo "channel=main" >> "$GITHUB_OUTPUT"
echo "tag_name=main-build" >> "$GITHUB_OUTPUT"
echo "version_name=${main_version}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$DISPATCH_TAG" ]; then
tag_name="$DISPATCH_TAG"
else
year="$(date -u +%Y)"
month="$(date -u +%-m)"
day="$(date -u +%-d)"
# Keep the v0.1 prefix so the tag makes clear the product is still
# v0.1, while the date stays useful (e.g. v0.1.2026.6.26).
tag_name="v0.1.${year}.${month}.${day}"
fi
echo "channel=release" >> "$GITHUB_OUTPUT"
echo "tag_name=${tag_name}" >> "$GITHUB_OUTPUT"
echo "version_name=${tag_name#v}" >> "$GITHUB_OUTPUT"
verify:
runs-on: ubuntu-latest
needs: prepare
if: github.event_name != 'push'
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Lint
run: make lint
- name: Type check
run: make typecheck
- name: CLI smoke tests
run: make test-cli-smoke
build-python-dist:
if: needs.prepare.outputs.channel == 'release' && needs.verify.result == 'success'
runs-on: ubuntu-latest
needs: [verify, prepare]
env:
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Sync release version
shell: bash
run: python platform/packaging/sync_release_version.py --tag "$TAG_NAME"
- name: Build Python distributions
run: |
uv sync --frozen --extra release-dist
uv run python -m build
uv run twine check dist/*
- name: Verify Python distribution version
shell: bash
run: |
set -euo pipefail
VERSION="${TAG_NAME#v}"
test -f "dist/opensre-${VERSION}.tar.gz"
test -f "dist/opensre-${VERSION}-py3-none-any.whl"
- name: Upload Python distributions
uses: actions/upload-artifact@v4
with:
name: release-python-dist
path: dist/*
if-no-files-found: error
build-binaries:
if: always() && (needs.prepare.outputs.channel == 'main' || needs.verify.result == 'success')
needs: [verify, prepare]
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
target: linux-x64
binary_name: opensre
archive_ext: tar.gz
pyinstaller_mode: onedir
- runner: ubuntu-22.04-arm
target: linux-arm64
binary_name: opensre
archive_ext: tar.gz
pyinstaller_mode: onedir
- runner: macos-15-intel
target: darwin-x64
binary_name: opensre
archive_ext: tar.gz
pyinstaller_mode: onedir
- runner: macos-latest
target: darwin-arm64
binary_name: opensre
archive_ext: tar.gz
pyinstaller_mode: onedir
- runner: windows-latest
target: windows-x64
binary_name: opensre.exe
archive_ext: zip
pyinstaller_mode: onefile
# windows-arm64 is currently excluded from the default release matrix:
# cryptography does not publish win_arm64 wheels, so dependency install
# falls back to a source build that requires an OpenSSL toolchain on the
# GitHub-hosted runner.
runs-on: ${{ matrix.runner }}
env:
RELEASE_CHANNEL: ${{ needs.prepare.outputs.channel }}
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
VERSION_NAME: ${{ needs.prepare.outputs.version_name }}
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Sync binary version
shell: bash
run: python platform/packaging/sync_release_version.py --version "$VERSION_NAME"
- name: Install binary build dependencies
shell: bash
run: uv sync --frozen --extra release-binary
- name: Stage stdlib platform module for bundling
shell: bash
run: |
set -euo pipefail
# The first-party ``platform`` package shadows the stdlib ``platform``
# module. PyInstaller does not lay out the stdlib as loose ``.py`` files,
# so bundle a copy of the genuine module that platform/__init__.py can
# load from ``sys._MEIPASS`` at runtime (see _FROZEN_STDLIB_DIR).
rm -rf .stdlib_vendor
mkdir -p .stdlib_vendor
uv run python -c "import os, shutil, sysconfig; src = os.path.join(sysconfig.get_path('stdlib'), 'platform.py'); shutil.copy(src, os.path.join('.stdlib_vendor', 'platform.py')); print('Staged stdlib platform from ' + src)"
- name: Build binary
run: >-
uv run pyinstaller surfaces/cli/__main__.py
--name opensre
--${{ matrix.pyinstaller_mode }}
--clean
--noconfirm
--collect-data surfaces.cli
--collect-data config
--copy-metadata opensre
--collect-data litellm
--hidden-import tiktoken_ext
--hidden-import tiktoken_ext.openai_public
--collect-submodules integrations
--collect-submodules surfaces.interactive_shell
--collect-submodules tools
--add-data "platform:platform"
--add-data ".stdlib_vendor:_opensre_stdlib_platform"
- name: Smoke test binary (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -uo pipefail
# Capture the exit status explicitly so a crashing binary still prints
# its stdout/stderr (under `set -e` the failed substitution aborted the
# step before the output was ever shown, hiding the real error).
# onefile builds place the executable at ./dist/<name>; onedir builds
# produce a ./dist/opensre/ directory containing the executable. A bare
# `-x` test matches the onedir directory too (dirs carry the search
# bit), so require a regular file before treating it as the binary.
if [ -f "./dist/${{ matrix.binary_name }}" ] && [ -x "./dist/${{ matrix.binary_name }}" ]; then
BINARY_PATH="./dist/${{ matrix.binary_name }}"
else
BINARY_PATH="./dist/opensre/${{ matrix.binary_name }}"
fi
set +e
VERSION_OUTPUT="$("$BINARY_PATH" --version 2>&1)"
VERSION_STATUS=$?
set -e
printf '%s\n' "$VERSION_OUTPUT"
if [ "$VERSION_STATUS" -ne 0 ]; then
printf '::error::%s --version exited with status %s\n' "${{ matrix.binary_name }}" "$VERSION_STATUS" >&2
exit "$VERSION_STATUS"
fi
case "$VERSION_OUTPUT" in
*"$VERSION_NAME"*) ;;
*)
printf 'Binary version mismatch: expected %s but saw %s\n' "$VERSION_NAME" "$VERSION_OUTPUT" >&2
exit 1
;;
esac
"$BINARY_PATH" -h >/dev/null
LITELLM_DATA="./dist/opensre/_internal/litellm/model_prices_and_context_window_backup.json"
if [ ! -f "$LITELLM_DATA" ]; then
echo "LiteLLM package data missing from Unix onedir bundle: ${LITELLM_DATA}" >&2
exit 1
fi
- name: Check Linux binary glibc compatibility
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
max_glibc="$(
find dist/opensre -type f \( -name opensre -o -name '*.so' -o -name '*.so.*' \) -print0 \
| xargs -0 strings 2>/dev/null \
| grep -Eo 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' \
| sort -Vu \
| tail -n 1 \
|| true
)"
echo "Max required glibc symbol: ${max_glibc:-none}"
if [ -n "$max_glibc" ] && [ "$(printf '%s\n' "$max_glibc" GLIBC_2.35 | sort -V | tail -n 1)" != "GLIBC_2.35" ]; then
echo "Linux binary requires ${max_glibc}; pin the Linux runner back to Ubuntu 22.04 or lower dependency wheel requirements." >&2
exit 1
fi
- name: Smoke test binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$versionOutput = & ".\dist\${{ matrix.binary_name }}" --version 2>&1 | Out-String
$versionText = $versionOutput.Trim()
Write-Host $versionText
$expectedVersion = $env:VERSION_NAME
if ($versionText -notmatch [regex]::Escape($expectedVersion)) {
throw "Binary version mismatch. Expected '$expectedVersion' but saw '$versionText'."
}
& ".\dist\${{ matrix.binary_name }}" -h | Out-Null
- name: Package binary archive (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
if [ "$RELEASE_CHANNEL" = "release" ]; then
ASSET_BASENAME="opensre_${TAG_NAME#v}_${{ matrix.target }}"
else
ASSET_BASENAME="opensre_main_${{ matrix.target }}"
fi
tar -C dist -czf "${ASSET_BASENAME}.tar.gz" "${{ matrix.binary_name }}"
shasum -a 256 "${ASSET_BASENAME}.tar.gz" > "${ASSET_BASENAME}.tar.gz.sha256"
- name: Package binary archive (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
if ($env:RELEASE_CHANNEL -eq "release") {
$assetBaseName = "opensre_$($env:TAG_NAME.TrimStart('v'))_${{ matrix.target }}"
} else {
$assetBaseName = "opensre_main_${{ matrix.target }}"
}
Compress-Archive -Path "dist\${{ matrix.binary_name }}" -DestinationPath "${assetBaseName}.zip"
$hash = (Get-FileHash -Algorithm SHA256 "${assetBaseName}.zip").Hash.ToLowerInvariant()
Set-Content -Path "${assetBaseName}.zip.sha256" -Value "$hash ${assetBaseName}.zip"
- name: Upload binary archive
uses: actions/upload-artifact@v4
with:
name: ${{ env.RELEASE_CHANNEL == 'main' && 'main-' || '' }}release-${{ matrix.target }}
path: |
opensre_*_${{ matrix.target }}.${{ matrix.archive_ext }}
opensre_*_${{ matrix.target }}.${{ matrix.archive_ext }}.sha256
if-no-files-found: error
publish-release:
if: needs.prepare.outputs.channel == 'release'
runs-on: ubuntu-latest
needs:
- prepare
- build-python-dist
- build-binaries
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.repository.default_branch }}
fetch-depth: 0
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: release-assets
merge-multiple: true
- name: Resolve release context
id: release_ctx
env:
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
default_branch="${{ github.event.repository.default_branch }}"
git fetch origin "$default_branch" --tags --force
target_sha="$(git rev-parse "origin/$default_branch")"
previous_tag="$(
git tag --list 'v0.1.[0-9][0-9][0-9][0-9].[0-9]*.[0-9]*' --sort=-v:refname \
| grep -v -x "$TAG_NAME" \
| head -n 1 \
|| true
)"
range_spec="$target_sha"
if [ -n "$previous_tag" ]; then
range_spec="${previous_tag}..${target_sha}"
fi
{
printf 'target_sha=%s\n' "$target_sha"
printf 'previous_tag=%s\n' "$previous_tag"
printf 'range_spec=%s\n' "$range_spec"
} >> "$GITHUB_OUTPUT"
- name: Create release notes
env:
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
RANGE_SPEC: ${{ steps.release_ctx.outputs.range_spec }}
PREVIOUS_TAG: ${{ steps.release_ctx.outputs.previous_tag }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
{
echo "## Changelog"
echo
if [ -n "$PREVIOUS_TAG" ]; then
echo "_Changes since ${PREVIOUS_TAG}_"
else
echo "_Changes up to ${TAG_NAME}_"
fi
echo
git log "$RANGE_SPEC" --no-merges -n 100 --pretty='- %s (%h) — %an'
echo
} > GENERATED_CHANGELOG.md
# Summarize commits into prose via GitHub Models (gpt-4o-mini).
# Falls back to raw changelog if the API is unavailable.
raw_commits="$(git log "$RANGE_SPEC" --no-merges --pretty='%s' | head -40 || true)"
if [ -n "$raw_commits" ]; then
api_body="$(jq -n --arg commits "$raw_commits" '{
model: "gpt-4o-mini",
messages: [
{
role: "system",
content: "You are writing a Discord release announcement for an open-source SRE CLI tool called opensre. Summarize the git commits into 2-4 short, punchy prose sentences — no bullet points, no headers, no markdown. Write in present tense, active voice. Focus on what users will notice: new features, fixes, performance, integrations. Be specific but concise."
},
{role: "user", content: ("Commits:\n" + $commits)}
],
max_tokens: 300,
temperature: 0.4
}')"
curl -sS --max-time 20 \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
"https://models.inference.ai.azure.com/chat/completions" \
-d "$api_body" 2>/dev/null \
| jq -r '.choices[0].message.content // empty' 2>/dev/null \
| tr -d '\r' > DISCORD_NARRATIVE.md || true
if [ -s DISCORD_NARRATIVE.md ]; then
echo "LLM narrative generated ($(wc -c < DISCORD_NARRATIVE.md) bytes)."
else
echo "LLM summary unavailable; Discord will use raw changelog."
rm -f DISCORD_NARRATIVE.md
fi
fi
CB='```'
{
printf '## Install\n\n'
printf '### cURL (macOS / Linux)\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash -s -- --release --version %s\n%s\n\n' "$CB" "${TAG_NAME#v}" "$CB"
printf '### Homebrew (macOS / Linux)\n\n%sbash\nbrew tap tracer-cloud/tap\nbrew install tracer-cloud/tap/opensre\n%s\n\n' "$CB" "$CB"
printf '### PowerShell (Windows)\n\n%spowershell\n$env:OPENSRE_INSTALL_CHANNEL="release"; $env:OPENSRE_VERSION="%s"; irm https://install.opensre.com | iex\n%s\n\n' "$CB" "${TAG_NAME#v}" "$CB"
printf '### Python\n\n%sbash\npipx install opensre\n%s\n\n' "$CB" "$CB"
} > RELEASE_NOTES.md
cat GENERATED_CHANGELOG.md >> RELEASE_NOTES.md
- name: Create GitHub release
id: github_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
TARGET_SHA: ${{ steps.release_ctx.outputs.target_sha }}
shell: bash
run: |
set -euo pipefail
if gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "created=false" >> "$GITHUB_OUTPUT"
echo "Release $TAG_NAME already exists; nothing to do."
exit 0
fi
VERSION="${TAG_NAME#v}"
# Every release-channel publish is the newest stable build at this
# point, so mark it as GitHub's Latest. The title keeps the full
# version (e.g. "OpenSRE 0.1.2026.6.26") so the v0.1 line is clear.
gh release create "$TAG_NAME" \
release-assets/* \
--repo "${{ github.repository }}" \
--target "$TARGET_SHA" \
--title "OpenSRE ${VERSION}" \
--notes-file RELEASE_NOTES.md \
--latest
echo "created=true" >> "$GITHUB_OUTPUT"
- name: Announce on Discord
if: steps.github_release.outputs.created == 'true' && !github.event.repository.fork && !github.event.repository.private
continue-on-error: true
env:
DISCORD_WEBHOOK_URL_UPDATE: ${{ secrets.DISCORD_WEBHOOK_URL_UPDATE }}
DISCORD_RELEASES_ROLE_ID: ${{ secrets.DISCORD_RELEASES_ROLE_ID }}
DISCORD_RELEASE_LOGO_EMOJI: ${{ secrets.DISCORD_RELEASE_LOGO_EMOJI }}
DISCORD_RELEASE_LOGO_URL: ${{ secrets.DISCORD_RELEASE_LOGO_URL }}
RELEASE_TAG: ${{ needs.prepare.outputs.tag_name }}
RELEASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.prepare.outputs.tag_name }}
shell: bash
run: |
set -euo pipefail
if [ -z "${DISCORD_WEBHOOK_URL_UPDATE:-}" ]; then
echo "DISCORD_WEBHOOK_URL_UPDATE is not set; skipping Discord announcement."
exit 0
fi
# Prefer LLM-generated narrative; fall back to raw changelog.
if [ -f DISCORD_NARRATIVE.md ] && [ -s DISCORD_NARRATIVE.md ]; then
changelog_file="DISCORD_NARRATIVE.md"
elif [ -f GENERATED_CHANGELOG.md ]; then
changelog_file="GENERATED_CHANGELOG.md"
else
echo "No changelog available." > /tmp/discord_fallback.md
changelog_file="/tmp/discord_fallback.md"
fi
# Use an external Python script to build the JSON payload.
# This avoids jq issues with multiline LLM output containing control characters.
payload="$(CHANGELOG_FILE="$changelog_file" python3 .github/scripts/build-discord-payload.py)"
curl --fail -sS --max-time 30 -X POST -H "Content-Type: application/json" \
-d "$payload" "$DISCORD_WEBHOOK_URL_UPDATE"
- name: Sync Homebrew tap formula
if: steps.github_release.outputs.created == 'true'
continue-on-error: true
env:
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
VERSION: ${{ needs.prepare.outputs.tag_name }}
ASSET_DIR: release-assets
shell: bash
run: |
set -euo pipefail
if [ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]; then
echo "HOMEBREW_TAP_GITHUB_TOKEN is not set; skipping Homebrew tap sync."
exit 0
fi
export VERSION="${VERSION#v}"
bash .github/scripts/sync-homebrew-tap-formula.sh
publish-main-release:
if: always() && needs.prepare.outputs.channel == 'main' && needs.build-binaries.result == 'success'
runs-on: ubuntu-latest
needs:
- prepare
- build-binaries
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# The rolling `main-build` tag is force-moved to each new main commit
# below. Whenever main has advanced past a commit that touches
# .github/workflows/**, the default GITHUB_TOKEN (the
# github-actions[bot] GitHub App) is refused with "refusing to allow a
# GitHub App to create or update workflow ... without `workflows`
# permission" — and that scope cannot be granted via the permissions
# block. MAIN_BUILD_TAG_TOKEN must be a token that carries workflow
# write access (classic PAT with `repo` + `workflow`, a fine-grained
# PAT with Contents + Workflows: write, or a GitHub App token). It is
# persisted so the tag push below authenticates with it. Falls back to
# GITHUB_TOKEN so the rest of the job still runs if the secret is unset
# (the tag push will then fail as before until the secret is added).
token: ${{ secrets.MAIN_BUILD_TAG_TOKEN || secrets.GITHUB_TOKEN }}
persist-credentials: true
- name: Download main release artifacts
uses: actions/download-artifact@v4
with:
pattern: main-release-*
path: main-release-assets
merge-multiple: true
- name: Create release notes
env:
VERSION_NAME: ${{ needs.prepare.outputs.version_name }}
shell: bash
run: |
set -euo pipefail
short_sha="$(printf '%s' "$GITHUB_SHA" | cut -c1-7)"
built_at="$(date -u +"%Y-%m-%d %H:%M UTC")"
CB='```'
{
printf '## Main build\n\nRolling binary build from `main`.\n\n'
printf -- '- Version: `%s`\n- Commit: `%s`\n- Built: %s\n\n' "$VERSION_NAME" "$short_sha" "$built_at"
printf '### Install\n\nmacOS / Linux:\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash\n%s\n\n' "$CB" "$CB"
printf 'Equivalent explicit main channel:\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash -s -- --main\n%s\n\n' "$CB" "$CB"
printf 'Windows:\n\n%spowershell\nirm https://install.opensre.com | iex\n%s\n' "$CB" "$CB"
} > MAIN_RELEASE_NOTES.md
- name: Move main build tag to the latest commit
shell: bash
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -f "${{ needs.prepare.outputs.tag_name }}" "$GITHUB_SHA"
git push origin "refs/tags/${{ needs.prepare.outputs.tag_name }}" --force
- name: Publish rolling main release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
tag_name="${{ needs.prepare.outputs.tag_name }}"
if gh release view "$tag_name" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release upload "$tag_name" main-release-assets/* --repo "${{ github.repository }}" --clobber
gh release edit "$tag_name" \
--repo "${{ github.repository }}" \
--title "Main" \
--notes-file MAIN_RELEASE_NOTES.md
exit 0
fi
gh release create "$tag_name" \
main-release-assets/* \
--repo "${{ github.repository }}" \
--target "$GITHUB_SHA" \
--title "Main" \
--notes-file MAIN_RELEASE_NOTES.md \
--prerelease
@@ -0,0 +1,84 @@
name: Synthetic Deterministic Tests
# Regression guardrail: run every offline (no-LLM) synthetic test on every
# PR and merge so pipeline refactors can't silently break investigations.
#
# Tests that need a live LLM are excluded via the marker filter below.
# Those suites live in interactive-shell-live.yml and hermes-tests.yml instead.
on:
push:
branches: [main]
paths:
- "surfaces/**"
- "config/**"
- "core/**"
- "integrations/**"
- "platform/**"
- "tools/**"
- "tests/synthetic/**"
- "pyproject.toml"
- "uv.lock"
- "pytest.ini"
- ".github/workflows/synthetic-deterministic.yml"
pull_request:
branches: [main]
paths:
- "surfaces/**"
- "config/**"
- "core/**"
- "integrations/**"
- "platform/**"
- "tools/**"
- "tests/synthetic/**"
- "pyproject.toml"
- "uv.lock"
- "pytest.ini"
- ".github/workflows/synthetic-deterministic.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: synthetic-det-${{ github.ref }}
cancel-in-progress: true
jobs:
offline:
name: Synthetic offline (deterministic)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install dependencies
run: uv sync --frozen --extra dev
- name: Run deterministic synthetic tests
# Excludes markers that require a live LLM:
# synthetic — full RCA scenario suites (EKS, RDS, Hermes, Dagster …)
# axis2 — adversarial scenario suites (also call the LLM)
# live_llm — explicit live-credential gate
# e2e — requires live infrastructure
run: |
uv run pytest tests/synthetic \
-m "not synthetic and not live_llm and not e2e and not axis2" \
-q
- name: Run hermes_rca offline scenario suite
run: uv run python -m tests.synthetic.hermes_rca.run_suite --offline-only
+282
View File
@@ -0,0 +1,282 @@
name: terraform-bench (plan)
# Runs terraform fmt + init + validate + plan against tests/benchmarks/cloudopsbench/infra/ on every PR
# that touches the bench infra. Posts the plan output as a sticky PR comment so
# reviewers can see what would change before approving.
#
# Never applies — apply is developer-local in v1 (see tests/benchmarks/cloudopsbench/infra/README.md).
# When apply-from-CI is desirable later, add a separate workflow that runs on
# merge-to-main with a different (higher-privileged) AWS role.
on:
pull_request:
paths:
- "tests/benchmarks/cloudopsbench/infra/**"
- ".github/workflows/terraform-bench.yml"
workflow_dispatch:
permissions:
contents: read
pull-requests: write
id-token: write # required for AWS OIDC role assumption
concurrency:
group: terraform-bench-${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
plan:
name: terraform plan
if: github.repository == 'Tracer-Cloud/opensre' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
defaults:
run:
working-directory: tests/benchmarks/cloudopsbench/infra
env:
AWS_REGION: us-east-1
# tfplan + plan.txt artifacts are not uploaded; plan output goes into the
# PR comment. If you want a downloadable plan binary, add an
# actions/upload-artifact step after `terraform show`.
TF_IN_AUTOMATION: "true"
TF_INPUT: "0"
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.5"
# Wrapper MUST stay enabled (default) — it's what populates
# `steps.<id>.outputs.stdout` for the `terraform show` step that
# feeds the sticky PR comment. Disabling it makes the comment
# render with a blank plan body. See greptile review on initial
# PR for details.
- name: Configure AWS credentials (OIDC role assumption)
# No long-lived AWS keys. The role is provisioned by tests/benchmarks/cloudopsbench/infra/
# Terraform; ARN is hardcoded here because Terraform outputs aren't
# available before the workflow can run. Account ID is the
# tracer-cloud account.
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/opensre-bench-terraform-plan
role-session-name: github-actions-terraform-plan-${{ github.run_id }}
aws-region: us-east-1
- name: terraform fmt
id: fmt
run: terraform fmt -check -recursive -no-color
continue-on-error: true
- name: terraform init
id: init
run: terraform init -no-color
- name: terraform validate
id: validate
run: terraform validate -no-color
- name: terraform plan
id: plan
run: terraform plan -no-color -input=false -lock=false -out=tfplan
continue-on-error: true
- name: terraform show
id: show
if: steps.plan.outcome == 'success'
run: terraform show -no-color tfplan
- name: Post plan to PR
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
env:
PLAN_STDOUT: ${{ steps.show.outputs.stdout }}
FMT_OUTCOME: ${{ steps.fmt.outcome }}
INIT_OUTCOME: ${{ steps.init.outcome }}
VALIDATE_OUTCOME: ${{ steps.validate.outcome }}
PLAN_OUTCOME: ${{ steps.plan.outcome }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
script: |
const fs = require('fs');
const MARKER = '<!-- terraform-bench-plan-comment -->';
const fmt = process.env.FMT_OUTCOME;
const init = process.env.INIT_OUTCOME;
const validate = process.env.VALIDATE_OUTCOME;
const plan = process.env.PLAN_OUTCOME;
const url = process.env.WORKFLOW_URL;
const icon = (outcome) => outcome === 'success' ? '✅' :
outcome === 'failure' ? '❌' :
outcome === 'skipped' ? '⏭️' : '⚠️';
let planBody = process.env.PLAN_STDOUT || '';
// GitHub PR comments cap at 65536 chars. Reserve ~1000 for surrounding text.
const MAX = 60000;
let truncated = false;
if (planBody.length > MAX) {
planBody = planBody.slice(-MAX);
truncated = true;
}
const body = [
MARKER,
'### terraform-bench plan',
'',
'| step | outcome |',
'| --- | --- |',
`| fmt | ${icon(fmt)} \`${fmt}\` |`,
`| init | ${icon(init)} \`${init}\` |`,
`| validate | ${icon(validate)} \`${validate}\` |`,
`| plan | ${icon(plan)} \`${plan}\` |`,
'',
fmt !== 'success' ? '> Run `terraform fmt -recursive` in `tests/benchmarks/cloudopsbench/infra/` to fix formatting.' : '',
'',
plan === 'success'
? `<details><summary>Plan output${truncated ? ' (truncated — see <a href="' + url + '">workflow logs</a> for full output)' : ''}</summary>\n\n\`\`\`hcl\n${planBody}\n\`\`\`\n\n</details>`
: `Plan did not run successfully — see [workflow logs](${url}) for details.`,
'',
`_Updated by [\`terraform-bench.yml\`](${url})._`,
].join('\n');
// Find an existing comment to update, else create a new one.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body && c.body.includes(MARKER));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Fail if any step errored
if: |
steps.fmt.outcome == 'failure' ||
steps.init.outcome == 'failure' ||
steps.validate.outcome == 'failure' ||
steps.plan.outcome == 'failure'
run: |
echo "::error::One or more terraform steps failed. See PR comment + workflow logs."
exit 1
# ---------------------------------------------------------------------- #
# Security + lint scanners — run in parallel with plan. #
# #
# Suppression policy (intentional skips) is defined per-tool: #
# - checkov: tests/benchmarks/cloudopsbench/infra/.checkov.yaml #
# - tfsec: tests/benchmarks/cloudopsbench/infra/.tfsec/config.yml #
# - tflint: tests/benchmarks/cloudopsbench/infra/.tflint.hcl #
# Each suppression includes a justification at the source. Day-one #
# findings should be REAL findings — change configs, not the workflow, #
# if a rule needs to be skipped. #
# #
# checkov + tfsec hard-fail (security blocks PRs). tflint soft-fails #
# because its rules cover code quality / deprecations, not security — #
# we want visibility without blocking. #
# ---------------------------------------------------------------------- #
tflint:
name: tflint
runs-on: ubuntu-latest
defaults:
run:
working-directory: tests/benchmarks/cloudopsbench/infra
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup tflint
uses: terraform-linters/setup-tflint@v4
with:
tflint_version: latest
- name: tflint --init (download plugins)
run: tflint --init
- name: tflint
id: tflint
run: tflint --recursive --format compact
continue-on-error: true
- name: Surface tflint findings (non-blocking)
if: steps.tflint.outcome == 'failure'
run: |
echo "::warning::tflint reported findings. Code-quality only — does not block PR. Review in workflow logs."
tfsec:
name: tfsec
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# tfsec via Docker, bypassing aquasecurity/tfsec-action.
#
# Why direct Docker run: the action shells out to api.github.com to
# resolve the latest release binary, and the aquasecurity org has an
# IP allow list that blocks GitHub Actions runner IPs — making the
# action effectively unusable from CI. The aquasec/tfsec image on
# Docker Hub has no such restriction.
#
# Migration path (v2): tfsec is in maintenance mode; rule set lives on
# in Trivy. Replace with `aquasec/trivy:latest config /src` when
# ready — same .tfsec/config.yml is recognized.
#
# Suppressions: tests/benchmarks/cloudopsbench/infra/.tfsec/config.yml. tfsec exits 1 on any
# finding by default, which fails the step — exactly the hard-fail
# behavior we want. Add a justified exclude to the config rather
# than disabling the step.
- name: tfsec scan (via Docker)
run: |
docker run --rm \
-v "${{ github.workspace }}/tests/benchmarks/cloudopsbench/infra:/src" \
aquasec/tfsec:latest \
/src \
--config-file=/src/.tfsec/config.yml \
--no-color
checkov:
name: checkov
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
# Suppressions live in tests/benchmarks/cloudopsbench/infra/.checkov.yaml. soft_fail is
# FALSE — real findings block the PR. Add a justified skip-check
# to the config rather than disabling here.
- name: checkov scan
uses: bridgecrewio/checkov-action@v12
with:
directory: tests/benchmarks/cloudopsbench/infra
framework: terraform
config_file: tests/benchmarks/cloudopsbench/infra/.checkov.yaml
quiet: true
soft_fail: false
# Re-enable SARIF upload when CodeQL alerts are wanted:
# output_format: cli,sarif
# output_file_path: console,checkov-results.sarif
@@ -0,0 +1,76 @@
name: Tracer Demo Prefect ECS
on:
workflow_dispatch:
permissions:
contents: read
jobs:
tracer-demo-prefect-ecs:
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
JWT_TOKEN: ${{ secrets.JWT_TOKEN }}
TRACER_ORG_ID: ${{ secrets.TRACER_ORG_ID }}
TRACER_WEB_APP_URL: ${{ secrets.TRACER_WEB_APP_URL }}
TRACER_API_URL: ${{ secrets.TRACER_API_URL }}
TRACER_INGEST_TOKEN: ${{ secrets.TRACER_INGEST_TOKEN }}
AWS_REGION: us-east-1
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install Python dependencies
run: uv sync --frozen --extra dev
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Install tracer CLI
run: |
curl -sSL https://install.tracer.cloud | CLI_BRANCH=dev sh -s user_36fbN6K6FwEgJFHsQv1pUByo8K6
- name: Initialize tracer
run: |
sudo tracer init --token ${{ secrets.JWT_TOKEN }}
- name: Export Tracer run id
shell: bash
run: |
RUN_ID=$(tracer info --json | python3 -c "import sys,json; print(json.load(sys.stdin)['run']['id'])")
PIPELINE_NAME=$(tracer info --json | python3 -c "import sys,json; print(json.load(sys.stdin)['pipeline']['name'])")
ORG_SLUG=$(tracer info --json | python3 -c "import sys,json; print(json.load(sys.stdin)['pipeline']['organization'])")
echo "TRACER_RUN_ID=$RUN_ID" >> $GITHUB_ENV
echo "TRACER_PIPELINE_NAME=$PIPELINE_NAME" >> $GITHUB_ENV
echo "TRACER_ORG_SLUG=$ORG_SLUG" >> $GITHUB_ENV
# Optional but helpful: stable trace id for ingest rows
echo "TRACER_TRACE_ID=trace_$RUN_ID" >> $GITHUB_ENV
echo "Exported TRACER_RUN_ID=$RUN_ID"
- name: Run Prefect ECS Demo
continue-on-error: true
run: |
uv run python -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e
+85
View File
@@ -0,0 +1,85 @@
name: Trigger K8s Alert
on:
workflow_dispatch:
inputs:
verify:
description: "Verify logs in Datadog + post to Slack"
type: boolean
default: true
schedule:
- cron: "0 9 * * 1" # every Monday 9am UTC
permissions:
contents: read
jobs:
trigger-alert:
if: github.repository == 'Tracer-Cloud/opensre'
runs-on: ubuntu-latest
env:
AWS_REGION: us-east-1
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_APP_KEY: ${{ secrets.DD_APP_KEY }}
DD_SITE: datadoghq.com
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_DEVS_ALERTS_CHANNEL_ID: C09S7GDG60J
steps:
- uses: actions/checkout@v5
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install project
run: uv sync --frozen
- name: Trigger pipeline failure via centralized config (~10s)
id: trigger
run: |
echo "trigger_epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
set +e
uv run python -m tests.e2e.kubernetes.trigger_alert | tee /tmp/trigger.log
trigger_exit=${PIPESTATUS[0]}
set -e
echo "trigger_completed_epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
if grep -q '"status": "accepted_timeout"' /tmp/trigger.log; then
echo "trigger_accepted_timeout=true" >> "$GITHUB_OUTPUT"
else
echo "trigger_accepted_timeout=false" >> "$GITHUB_OUTPUT"
fi
exit "$trigger_exit"
- name: Report trigger result
run: |
echo "Trigger command completed successfully"
- name: Verify Datadog + Slack (~5 min)
if: ${{ inputs.verify == true || github.event_name == 'schedule' }}
run: |
POST_TRIGGER_WAIT=""
if [ "${{ steps.trigger.outputs.trigger_accepted_timeout }}" = "true" ]; then
POST_TRIGGER_WAIT="--post-trigger-wait 90"
fi
uv run python -m tests.e2e.kubernetes.trigger_alert \
--verify-only \
--since-epoch ${{ steps.trigger.outputs.trigger_completed_epoch }} \
--dd-max-wait 300 \
$POST_TRIGGER_WAIT
+186
View File
@@ -0,0 +1,186 @@
# Environment variables
*.env
.env
.env.local
.env.*.local
.env.backup*
.env.bak
.env.bench
.secrets
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
.stdlib_vendor/
# PyInstaller generates this from CLI args during the release build.
opensre.spec
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
.venv-devcontainer/
virtualenv/
tracer_agent/
# Testing
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
.hypothesis/
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
*.log
!tests/synthetic/hermes/**/*.log
.turn-shard-logs/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# uv (uv.lock is committed)
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# ruff
.ruff_cache/
# import-linter
.import_linter_cache/
# PyInstaller build cache
.pyinstaller-cache/
# Pyre type checker
.pyre/
# MacOS
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# IDE / Editors
.vscode/
.idea/
*.swp
*.swo
*~
.project
.classpath
.settings/
*.sublime-project
*.sublime-workspace
*.code-workspace
# Wrangler local state
**/.wrangler/
# Project specific
.antigravitycli/
output/
# CLI terminal output package (must stay tracked; ``output/`` above ignores new files here).
# Both the pre-restructure path and the post-restructure path under ``surfaces/`` are
# negated so the package keeps being tracked across the T-1 move (#3299).
!interactive_shell/ui/output/
!interactive_shell/ui/output/**
interactive_shell/ui/output/**/__pycache__/
!surfaces/interactive_shell/ui/output/
!surfaces/interactive_shell/ui/output/**
surfaces/interactive_shell/ui/output/**/__pycache__/
.cloudopsbench-results/
.bench-results/
tests/benchmarks/cloudopsbench/benchmark/
tests/benchmarks/cloudopsbench/.cache/
# Vendorized packages in api_ingester
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/certifi/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/certifi-*/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/charset_normalizer/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/charset_normalizer-*/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/idna/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/idna-*/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/requests/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/requests-*/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/urllib3/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/urllib3-*/
tests/test_case_upstream_downstream_pipeline/pipeline_code/api_ingester/bin/
# CDK build artifacts
**/cdk.out/
CLAUDE_PERSONAL.md
# Local planning notes (not tracked)
plans/
tasks/
app/memories
.aider*
.worktrees/
.claude/worktrees/
# Node dev tooling
node_modules/
package-lock.json
# Synthetic test run observations (generated test output, not source)
tests/synthetic/rds_postgres/_observations/
# Local CI-fix scratch clone (not part of this repo)
opensre-ci-fix/
# Terraform local state + provider cache
**/.terraform/
**/.terraform.lock.hcl
*.tfstate
*.tfstate.*
*.tfvars
crash.log
+19
View File
@@ -0,0 +1,19 @@
[importlinter]
root_packages =
surfaces
config
core
gateway
integrations
platform
tools
[importlinter:contract:config-independent]
name = Config is independent
type = independence
modules =
config
# Full layered contracts (core ⊄ tools, integrations ⊄ cli, …) live in
# ``.importlinter.strict`` for local burn-down. Run: ``make check-layers-strict``
# Regenerate baselines: ``uv run lint-imports --config .importlinter.strict``
+105
View File
@@ -0,0 +1,105 @@
# Counterpart to .github/ci/check_direct_imports.py (direct edges only).
# Run: make check-layers-strict
#
# Remove ignore_imports entries as refactors land; link the fixing PR/issue.
[importlinter]
root_packages =
surfaces
config
core
gateway
integrations
platform
tools
[importlinter:contract:opensre-layers]
name = OpenSRE package layers (transitive)
type = layers
layers =
surfaces | gateway
tools | integrations
# ``:`` = siblings may cross-import (import-linter multi-item layers).
# ``|`` would forbid core <-> platform, but both packages depend on each other.
core : platform
config
ignore_imports =
# config upward imports (T-4 debt — refactor into lower layers)
config.config -> core.llm.providers.azure_openai
config.llm_auth.credentials -> integrations.llm_cli.registry
config.llm_keyring -> platform
config.repl_config -> platform.terminal.theme
# platform -> integrations (T-4 debt — scheduler / observability)
platform.analytics.cli -> integrations.github.identity
platform.observability.errors.sentry -> integrations.llm_cli.errors
platform.scheduler.credentials -> integrations.catalog
platform.scheduler.executor -> integrations.discord.delivery
platform.scheduler.executor -> integrations.slack.delivery
platform.scheduler.executor -> integrations.telegram.delivery
platform.scheduler.executor -> integrations.telegram.formatting
# surfaces -> gateway (cli + repl gateway commands)
surfaces.cli.commands.gateway -> gateway.daemon
surfaces.cli.commands.gateway -> gateway.manager
surfaces.interactive_shell.command_registry.gateway_cmds -> gateway.daemon
surfaces.interactive_shell.controller -> gateway.web_server
# tools.interactive_shell -> surfaces (T-4 debt — issue #3352)
tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.background.runner
tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.runtime.investigation_adapter
tools.interactive_shell.actions.investigation -> surfaces.interactive_shell.session
tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.session
tools.interactive_shell.shared.investigation_launch -> surfaces.interactive_shell.session
tools.interactive_shell.actions.llm_provider -> surfaces.cli.wizard.config
tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.command_registry
tools.interactive_shell.actions.llm_provider -> surfaces.interactive_shell.ui.execution_confirm
tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.background.runner
tools.interactive_shell.actions.sample_alert -> surfaces.interactive_shell.runtime.investigation_adapter
tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry
tools.interactive_shell.actions.slash -> surfaces.interactive_shell.command_registry.slash_catalog
tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui
tools.interactive_shell.actions.slash -> surfaces.interactive_shell.ui.execution_confirm
tools.interactive_shell.actions.slash -> surfaces.interactive_shell.utils.telemetry.turn_outcome
tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.command_registry
tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.runtime
tools.interactive_shell.actions.task_cancel -> surfaces.interactive_shell.ui.execution_confirm
# tools -> integrations (T-4 debt — vendor tools / reporting)
tools.community_followup_tool -> integrations.github.client
tools.community_followup_tool -> integrations.github.helpers
tools.community_followup_tool -> integrations.github.tools.workflow
tools.cross_vendor.fix_sentry_issue.context -> integrations.sentry
tools.cross_vendor.fix_sentry_issue.context -> integrations.sentry.issue_url
tools.cross_vendor.fix_sentry_issue.pr -> integrations.github.client
tools.cross_vendor.fix_sentry_issue.pr -> integrations.github.repo_scope
tools.cross_vendor.fix_sentry_issue.runner -> integrations.coding_agent
tools.cross_vendor.fix_sentry_issue.runner -> integrations.git
tools.cross_vendor.fix_sentry_issue.runner -> integrations.github.client
tools.cross_vendor.fix_sentry_issue.ship -> integrations.coding_agent
tools.cross_vendor.fix_sentry_issue.ship -> integrations.git
tools.cross_vendor.fix_sentry_issue.ship -> integrations.github.client
tools.git_deploy_timeline_tool -> integrations.github.helpers
tools.git_deploy_timeline_tool -> integrations.github.mcp
tools.interactive_shell.implementation.claude_code_executor -> integrations.llm_cli.claude_code
tools.interactive_shell.implementation.claude_code_executor -> integrations.llm_cli.subprocess_env
tools.investigation.reporting.delivery.bootstrap -> integrations.discord.reporting_adapter
tools.investigation.reporting.delivery.bootstrap -> integrations.openclaw.reporting_adapter
tools.investigation.reporting.delivery.bootstrap -> integrations.slack.reporting_adapter
tools.investigation.reporting.delivery.bootstrap -> integrations.telegram.reporting_adapter
tools.investigation.reporting.delivery.bootstrap -> integrations.twilio.reporting_adapter
tools.investigation.reporting.delivery.bootstrap -> integrations.whatsapp.reporting_adapter
tools.investigation.reporting.evaluation -> integrations.opensre.llm_eval_judge
tools.investigation.reporting.formatters.report -> integrations.telegram.formatting
tools.investigation.reporting.gitlab_writeback -> integrations.gitlab
tools.investigation.reporting.upstream_correlation.registry -> integrations.datadog.correlation.registration
tools.investigation.state_factory -> integrations.opensre.hf_remote
tools.pi_coding_tool -> integrations.pi
tools.pi_coding_tool.runner -> integrations.pi
tools.pi_coding_tool.validation -> integrations.pi
tools.system.python_execution_tool.credentials -> integrations.github.client
tools.system.python_execution_tool.credentials -> integrations.github.helpers
tools.slack_send_message_tool.delivery -> integrations.catalog
tools.slack_send_message_tool.delivery -> integrations.slack.delivery
tools.system.watch_dog.runner -> integrations.telegram.alarms
tools.system.watch_dog.runner -> integrations.telegram.credentials
tools.work_status_report_tool -> integrations.github.client
tools.work_status_report_tool -> integrations.github.helpers
tools.work_status_report_tool -> integrations.github.tools.work_status
tools.work_status_report_tool -> integrations.github.tools.workflow
+15
View File
@@ -0,0 +1,15 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
hooks:
- id: ruff-check
args: [--no-fix]
files: ^(config|core|gateway|integrations|platform|surfaces|tools|tests)/
- id: ruff-format
args: [--check]
files: ^(config|core|gateway|integrations|platform|surfaces|tools|tests)/
+6
View File
@@ -0,0 +1,6 @@
python 3.13.11
nodejs 22.12.0
pnpm 10.28.0
uv 0.11.11
ruff 0.15.12
mypy 1.20.2
+237
View File
@@ -0,0 +1,237 @@
## OpenSRE Development Reference
## Build and Run commands
- Build `make install` (sets up the project environment via `uv sync` and installs this repo in editable mode)
- Run **`uv run opensre …`** from the repo root while developing — preferred approach, uses this checkout even if another `opensre` is on your `PATH`.
- Use **`uv run python …`** for any Python commands.
## Code Style
- Use strict typing, follow DRY principle
- One clear purpose per file (separation of concerns)
- Do not keep compatibility-only forwarding modules after refactors. Once imports and tests
are migrated, remove the old module path in the same change and use one canonical import path.
Before any push or PR creation follow **[CI.md](CI.md)** — lint, format, typecheck, and test commands all live there.
When opening a PR, fill out the **[PR template](.github/PULL_REQUEST_TEMPLATE.md)** — it is not optional boilerplate; it has a required AI-usage disclosure section.
## 1. Repo Map
| Path | What it does |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/` | Investigation orchestration, context assembly, the shared runtime tool-calling loop, and domain logic (state, types, correlation rules). Includes `core/tool_framework/` — the `BaseTool` base class, `@tool` decorator, registered-tool primitives, error telemetry, skill-guidance helpers, and shared payload utilities (`utils/`). |
| `surfaces/cli/` | Command-line interface, onboarding wizard, local LLM helpers, and CLI tests support. |
| `surfaces/interactive_shell/` | Interactive terminal (REPL) loop, slash commands, chat/help surfaces, action-planning harness, and terminal UI. |
| `integrations/` | Per-integration config normalization, verification, clients, helpers, store/catalog logic, the Hermes log pipeline, and per-vendor tool packages under `integrations/<vendor>/tools/`. |
| `tools/` | Tool registry, per-tool packages for cross-cutting tools that aren't vendor-specific (e.g. `tools/system/fleet_monitoring/`, `tools/system/watch_dog/`, `tools/system/sre_guidance_tool/`), and the interactive-shell action tools. Framework primitives (decorator, base class, utils) live in `core/tool_framework/`. |
| `platform/` | Cross-cutting platform services: guardrails, masking, sandbox, analytics, auth, notifications, observability, harness ports (`platform/harness_ports.py`), and EC2 deployment (`platform/deployment/`). |
| `config/` | Shared constants, prompts, and UI theme. |
| `tests/` | Unit, integration, synthetic, deployment, e2e, chaos engineering, and support tests. |
| `docs/` | User-facing documentation, integration guides, and docs-site assets. |
| `.github/` | CI workflows, issue templates, pull request template, and repository automation. |
| `Dockerfile` | Optional production container image (FastAPI health app via uvicorn). |
| `pyproject.toml` | Python project metadata, dependency configuration, tooling, and package settings. |
| `Makefile` | Canonical local automation for install, test, verify, deploy, and cleanup targets. |
| `README.md` | Product overview, install, quick start, high-level capabilities, and links to deeper docs. |
| `docs/DEVELOPMENT.md` | Contributor workflows: CI parity commands, dev container, benchmark, deployment, telemetry detail. |
| `docs/ARCHITECTURE.md` | Package architecture: the four-tier layer table, folder diagram, per-layer responsibilities, allowed cross-layer edges, and cross-layer flows. |
| `docs/investigation-pipeline-architecture.md` | Investigation pipeline stages, ReAct loop control flow, and guardrails (tool cap, stagnation breaker, context budget), with diagrams. |
| `docs/investigation-tool-calling.md` | Investigation ReAct tool schemas, LLM invoke payloads, and message shapes (all providers). |
| `docs/tool-placement-policy.md` | Decision rule for where a tool lives: `integrations/<vendor>/tools/` vs. `tools/system/` vs. `tools/cross_vendor/` vs. `surfaces/shared/`. |
| `docs/NAMING.md` | Naming conventions for `core/`: the glossary (State/Snapshot/RunInput/RunResult/Slice/Resources/Budget), the `{domain}_{role}.py` file rule, type naming (`Mixin` suffix, role-named Protocols, no package-name prefix), and anti-patterns. |
| `SETUP.md` | Machine setup (all platforms, Windows, MCP/OpenClaw, troubleshooting). |
| `CI.md` | Mandatory pre-push checklist: lint, format, typecheck, tests — agents MUST follow before pushing. |
| `TESTING.md` | `ReplDriver` reference: API, usage patterns, wait-time guide, and limitations. |
| `CONTRIBUTING.md` | Contribution workflow, branch/PR guidance, and quality expectations. |
Main packages one level deeper:
- `platform/analytics/` — Analytics event plumbing and install helpers used by the onboarding flow.
- `platform/auth/` — JWT and authentication helpers for local and hosted runtime access.
- `surfaces/cli/` — Command-line interface, onboarding wizard, local LLM helpers, and CLI tests support.
- `surfaces/interactive_shell/` — Interactive terminal (TTY) loop, slash-command surface, chat/help handoff, session runtime, and terminal UI. REPL watchdog slash commands (`/watch`, `/watches`, `/unwatch`): PR demo steps live under **Interactive shell: REPL watchdog demo** in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#interactive-shell-repl-watchdog-demo).
- `config/constants/` — Shared prompt and other static constants.
- `platform/deployment/aws/` — Shared boto3 client factory, deployment constants (`config.py`), VPC/subnet/SG helpers, EC2/IAM provisioning, ECR build/push, and SSM run-command primitives. Import from here in deployment scripts instead of duplicating.
- `platform/deployment/` — EC2 deploy/destroy: `opensre-web` and `opensre-gateway` on one instance. Makefile: `make deploy`.
- `platform/guardrails/` — Guardrail rules, evaluation engine, audit helpers, and CLI bindings.
- `platform/harness_ports.py` — Harness port layer (integration resolution, tool registry, investigation tools, GitHub repo scope). Real implementations are wired at startup via `integrations/harness_adapters.py` and `tools/harness_adapters.py` through `install_harness_ports()` in `surfaces/interactive_shell/ui/output/boundary.py`. See `core/agent_harness/AGENTS.md` for the import boundary.
- `integrations/` — Integration config normalization, verification, selectors, clients, integration-local helpers, store, and catalog logic.
- `integrations/hermes/` — Hermes log tailing, incident classification, correlator, sinks, and investigation bridge.
- `integrations/llm_cli/` — Subprocess-backed LLM CLIs (e.g. Codex). Extension guide: `integrations/llm_cli/AGENTS.md`.
- `platform/masking/` — Masking utilities for redacting or normalizing sensitive content.
- `tools/investigation/` — Composite investigation capability, public entrypoints, semantic stages, and reporting.
- `core/` — Shared LLM tool-calling loop (execute tools, message shaping, context budget).
- `core/llm/` — Hosted LLM provider clients, retry/schema helpers, and investigation tool-calling adapters.
- `platform/sandbox/` — Sandboxed execution helpers for controlled runtime actions.
- `core/state/` — Shared agent runtime envelope (`AgentState`), chat slice, investigation pipeline slice contracts, `EvidenceEntry`, state-update helpers, and pure defaults.
- `tools/` — Tool registry, decorator, base classes, per-tool packages, shared utilities, and registry helpers.
- `core/domain/types/` — Shared typed contracts for evidence, retrieval, and tool-related payloads.
- `platform/` — Guardrails, masking, sandbox, analytics, auth, and cross-cutting platform services (e.g. `integrations/telegram/*`).
- `tools/system/watch_dog/` — Watchdog feature: per-threshold Telegram alarm dispatch with cooldown, sitting on top of `integrations/telegram/*`.
- `gateway/webapp.py` — Web-facing health app served by the gateway daemon; the `opensre` CLI is `surfaces/cli/__main__.py`.
## 2. Entry Points
### Adding a Tool
The tool registry auto-discovers modules under `tools/`, so the normal path is to add one module or package there and let discovery pick it up.
Files to touch:
- `integrations/<vendor>/tools/<tool_name>_tool/__init__.py` when the tool belongs to an existing vendor integration (most common path).
- `tools/system/<ToolName>/__init__.py` or `tools/cross_vendor/<ToolName>/__init__.py` only when the tool is not vendor-specific — see [docs/tool-placement-policy.md](docs/tool-placement-policy.md) for the system vs. cross_vendor decision rule (e.g. `tools/system/sre_guidance_tool/`).
- `core/tool_framework/utils/` if the tool needs shared helper code reused across vendors.
- `integrations/<name>/client.py` if the tool should reuse a dedicated integration API client instead of inlining requests.
- `docs/<tool_name>.mdx` for user-facing usage, parameters, and examples.
- `docs/docs.json` — add the page path (without `.mdx`) to the appropriate `pages` array so Mintlify navigation includes it.
- `tests/tools/test_<tool_name>.py` for behavior and regression coverage.
Steps:
1. Pick the simplest shape that fits the tool. Use a `BaseTool` subclass (from `core.tool_framework.base`) for richer behavior; use `@tool(...)` from `core.tool_framework.tool_decorator` for a lightweight function tool.
2. Declare clear metadata: `name`, `description`, `source`, `input_schema`, and any `use_cases`, `requires`, `outputs`, or `retrieval_controls` you need.
3. Treat tool packages as production code, not registry placeholders. A tool package may not be an empty or nearly-empty `__init__.py` whose only purpose is discovery. Directionally, non-trivial tools should use focused sibling modules such as `tool.py`, `client.py`/`delivery.py`, `validation.py`, `models.py`, or `results.py`; `__init__.py` should usually be a small registry entrypoint that imports the public tool object.
4. Keep separation of concerns. Put reusable transport or integration-specific parsing code in `integrations/<name>/` or shared tool glue in `core/tool_framework/utils/` rather than copying it into the tool body. Split validation, credential/parameter resolution, dispatch/client calls, result normalization, and error handling into focused helpers or sibling files instead of tangling them inside `run()`.
5. Return stable, planner-friendly results. Expected failures should produce a structured error shape; external side effects must declare `side_effect_level`, require approval when appropriate, and avoid leaking secrets through `extract_params`, return values, logs, or traceable tool-call kwargs.
6. If the tool should appear in both investigation and chat surfaces, set `surfaces=("investigation", "chat")`.
7. Add tests that cover schema shape, availability, extraction, success, failure, and the runtime behavior that the planner depends on.
8. Before opening or approving the PR, follow [TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md) for tool/integration-specific wiring, payload, docs, and regression checks.
### Changing the investigation pipeline
Investigations are coordinated in `tools/investigation/lifecycle.py` and exposed via
`tools/investigation/capability.py`. Semantic stages live under
`tools/investigation/stages/`; reporting lives under
`tools/investigation/reporting/`. See
[docs/investigation-pipeline-architecture.md](docs/investigation-pipeline-architecture.md)
for the end-to-end stage/loop diagrams before making structural changes.
Files to touch:
- `tools/investigation/lifecycle.py` for high-level stage ordering.
- `core/state/` for shared agent state and investigation pipeline slice contracts
that cross stage boundaries.
- `core/domain/` for pure investigation rules (alert source mapping, tool planning,
category alignment, correlation scoring).
- `core/` for shared LLM runtime helpers (tool loop and LLM invoke error
classification).
- `core/state/*.py` when adding or renaming persisted investigation fields
(update `AgentStateModel` and the matching slice).
- `docs/` — update or add a page if the change introduces user-visible behavior or configuration.
- `tests/` coverage for the affected CLI, synthetic, or integration paths.
Steps:
1. Keep each stage focused on one responsibility.
2. Extend state models when new fields cross stage boundaries.
3. Update tests that exercise `run_investigation` / streaming entry points.
### Adding an Integration
Integration work usually spans config normalization, verification, integration-local clients/helpers, tools, docs, and tests.
Files to touch:
- `integrations/<name>/__init__.py` for config builders, validators, selectors, and normalization helpers.
- `integrations/<name>/client.py` when the integration needs a dedicated API client.
- `integrations/<name>/verifier.py` when the integration needs local verification logic.
- `integrations/catalog.py` when the new integration must be resolved into the shared runtime config.
- `integrations/verify.py` when the integration needs a local verification path.
- `tools/<Name>Tool/` or `tools/<tool_file>.py` for the user-facing tool layer, or
`integrations/<name>/tools/` when consolidating a vendor's tools under its integration package.
- `docs/<name>.mdx` for user-facing setup, usage, and verification docs.
- `docs/docs.json` — add the page path (without `.mdx`) to the appropriate `pages` array so Mintlify navigation includes it.
- `tests/integrations/test_<name>.py` for config, verification, and store coverage.
- `tests/tools/test_<tool_name>.py` and any relevant `tests/e2e/` or `tests/synthetic/` files if the integration is exercised by tools or scenarios.
Treat `integrations/` as the canonical user/config and external-client boundary, and `tools/` as the canonical agent-callable boundary. Do not add or import top-level `vendors/` or `services/` packages.
Examples from the repo:
- Datadog: `integrations/datadog/` (including `integrations/datadog/tools/` for query tools), `integrations/catalog.py`, and Datadog-related tests under `tests/integrations/datadog/` and `tests/tools/test_datadog_*.py`.
- Grafana: `integrations/grafana/` (including `integrations/grafana/tools/` for query tools), `integrations/catalog.py`, `surfaces/cli/wizard/local_grafana_stack/`, and Grafana-related tests under `tests/integrations/grafana/` and `tests/tools/test_grafana_*.py`.
- Hermes: `integrations/hermes/`, `tools/HermesLogsTool/`, `tools/HermesSessionEvidenceTool/`, `surfaces/cli/commands/hermes.py`, `tests/hermes/`, and `tests/synthetic/hermes/`.
Basic steps:
1. Add the integration config and normalization logic first so the rest of the stack can consume a consistent shape.
2. Add or update the integration-local client only when the integration needs direct remote calls.
3. Wire the tool layer after the config path is stable.
4. Add docs and tests together so the integration is understandable and verifiable.
5. Run `make verify-integrations` before treating the integration as complete.
6. Before opening or approving the PR, follow [TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md) for integration completeness, investigation wiring, docs, and demo/test requirements.
### Large multi-surface refactors
A consolidation refactor collapses behavior that has diverged across
multiple surfaces (`interactive_shell/`, `gateway/`, `tools/investigation/`,
`core/agent_harness/`, etc.) into one shared class or module — e.g. the
`agent_harness` T-2/T-3 series (session management, integration resolution,
startup consolidation). These are higher-risk than a normal feature or tool
change: they touch several call sites at once and the source issue's file
paths tend to be stale by the time work starts.
Before starting this class of work, follow
[REFACTOR_CHECKLIST.md](REFACTOR_CHECKLIST.md) — it covers dependency
ordering, re-validating the issue against current repo state, incremental
per-surface migration, and the import-boundary tests that must keep
enforcing the new pattern.
## 3. Rules (if X -> do Y)
- If core agent or pipeline logic changes -> run `make test-cov` and `make typecheck`.
- If a change consolidates or re-homes behavior across multiple surfaces (a "refactor" issue, not a localized fix) -> follow [REFACTOR_CHECKLIST.md](REFACTOR_CHECKLIST.md) before writing code and before opening the PR.
- If a new feature is shipped (tool, CLI command, pipeline behavior, integration) -> add a `docs/` page or section covering usage, configuration, and examples before the PR is opened.
- If a new `docs/` page is added or renamed -> register it in `docs/docs.json` under the correct `pages` array in the same PR (path without `.mdx`, e.g. `messaging/whatsapp` for `docs/messaging/whatsapp.mdx`).
- If an existing feature changes behavior, flags, or config shape -> update the relevant `docs/` page in the same PR; docs and code must stay in sync.
- When writing or editing a `docs/` page -> write for **users, not contributors**. Open with a command quick-reference table (command | what it does) if the page covers CLI commands. Follow with brief practical examples. Keep internal file formats, JSONL schemas, and implementation details out of user-facing pages — move those to `docs/DEVELOPMENT.md` or a contributor-only reference file if truly needed.
- If a tool's API or schema changes -> update docs in `docs/` and update the related unit tests, usually under `tests/tools/`. For investigation LLM tool-calling (any provider), follow [docs/investigation-tool-calling.md](docs/investigation-tool-calling.md).
- If adding or materially changing a tool/integration -> follow [TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md) in the same PR.
- If an integration changes -> update `tests/integrations/` and verify with `make verify-integrations`.
- If adding a new integration -> follow [TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md) before opening the PR for review.
- If adding new tests -> place them in `tests/`, never inside the source packages (no inline tests), except gateway tests which intentionally live in `gateway/tests/` per `gateway/AGENTS.md`.
- If CI-only tests are added -> mark them with the right pytest marker or place them in the appropriate e2e/synthetic/chaos folder so they do not run in the default local suite.
- If investigation branching or loop behavior changes -> update `tools/investigation/lifecycle.py` and the tests for that path.
- If adding or changing interactive REPL behavior (slash commands, session management, display output) -> use `ReplDriver` from `tests/utils/repl_driver.py` for live verification alongside unit tests; see [TESTING.md](TESTING.md).
- If pushing or creating a PR -> follow the full pre-push checklist in [CI.md](CI.md).
## 4. Testing
Test commands, turn-handling rules, CI-only paths: **[CI.md](CI.md)**. Live REPL testing with `ReplDriver`: **[TESTING.md](TESTING.md)**.
## 5. Footguns (common mistakes to avoid)
- No planning-stage fail-closed safeguard (v0.1): the interactive-shell action planner never denies a turn with "I couldn't safely decide actions". All terminal actions are read-only, so unmatched/ambiguous/chatty clauses run what they can and fall through to the assistant. Do **not** reintroduce a planner denial, the `mark_unhandled` tool, or the `UNHANDLED:` convention. Rationale and details: `core/agent_harness/AGENTS.md` and `docs/interactive-shell-action-policy.md`. If mutating actions are ever added, gate them at the execution stage (`tools/interactive_shell/shared/execution_policy.py`), not the planner.
- Vendored deps: No obvious vendored third-party dependencies are present. Python dependencies are managed in `pyproject.toml`, and the docs site has its own `docs/package.json` and `docs/pnpm-lock.yaml`. Do not vendor new libraries unless there is a strong reason.
- Secrets: Never commit `.env` - always use `.env.example` as the template. Use read-only credentials for production integrations.
- CI-only tests: Some e2e tests, including Kubernetes, EKS, and chaos engineering paths, require live infrastructure and are excluded from `make test-cov`. Do not expect them to pass locally without that environment.
- Legacy graph dev server: removed; use `make dev` for a local uvicorn hint or run investigations via the CLI.
- Docker requirement: Several targets, including the Grafana local stack and Chaos Mesh workflows, require a running Docker daemon.
- Docs navigation: Adding an `.mdx` file under `docs/` is not enough — Mintlify only shows pages listed in `docs/docs.json`. Forgetting the `pages` entry leaves the doc unreachable from the site sidebar.
- Investigation tool schemas: draft-07 JSON Schema (e.g. `"type": ["object", "null"]`) can pass loose checks but fail the LLM API on first invoke because **all** available investigation tools are sent together. Normalize in the provider adapter and extend registry contract tests; see [docs/investigation-tool-calling.md](docs/investigation-tool-calling.md).
- External-system code: `integrations/` owns config, clients, verifiers, and integration-local helpers; `tools/` owns every `@tool(...)` function and `BaseTool` class. Do not reintroduce top-level `vendors/` or `services/` packages.
- Compatibility shims: Do not leave modules whose only job is to re-export symbols from a new
location. Update callers to the canonical module and delete the old path.
- Empty or monolithic tool packages: Do not add a `tools/<name>/__init__.py`
that exists only to make discovery pass, and do not hide a non-trivial tool
implementation entirely in `__init__.py`. Use sibling modules for validation,
models, delivery/client calls, result shaping, and error handling whenever the
tool is more than a small function. Every tool must meet the implementation
and quality standards in the Adding a Tool section and
[TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md).
- Interactive-shell action selection: do not implement regex/keyword/fuzzy
intent routing, literal slash-command shortcuts, or deterministic action
bypasses around the action-agent AgentTool path. Engineers have been fired
before for implementing this exact shortcut. The runtime's literal-`/slash`
detection (`input_policy._literal_slash_command_text`) is terminal UI policy
only (spinner/stdin gating), not an execution path.
## 6. New Integration Checklist
Follow [TOOL_INTEGRATION_CHECKLIST.md](TOOL_INTEGRATION_CHECKLIST.md) — it is the single definition of done for all tool and integration work.
## 7. Large Refactor Checklist
Follow [REFACTOR_CHECKLIST.md](REFACTOR_CHECKLIST.md) — it is the single definition of done for refactors that consolidate or re-home behavior across multiple surfaces (see "Large multi-surface refactors" above).
+155
View File
@@ -0,0 +1,155 @@
# CI Readiness — Mandatory Push/PR Harness
This file is the **single source of truth** for local CI readiness before any push or PR.
## 0) Docs / process-only shortcut
If your diff is **only** documentation or contributor-process files, you may
skip the code-quality and test commands below.
Examples of files that qualify:
- `AGENTS.md`
- `CI.md`
- `CONTRIBUTING.md`
- `README.md`
- `TESTING.md`
- `TOOL_INTEGRATION_CHECKLIST.md`
- `docs/**/*.md`
- `docs/**/*.mdx`
- `docs/docs.json`
You may use the shortcut only when **all** changed files are non-runtime and
non-executable. If the diff touches application code, tests, build tooling,
dependency manifests, CI workflows, scripts, or anything with runtime impact,
run the normal harness.
For docs/process-only changes, the minimum required local check is:
```bash
git status --short
```
If you are unsure whether the shortcut applies, do **not** use it — run the
standard checks below.
## 1) Mandatory baseline checks (every code change that is not docs/process-only)
Run all of these first:
1. Clean working tree
```bash
git status --short
```
- No accidental untracked files
- Never commit `.env` or secrets
2. Lint
```bash
make lint
```
3. Format check
```bash
make format-check
```
If it fails:
```bash
make format && make format-check
```
4. Typecheck
```bash
make typecheck
```
## 2) Mandatory test harness (scope by touched modules)
**Recommended — run this instead of manually looking up the table below:**
```bash
make test-scope
```
`make test-scope` reads `git diff` against `main`, maps each changed path to
its test target(s) using [`.github/ci/test_scope_rules.py`](.github/ci/test_scope_rules.py),
and runs the minimal `pytest` invocation. It escalates automatically to
`make test-cov` when shared/core code is touched or 3+ app areas change.
Pass `ARGS=--dry-run` to preview without running.
### Manual lookup (reference only)
If you prefer to pick the command yourself, or need a focused `-k` filter,
see the `PathRule` entries in [`.github/ci/test_scope_rules.py`](.github/ci/test_scope_rules.py).
Rules with `always_escalate=True` map to `make test-cov`; all others list their
`test_targets` tuple. Changed files under `tests/` with no app rule run as-is.
## 3) Escalation rules (must run full unit CI suite)
Run `make test-cov` (instead of only targeted tests) when any of these are true:
- Shared/core code changed (`core/state/`, `core/domain/types/`, `tools/investigation/`, `tools/investigation/stages/`)
- 3+ app areas changed in one diff
- New files with unclear blast radius
- Cross-cutting refactor
- You are unsure test scope is sufficient
```bash
make test-cov
```
## 4) Conditional checks
If integration config, integration wiring, or related tools changed, also run:
```bash
make verify-integrations
```
## 5) Optional extra confidence
You may run `make check` as a final pass, but it is heavier (`test-full`) than the required harness.
## 6) Interactive-shell turn tests
Interactive-shell live turn tests always run with live coverage enabled. Do not use deselection filters like `-k "not live_llm"`. Fix failures by improving planner/tool correctness or updating fixtures only when behavior changes are explicitly approved.
For fast **local** iteration only, you can narrow the live suite with `--turn-select` (or the `TURN_SELECT` env var) without disabling live coverage:
- `--turn-select=complex:N` runs the N most complex scenarios (multi-step plans, `runs > 1`, gather contracts, and `@live` integrations score highest).
- `--turn-select=sample:N` runs a random N; add `--turn-select-seed` (or `TURN_SELECT_SEED`) for reproducibility.
- `N` may be a count (`5`), a fraction (`0.1`), or a percentage (`10%`); a bare `complex`/`sample` defaults to 5%.
```bash
# Most complex five scenarios
uv run python -m pytest tests/core/agent/test_turn_scenarios.py --turn-select=complex:5
# Random ~5% sample, reproducible
TURN_SELECT=sample:5% TURN_SELECT_SEED=7 uv run python -m pytest tests/core/agent/test_turn_scenarios.py
```
This is an iteration aid, not a substitute for full coverage: leave it unset for the pre-push/PR validation run, and never set it in CI (the sharded `turn-live` job runs every scenario).
In CI, [`.github/workflows/interactive-shell-live.yml`](.github/workflows/interactive-shell-live.yml) runs two jobs on same-repo PRs and post-merge `main` pushes: a no-LLM `turn-checks` gate (deterministic command detection + fixture integrity, `-m "not live_llm"`) and the sharded `turn-live` job (8 shards, live coverage). The no-LLM gate is a fast guardrail, not a substitute for live coverage.
`@live` gather scenarios **fail** (not skip) in GitHub Actions when integration credentials are missing; locally they may still skip. Natural-language investigation dispatch is **enabled** by default (`INTERACTIVE_SHELL_INVESTIGATION_ENABLED = True`). Investigation dispatch scenarios run in `turn-live`; if the flag is set to `False` for emergency rollback, those scenarios **skip** in live shards and `turn-checks` stays green. Require all `turn-checks` and `turn-live shard *` checks on `main` branch protection.
## 7) CI-only tests
Some paths require live infrastructure and are excluded from `make test-cov`:
- Kubernetes / EKS scenarios (`tests/e2e/`)
- Chaos Mesh workflows (`tests/chaos_engineering/`)
- Docker-dependent Grafana stack tests
Mark CI-only tests with the appropriate pytest marker or place them in the correct folder so they do not run locally by default.
## Precedence
If readiness instructions conflict across docs, **this file wins** for push/PR checks.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md @CLAUDE_PERSONAL.md
+17
View File
@@ -0,0 +1,17 @@
# Code of Conduct
We aim to keep the Tracer project respectful, constructive, and professional.
Please:
- be respectful in discussions
- focus on technical issues and improvements
- provide constructive feedback
Harassment, discrimination, or personal attacks are not tolerated.
If you encounter unacceptable behavior, contact:
support@opensre.com
Maintainers may remove comments, issues, or contributions that violate these expectations.
+305
View File
@@ -0,0 +1,305 @@
# Contributing
Welcome to OpenSRE
## Quick Links
- **GitHub:** [https://github.com/Tracer-Cloud/opensre](https://github.com/Tracer-Cloud/opensre)
- **Discord:** [https://discord.gg/opensre](https://discord.gg/opensre)
- **X/Twitter:** [@open_sre](https://x.com/open_sre)
## How to Contribute
Looking for a safe first contribution? See [Good First Issues](docs/good-first-issues/README.md).
Use the path that matches the kind of contribution you want to make:
1. **Bugs & small fixes** -> Open a PR. If you need to file an issue first, use the [bug report template](https://github.com/Tracer-Cloud/opensre/issues/new?template=bug_report.yml).
2. **New features or behavioral changes** -> Start with a [feature request](https://github.com/Tracer-Cloud/opensre/issues/new?template=feature_request.yml) or ask in Discord before coding. Most feature ideas are better shipped as third-party plugins via the plugin SDK.
3. **Improvements tied to concrete work** -> Use the [improvement template](https://github.com/Tracer-Cloud/opensre/issues/new?template=improvement.yml) when proposing a focused refactor, optimization, or quality improvement.
4. **Refactor-only PRs** -> Do not open one unless a maintainer explicitly asked for it as part of a real fix.
5. **Test/CI-only PRs for known `main` failures** -> Do not open one unless the change is required to validate a real fix the maintainers asked for.
6. **Questions** -> Use the docs, email [support@opensre.com](mailto:support@opensre.com), or ask in Discord [#contribute](http://discord.gg/opensre). GitHub Issues are for actionable work.
7. **Security issues** -> Follow `SECURITY.md`; do not open a public issue.
### Environment Setup
See **[SETUP.md](SETUP.md)** for detailed setup instructions including Windows-specific guidance. For benchmark, deployment detail, and telemetry reference, see **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)**.
**Quick start:**
1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and clone the repository (see [SETUP.md](SETUP.md) for Windows and alternatives)
2. Install dependencies: `make install`
3. Run checks: `make lint && make format-check && make typecheck && make test-cov`
- When invoking the CLI from your checkout, prefer **`uv run opensre …`** (see `SETUP.md` troubleshooting if another `opensre` shadows `.venv`).
4. Build release artifacts when needed: `make build`
If you prefer VS Code, use the devcontainer at [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json). Details: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#vs-code-dev-container).
---
**Contribution flow:**
1. **Find or create an issue** — Pick an existing one (Path A) or raise a new one (Path B)
2. **Request assignment** — Comment on the issue so maintainers know you're working on it
3. **Discuss (if needed)** — For features/changes, discuss approach in the issue before coding
4. **Fork and branch** — Create a branch for your work: `git checkout -b issue/123-description`
5. **Code and test** — Make changes, add tests, ensure all checks pass
6. **Submit a PR** — Open a pull request (or draft PR) linked to the issue; use the PR template
7. **Review & iterate** — Respond to feedback, make changes as needed
8. **Merge** — Maintainer merges once approved
**Detailed steps:** See the "Development Workflow" section below.
---
## Development Workflow
### 1. Create a Branch
```bash
git checkout -b issue/123-short-description
```
Use `issue/` or `fix/` prefix. Branch names should be lowercase with hyphens.
### 2. Make Changes
- Keep commits focused and logical
- Write clear commit messages: `"Fix: CLI returns error on incomplete commands"`
- One concern per commit when possible
### 2.1 Add a Tool (Fast Path: Single File)
For simple tools, you do not need a class or `ClassVar` metadata. Add one file under `tools/` and register a function with `@tool`.
Example (`tools/example_status_tool.py`):
```python
from core.tool_framework.tool_decorator import tool
@tool(source="knowledge")
def get_example_status(run_id: str, include_history: bool = False) -> dict[str, object]:
"""Return a lightweight status summary for a run."""
return {
"run_id": run_id,
"include_history": include_history,
}
```
Notes:
- `source` is required for function tools.
- `name`, `description`, and `input_schema` are inferred by default.
- `surfaces` defaults to `("investigation",)`. Pass `surfaces=("investigation", "chat")` to expose the tool in both investigation and chat contexts.
- Use the existing package/class style when a tool has complex helper logic, multiple exports, or substantial integration-specific code.
### 3. Add or Update Tests
- **Test Location:** New tests should be placed in the `tests/` directory, mirroring the source package area when useful (e.g., tests for `surfaces/cli/` go in `tests/cli/`).
- **No Inline Tests:** Avoid adding `*_test.py` files directly inside source packages. We are phasing out existing inline tests to keep the core logic clean.
- Bug fixes should include a test that would have caught the bug
- New features should have corresponding tests
- Aim for >80% code coverage (run `make test-cov` to check)
#### Tests under `tests/synthetic/` need an explicit `pytest.mark.synthetic` marker
The synthetic test tree has its own Make target (`make test-synthetic`) and is excluded from `make test-cov`. The two targets use marker filters:
- `make test-cov` runs `pytest --ignore=tests/synthetic -m "not synthetic"`, so the whole `tests/synthetic/` tree is excluded.
- `make test-synthetic` runs `pytest -m synthetic`, so a file without `pytest.mark.synthetic` is collected but skipped.
If you add a new test file under `tests/synthetic/`, declare the marker at module level so the file runs under `make test-synthetic`:
```python
import pytest
pytestmark = pytest.mark.synthetic
```
Without this marker the new file silently runs in **zero** standard CI configurations. The pattern is already in `tests/synthetic/rds_postgres/test_suite.py`; new files in the same tree should follow it.
See [#1671](https://github.com/Tracer-Cloud/opensre/issues/1671) for the meta-issue tracking this discoverability gap.
### 4. Run Local Checks (Required Before PR)
```bash
make lint # ruff: check code style
make format-check # ruff: check formatting (read-only)
make typecheck # mypy: check type annotations
make test-cov # pytest: run tests with coverage report
```
All four must pass. **CI will block merging if any fail.**
### Run one focused test
Replace the placeholders with your actual file or test name:
```bash
pytest tests/cli/test_.py # single file
pytest tests/cli/test_.py::test_ # single function
pytest tests/tools/ -k "test_registry" # tools example
pytest tests/synthetic/ -k "test_scenario" # no live infra needed
```
### 5. Open a Pull Request
Follow the PR template (see below). Link the relevant issue and describe what changed and why.
## Pull Request Guidelines
### How to Write a Good PR Description
Use the **[PR template](.github/PULL_REQUEST_TEMPLATE.md)** (automatically provided when you open a PR). Key sections:
- **Issue link:** `Fixes #123` (auto-closes the issue when merged)
- **Type of Change:** Select bug fix, feature, breaking change, or docs (helps categorize)
- **Description:** What changed and why
- **Testing:** How you tested it with specific steps and evidence
- **Impact Analysis:** Is it backward compatible? Any breaking changes? Performance impact?
### PR Checklist Before Submitting
- Linked to the relevant issue
- All local checks pass: `make lint && make format-check && make typecheck && make test-cov`
- Added tests for bug fixes or new features
- Updated documentation if behavior changed
- Code follows project style (see **Code Quality** section below)
- Self-reviewed your own code first
- Considered edge cases
### Greptile Code Review
We use [Greptile](https://greptile.com) for automated code review. Before a PR can be merged it must reach a **5/5 confidence score** with zero unresolved comments.
**Trigger a review** by posting this comment on your PR:
```
@greptile review
```
Wait 3060 seconds for the review to appear, then address each comment and re-trigger until you hit 5/5.
> **Automate the loop** — the [greploop skill](https://skills.sh/greptileai/skills/greploop) handles triggering, waiting, fixing, and re-reviewing automatically until 5/5 is reached.
### If Your PR Includes Screenshots or Logs
Provide **before** and **after** examples when:
- Changing CLI output or error messages
- Updating agent behavior
- Fixing a bug with visible impact
### AI-Assisted PRs
If you used AI tools (Claude, ChatGPT, Copilot, etc.) to generate code, the **[PR template](.github/PULL_REQUEST_TEMPLATE.md)** requires you to confirm:
- I reviewed **every single line** of AI-generated code (not just skimmed)
- I understand the logic and can explain it in my own words
- I tested edge cases (what could break?)
- I modified output to match project conventions ([Code Quality Standards](#code-quality-standards))
- Verified tests pass with the AI-generated code
This ensures you understand the code, not just copied it. Reviewers will pay extra attention to AI-assisted code.
## Code Quality Standards
- **Clarity over cleverness:** Code should be easy to understand and maintain
- **DRY principle:** Don't repeat yourself; extract common patterns
- **Strong typing:** Use type hints for all function parameters and returns
- **One responsibility:** Each function/class should do one thing well
- **Comments for "why":** Explain non-obvious logic; code already shows the "what"
- **Breaking changes:** Call them out explicitly in PR descriptions and docs
### Style & Formatting
We use:
- **ruff** for linting and import sorting
- **mypy** for strict type checking
- **Black-compatible** formatting (4-space indents)
- **pytest** for testing with coverage tracking
Run these before every commit:
```bash
make lint # Auto-fixes many style issues
make format-check # Checks formatting without modifying files
make typecheck # Catches type errors
make test-cov # Ensures tests pass and coverage is tracked
```
To verify the package can be shipped, run:
```bash
make build
```
## Reporting Bugs
Use the **[bug report template](https://github.com/Tracer-Cloud/opensre/issues/new?template=bug_report.yml)** when creating an issue. It guides you to include:
- **Summary:** One-line description of the bug (specific, not vague)
- **Expected behavior:** What should happen
- **Actual behavior:** What actually happens (with error message)
- **Reproduction steps:** Clear, minimal steps to consistently trigger the bug
- **Can you reproduce it consistently?** Every time / Intermittent / Sometimes
- **Environment:** OS, Python version, agent version, install method, relevant config
- **Error output:** Full error messages and logs (redact secrets like API keys)
- **Workarounds:** If you found a way to work around it
- **Context:** What were you trying to do? Is this blocking your work?
**Example:**
```
### Expected Behavior
`opensre investigate --org myorg` should return investigation results
### Actual Behavior
Command exits silently with no output
Error: exit code 0
### Steps to Reproduce
1. Run `opensre investigate --org myorg`
2. Observe output
### Environment
- OS: macOS 14.2
- Python: 3.11.5
- opensre version: v0.2.1
```
## Requesting Features
Use the **[feature request template](https://github.com/Tracer-Cloud/opensre/issues/new?template=feature_request.yml)** to propose new functionality. It guides you to clarify:
- **Problem statement:** Why do we need this? (focus on the problem, not solution)
- **Proposed solution:** How should it work? (specific and concrete with examples)
- **Acceptance criteria:** What needs to be true for this to be "done"?
- **Alternative approaches:** Other solutions you considered and why you prefer this one
- **Backward compatible?** Yes / No / Breaking changes (describe what changes)
- **Impact:** Which modules? New dependencies?
## Suggesting Improvements
Use the **[improvement template](https://github.com/Tracer-Cloud/opensre/issues/new?template=improvement.yml)** to propose refactors, optimizations, or quality improvements. It requires:
- **Current state:** How does it work now? (with code references)
- **Desired state:** How should it work instead?
- **Why it matters:** Performance? Maintainability? Reliability?
- **Scope:** One focused concern per issue (not bundled work)
- **Acceptance criteria:** How will we measure success?
- **Metrics:** Before and after values (e.g., "15ms → <1ms")
## Need Help?
- **Setup issues?** Check this guide first, then open an issue with details
- **How do I...?** Check the project docs or ask in a Discussion
- **Found a bug?** Open a bug report issue with the template
- **Have an idea?** Start a Discussion to gauge interest before opening an issue
## Licensing
By contributing, you agree that your contributions will be licensed under the project's license (see `LICENSE`).
+154
View File
@@ -0,0 +1,154 @@
## Deployment
OpenSRE has two primary deployment paths (both target AWS EC2) and a general hosted
runtime option for ASGI-compatible platforms.
---
## EC2 Deploy — Docker/ECR (web + gateway)
Runs `opensre-web` and `opensre-gateway` as Docker containers on a single EC2 instance.
The image is built once and pushed to ECR; subsequent redeploys reuse the cached image.
**Prerequisites:** Docker daemon running locally, AWS credentials with EC2 / ECR / IAM /
SSM permissions, region defaults to `us-east-1`.
Copy [`.env.deploy.example`](.env.deploy.example) and export the required variables:
| Variable | Required | Used by |
| -------- | -------- | ------- |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Yes (or role) | Provisioning |
| `TELEGRAM_BOT_TOKEN` | Yes | Gateway container |
| `TELEGRAM_ALLOWED_USERS` | Recommended | Gateway pairing gate |
| `LLM_PROVIDER` + API key | Yes | Both containers |
```bash
# Step 1 — build and push Docker image to ECR (run once per code change):
make build-image
# Step 2 — launch EC2 instance using the pre-built image (fast, no Docker build):
make deploy
# Tear down the stack (keeps ECR image by default):
make destroy
# Full teardown including ECR repository:
OPENSRE_DESTROY_PURGE_ECR=1 make destroy
```
After deploy:
```bash
curl http://<PublicIpAddress>:8000/health
```
Outputs (instance ID, public IP, image URI) are written to
`~/.opensre/deployments/opensre-ec2.json`.
`make deploy` auto-destroys any existing stack before provisioning a fresh one. Set
`OPENSRE_DEPLOY_ABORT_IF_EXISTS=1` to fail instead of auto-destroying.
---
## Gateway Deploy — AMI + systemd (Telegram gateway only)
Runs the Telegram gateway directly on EC2 as a systemd service — no Docker or ECR
required. The gateway is baked into a custom AMI once; subsequent deploys launch from
that AMI in ~23 minutes.
**Prerequisites:** AWS credentials with EC2 / IAM / SSM permissions. No Docker needed.
```bash
# Step 1 — bake a gateway AMI (run once per code change, takes ~5-10 minutes):
make bake-gateway
# Step 2 — launch EC2 instance from the saved AMI (fast):
make deploy-gateway
# Tear down (keeps AMI by default):
make destroy-gateway
# Full teardown including AMI deregistration:
OPENSRE_GATEWAY_DESTROY_PURGE_AMI=1 make destroy-gateway
```
Rollback to a previously baked AMI:
```bash
OPENSRE_GATEWAY_AMI_ID=ami-<previous-id> make deploy-gateway
```
Check the running gateway via SSM:
```bash
aws ssm start-session --target <InstanceId>
# inside:
sudo systemctl status opensre-gateway
sudo journalctl -u opensre-gateway -f
```
Outputs are written to `~/.opensre/deployments/opensre-gateway.json`.
After deploy, the web API is reachable publicly:
```bash
curl http://<PublicIpAddress>:8000/health
```
Restrict the allowed source CIDR with `OPENSRE_WEB_API_INGRESS_CIDR` (default `0.0.0.0/0`).
### Direct deploy (no pre-baked AMI)
Installs OpenSRE inline on a fresh EC2 instance via SSM — slower but requires no bake step:
```bash
make deploy-gateway-direct
make destroy-gateway-direct
```
---
## Comparison
| | Docker/ECR (`make deploy`) | Gateway (`make deploy-gateway`) |
| - | - | - |
| What deploys | web + gateway containers | gateway service only |
| Runtime | Docker inside EC2 | systemd on EC2 host |
| Shell access | Inside slim container | Full EC2 host |
| ECR repository | Required | Not needed |
| Update path | `make build-image && make deploy` | `make bake-gateway && make deploy-gateway` |
---
## Runtime Environment (Hosted / General)
Deploy OpenSRE as a standard Python/FastAPI app using the repo `Dockerfile`, Railway,
ECS, Vercel, or another ASGI-capable host.
1. Build and deploy using your hosting provider's normal workflow.
2. Set `LLM_PROVIDER` and the matching provider API key:
- `ANTHROPIC_API_KEY` when `LLM_PROVIDER=anthropic`
- `OPENAI_API_KEY` when `LLM_PROVIDER=openai`
- `OPENROUTER_API_KEY` when `LLM_PROVIDER=openrouter`
- `GEMINI_API_KEY` when `LLM_PROVIDER=gemini`
3. Add `DATABASE_URI` and `REDIS_URI` for hosted layouts that need persistence.
4. Add any additional environment variables required by your integrations.
Minimum environment:
```bash
LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=...
```
The full set of supported provider keys and optional model overrides is documented in
[`.env.example`](.env.example).
### Railway
Ensure the Railway project has Postgres and Redis services, and that the OpenSRE service
has `DATABASE_URI` and `REDIS_URI` set to those connection strings before deploying.
For telemetry labeling, set `OPENSRE_DEPLOYMENT_METHOD=railway` on the Railway service.
---
+43
View File
@@ -0,0 +1,43 @@
# Unified Dockerfile for OpenSRE
# Supports two runtime modes via MODE environment variable:
# MODE=web - FastAPI health application (default)
# MODE=gateway - Telegram two-way messaging gateway
#
# EC2 deploy (make deploy) runs both as separate containers on one instance.
#
# Web mode usage:
# docker build -t opensre:latest .
# docker run -p 8000:8000 --env-file .env opensre:latest
# curl http://localhost:8000/health
#
# Gateway mode usage:
# docker build -t opensre-gateway:latest .
# docker run -e MODE=gateway --env-file .env opensre-gateway:latest
#
# Required env vars for gateway mode:
# TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS, LLM_PROVIDER, API keys
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir .
ENV PORT=8000
ENV MODE=web
# Note: EXPOSE and HEALTHCHECK only apply to web mode
# Gateway mode uses outbound-only long-polling (no inbound HTTP)
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD if [ "$MODE" = "web" ]; then python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)" || exit 1; else exit 0; fi
CMD ["sh", "-c", "if [ \"$MODE\" = \"gateway\" ]; then exec opensre gateway start --foreground; else exec uvicorn gateway.webapp:app --host 0.0.0.0 --port ${PORT:-8000}; fi"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Tracer Cloud
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+570
View File
@@ -0,0 +1,570 @@
-include .env
export
.PHONY: install build onboard demo benchmark benchmark-update-readme \
alert-template investigate-alert verify-integrations check-docker \
grafana-local-up grafana-local-down grafana-local-seed \
cloudwatch-demo datadog-demo crashloop-demo prefect-demo \
flink-demo upstream-downstream \
test-rca test-rca-grafana test-synthetic test-rds-synthetic test-k8s-synthetic \
test-cloudopsbench download-cloudopsbench-hf validate-cloudopsbench \
simulate-k8s-alert test-k8s-local test-k8s test-k8s-datadog test-k8s-eks \
chaos-mesh-up chaos-mesh-down chaos-engineering-apply chaos-engineering-delete \
chaos-lab-up chaos-lab-down chaos-experiment-list chaos-experiment-up chaos-experiment-down \
deploy-dd-monitors cleanup-dd-monitors deploy-eks destroy-eks \
trigger-alert trigger-alert-verify regen-trigger-config \
prefect-local-test run dev docs-dev \
build-image deploy destroy test-deploy \
bake-gateway deploy-gateway destroy-gateway \
deploy-gateway-direct destroy-gateway-direct \
deploy-lambda deploy-prefect deploy-flink destroy-lambda destroy-prefect destroy-flink \
test test-full test-cov test-scope test-cli-smoke test-turn-live test-grafana \
rabbitmq-local-up rabbitmq-local-down test-rabbitmq-real \
test-openclaw test-openclaw-synthetic \
test-hermes test-hermes-synthetic test-hermes-synthetic-only refresh-hermes-tuples \
clean lint format-check format typecheck \
check-imports check-cycles check-layers check-imports-strict check-layers-strict check help
ifneq ($(wildcard .venv/bin/python),)
PYTHON = .venv/bin/python
PIP = .venv/bin/python -m pip
else ifeq ($(OS),Windows_NT)
ifneq ($(wildcard .venv/Scripts/python.exe),)
PYTHON = .venv/Scripts/python.exe
PIP = .venv/Scripts/python.exe -m pip
else
PYTHON = python
PIP = python -m pip
endif
else ifneq ($(shell command -v python3 2>/dev/null),)
PYTHON = python3
PIP = python3 -m pip
else
PYTHON = python
PIP = python -m pip
endif
# PIP_INSTALL_FLAGS = --user --break-system-packages
USER_BASE := $(shell $(PYTHON) -m site --user-base)
USER_BIN := $(if $(filter Windows_NT,$(OS)),$(USER_BASE)/Scripts,$(USER_BASE)/bin)
export PATH := $(if $(wildcard .venv/bin),$(CURDIR)/.venv/bin:,$(if $(wildcard .venv/Scripts),$(CURDIR)/.venv/Scripts:))$(USER_BIN):$(PATH)
PYTHON_SOURCE_PATHS := config core gateway integrations platform surfaces tools
# Create venv and install dependencies (requires https://docs.astral.sh/uv/)
install:
uv sync --frozen --extra dev
uv run python -m platform.analytics.install
build:
$(PYTHON) -m build
# Run the local onboarding flow
onboard:
opensre onboard
# Run Prefect ECS demo (default demo) - shows Investigation Trace in RCA
demo:
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e
# Run Benchmarking Script based on Synthetic Scenarios
benchmark:
$(PYTHON) -m tests.benchmarks.toolcall_model_benchmark.benchmark_generator
# Update README benchmark section from cached results (no LLM calls)
benchmark-update-readme:
$(PYTHON) -m tests.benchmarks.toolcall_model_benchmark.readme_updater
alert-template:
opensre investigate --print-template $(or $(TEMPLATE),generic)
investigate-alert:
@[ -n "$(ALERT)" ] || { echo "Usage: make investigate-alert ALERT=/path/to/alert.json"; exit 1; }
opensre investigate --input "$(ALERT)"
CLOUDOPSBENCH_HF_DATASET_ID ?= tracer-cloud/cloud-ops-bench-dataset
CLOUDOPSBENCH_DATASET_DIR ?= tests/benchmarks/cloudopsbench
CLOUDOPSBENCH_BENCHMARK_DIR ?= $(CLOUDOPSBENCH_DATASET_DIR)/benchmark
CLOUDOPSBENCH_HF_INCLUDE ?= benchmark/**
CLOUDOPSBENCH_LIMIT ?=
verify-integrations:
uv run opensre integrations verify $(if $(SERVICE),$(SERVICE),) $(if $(SLACK_TEST),--send-slack-test,)
check-docker:
@command -v docker >/dev/null 2>&1 || { echo "Docker is required for the live local Grafana stack. Install Docker Desktop or another Docker-compatible runtime, then rerun this target."; exit 1; }
@docker info >/dev/null 2>&1 || { echo "Docker is installed, but the Docker daemon is not running. Start Docker Desktop, OrbStack, or Colima, then rerun this target."; exit 1; }
grafana-local-up: check-docker
docker compose -f surfaces/cli/wizard/local_grafana_stack/docker-compose.yml up -d
grafana-local-down: check-docker
docker compose -f surfaces/cli/wizard/local_grafana_stack/docker-compose.yml down
grafana-local-seed:
$(PYTHON) -m surfaces.cli.wizard.grafana_seed
# Run CloudWatch demo
cloudwatch-demo:
$(PYTHON) -m tests.e2e.cloudwatch_demo.test_aws
# Run Datadog demo (local kind cluster + real DD monitor + investigation agent)
datadog-demo:
$(PYTHON) -m tests.e2e.datadog.test_local
# Run CrashLoopBackOff demo
crashloop-demo:
$(PYTHON) -m tests.e2e.crashloop.test_local
# Run Prefect ECS Fargate E2E test (alias for demo)
prefect-demo:
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e
# Run RCA tests from markdown alert files in tests/e2e/rca/ (pass FILE= to run one)
test-rca:
$(PYTHON) -m tests.e2e.rca.run_rca_test $(FILE)
# Run synthetic tests via pytest markers (fixture-based, no live infra required)
test-synthetic:
$(PYTHON) -m pytest -m synthetic -v tests/synthetic/
# Run synthetic RDS PostgreSQL RCA benchmark suite via the CLI runner (supports --json, --scenario)
test-rds-synthetic:
$(PYTHON) -m tests.synthetic.rds_postgres.run_suite $(if $(SCENARIO),--scenario $(SCENARIO),)
# Run synthetic Kubernetes RCA benchmark suite via the CLI runner (supports --json, --scenario, --mock-backends)
test-k8s-synthetic:
$(PYTHON) -m tests.synthetic.eks.run_suite $(if $(SCENARIO),--scenario $(SCENARIO),)
# Run Cloud-OpsBench RCA benchmark suite via the OpenSRE runner
test-cloudopsbench:
$(PYTHON) -m tests.benchmarks.cloudopsbench.run_suite --benchmark-dir "$(CLOUDOPSBENCH_BENCHMARK_DIR)" $(if $(SYSTEM),--system $(SYSTEM),) $(if $(FAULT),--fault-category $(FAULT),) $(if $(CASE),--case $(CASE),) $(if $(CLOUDOPSBENCH_LIMIT),--limit $(CLOUDOPSBENCH_LIMIT),$(if $(LIMIT),--limit $(LIMIT),))
# Download Cloud-OpsBench benchmark data from Hugging Face.
download-cloudopsbench-hf:
@command -v hf >/dev/null 2>&1 || { echo "Install the Hugging Face CLI with: pip install 'huggingface_hub[cli]'"; exit 1; }
hf download "$(CLOUDOPSBENCH_HF_DATASET_ID)" --repo-type dataset --local-dir "$(CLOUDOPSBENCH_DATASET_DIR)" --include "$(CLOUDOPSBENCH_HF_INCLUDE)"
validate-cloudopsbench:
$(PYTHON) -m tests.benchmarks.cloudopsbench.run_suite --benchmark-dir "$(CLOUDOPSBENCH_BENCHMARK_DIR)" --validate-only
# Boot local Grafana+Loki, seed deterministic test logs, then run the RCA pipeline
# Requires GRAFANA_INSTANCE_URL + GRAFANA_READ_TOKEN in .env (see .env.example for local defaults)
test-rca-grafana: grafana-local-up grafana-local-seed
$(PYTHON) -m tests.e2e.rca.run_rca_test grafana_pipeline_failure
# Run Kubernetes local alert simulation against the in-process investigation API
simulate-k8s-alert:
$(PYTHON) -m pytest tests/e2e/kubernetes_local_alert_simulation/test_simulation.py -s; \
EXIT=$$?; exit $$EXIT
# Run Kubernetes local test (kind)
test-k8s-local:
$(PYTHON) -m tests.e2e.kubernetes.test_local --both
# Run Kubernetes test (matches CI)
test-k8s:
$(PYTHON) -m tests.e2e.kubernetes.test_local
# Run Kubernetes + Datadog test (kind + DD Agent)
test-k8s-datadog:
$(PYTHON) -m tests.e2e.kubernetes.test_datadog
# Chaos Mesh on the kube context (default: kind-tracer-k8s-test). Override: make chaos-mesh-up KUBECTL_CONTEXT=...
# CHAOS_MESH_RUNTIME=containerd matches kind; use docker only on older clusters.
CHAOS_MESH_NS ?= chaos-mesh
KUBECTL_CONTEXT ?= kind-tracer-k8s-test
CHAOS_MESH_RUNTIME ?= containerd
HELM_KUBE := $(if $(KUBECTL_CONTEXT),--kube-context $(KUBECTL_CONTEXT),)
KUBECTL_FLAGS := $(if $(KUBECTL_CONTEXT),--context=$(KUBECTL_CONTEXT),)
chaos-mesh-up:
@helm repo list 2>/dev/null | grep -q '^chaos-mesh' || helm repo add chaos-mesh https://charts.chaos-mesh.org
helm repo update
kubectl create namespace $(CHAOS_MESH_NS) --dry-run=client -o yaml | kubectl apply -f - $(KUBECTL_FLAGS)
helm upgrade --install chaos-mesh chaos-mesh/chaos-mesh -n $(CHAOS_MESH_NS) \
--set chaosDaemon.runtime=$(CHAOS_MESH_RUNTIME) \
$(HELM_KUBE)
chaos-mesh-down:
-helm uninstall chaos-mesh -n $(CHAOS_MESH_NS) $(HELM_KUBE)
-kubectl delete namespace $(CHAOS_MESH_NS) $(KUBECTL_FLAGS)
# Apply chaos-engineering manifests on KUBECTL_CONTEXT (nginx target, CrashLoop deployment, PodChaos).
# Requires Chaos Mesh CRDs for pod-kill-demo.yaml (run make chaos-mesh-up first).
chaos-engineering-apply:
kubectl apply -f tests/chaos_engineering/chaos-demo.yaml $(KUBECTL_FLAGS)
kubectl apply -f tests/chaos_engineering/experiments/crashloop/crashloop-demo.yaml $(KUBECTL_FLAGS)
kubectl apply -f tests/chaos_engineering/pod-kill-demo.yaml $(KUBECTL_FLAGS)
chaos-engineering-delete:
-kubectl delete -f tests/chaos_engineering/pod-kill-demo.yaml $(KUBECTL_FLAGS)
-kubectl delete -f tests/chaos_engineering/experiments/crashloop/crashloop-demo.yaml $(KUBECTL_FLAGS)
-kubectl delete -f tests/chaos_engineering/chaos-demo.yaml $(KUBECTL_FLAGS)
# Full chaos lab: kind + Datadog + Chaos Mesh + baseline workloads (same defaults as README).
# Optional flags: CHAOS_LAB_FLAGS='--skip-kind' '--skip-datadog' '--no-wait-datadog' etc.
chaos-lab-up:
$(PYTHON) -m tests.chaos_engineering lab up $(CHAOS_LAB_FLAGS)
# Tear down lab (baseline, Chaos Mesh, Datadog namespace, kind cluster). Optional: CHAOS_LAB_DOWN_FLAGS='--keep-kind' '--keep-datadog'
chaos-lab-down:
$(PYTHON) -m tests.chaos_engineering lab down $(CHAOS_LAB_DOWN_FLAGS)
chaos-experiment-list:
$(PYTHON) -m tests.chaos_engineering experiment list
# Apply experiments/<EXPERIMENT>/ (*-demo.yaml then *-chaos.yaml). Example: make chaos-experiment-up EXPERIMENT=pod-failure
chaos-experiment-up:
@test -n "$(EXPERIMENT)" || (echo "Set EXPERIMENT=name (see: make chaos-experiment-list)" && false)
$(PYTHON) -m tests.chaos_engineering experiment apply $(EXPERIMENT)
chaos-experiment-down:
@test -n "$(EXPERIMENT)" || (echo "Set EXPERIMENT=name (see: make chaos-experiment-list)" && false)
$(PYTHON) -m tests.chaos_engineering experiment delete $(EXPERIMENT)
# Deploy Datadog monitors (requires DD_API_KEY + DD_APP_KEY)
deploy-dd-monitors:
$(PYTHON) -c "from tests.e2e.kubernetes.test_datadog import deploy_monitors; deploy_monitors()"
# Remove Datadog monitors created by tracer tests
cleanup-dd-monitors:
$(PYTHON) -c "from tests.e2e.kubernetes.test_datadog import cleanup_monitors; cleanup_monitors()"
# Deploy EKS cluster + ECR image for Kubernetes tests
deploy-eks:
$(PYTHON) -c "from tests.e2e.kubernetes.infrastructure_sdk.eks import deploy_eks_stack; deploy_eks_stack()"
# Destroy EKS cluster and all associated resources
destroy-eks:
$(PYTHON) -c "from tests.e2e.kubernetes.infrastructure_sdk.eks import destroy_eks_stack; destroy_eks_stack()"
# Run Kubernetes + Datadog test on EKS
test-k8s-eks:
$(PYTHON) -m tests.e2e.kubernetes.test_eks
# Fast: trigger a K8s alert in ~15s (fire-and-forget)
trigger-alert:
$(PYTHON) -m tests.e2e.kubernetes.trigger_alert
# Recreate centralized trigger API config JSON from AWS
regen-trigger-config:
$(PYTHON) -m tests.e2e.kubernetes.trigger_alert --regen-config
# Fast trigger + wait for Slack confirmation
trigger-alert-verify:
$(PYTHON) -m tests.e2e.kubernetes.trigger_alert --verify
# Run Prefect ECS local test
prefect-local-test:
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.test_local $(if $(CLOUD),--cloud,)
# Run upstream/downstream pipeline E2E test
upstream-downstream:
$(PYTHON) -m tests.e2e.upstream_lambda.test_agent_e2e
# Run Apache Flink ECS E2E test
flink-demo:
$(PYTHON) -m tests.e2e.upstream_apache_flink_ecs.test_agent_e2e
# Run the generic CLI (reads from stdin or --input)
run:
opensre investigate
dev:
@echo "Run the health app with: uv run uvicorn gateway.webapp:app --reload --host 0.0.0.0 --port 8000"
docs-dev:
cd docs && mint dev
# Deploy all test case infrastructure in parallel (SDK - fast!)
# EC2 deploy (web + gateway containers on one instance)
# Step 1 — build once per code change, saves URI locally for reuse:
build-image:
$(PYTHON) -m platform.deployment.ecr_deploy.lifecycle build-image
# Step 2 — launch instance using the pre-built image (fast, no Docker build):
deploy:
$(PYTHON) -m platform.deployment.ecr_deploy.lifecycle deploy
destroy:
$(PYTHON) -m platform.deployment.ecr_deploy.lifecycle destroy
test-deploy:
$(PYTHON) -m pytest tests/deployment/ec2/ -v -s
# Gateway deploy (Telegram gateway only, no Docker/ECR)
# Step 1 — bake once per code change (launches temp EC2, installs opensre, snapshots AMI):
bake-gateway:
$(PYTHON) -m platform.deployment.gateway.lifecycle bake-ami
# Step 2 — launch gateway instance from pre-baked AMI (fast):
deploy-gateway:
$(PYTHON) -m platform.deployment.gateway.lifecycle deploy
destroy-gateway:
$(PYTHON) -m platform.deployment.gateway.lifecycle destroy
# Gateway direct deploy (no pre-baked AMI — installs inline via SSM)
deploy-gateway-direct:
$(PYTHON) -m platform.deployment.gateway.lifecycle deploy-direct
destroy-gateway-direct:
$(PYTHON) -m platform.deployment.gateway.lifecycle destroy-direct
# Deploy Lambda test case
deploy-lambda:
@echo "Deploying Lambda stack..."
$(PYTHON) -m tests.e2e.upstream_lambda.infrastructure_sdk.deploy
# Deploy Prefect ECS test case
deploy-prefect:
@echo "Deploying Prefect ECS stack..."
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.infrastructure_sdk.deploy
# Deploy Flink ECS test case
deploy-flink:
@echo "Deploying Flink ECS stack..."
$(PYTHON) -m tests.e2e.upstream_apache_flink_ecs.infrastructure_sdk.deploy
# Destroy Lambda test case
destroy-lambda:
@echo "Destroying Lambda stack..."
$(PYTHON) -m tests.e2e.upstream_lambda.infrastructure_sdk.destroy
# Destroy Prefect ECS test case
destroy-prefect:
@echo "Destroying Prefect ECS stack..."
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.infrastructure_sdk.destroy
# Destroy Flink ECS test case
destroy-flink:
@echo "Destroying Flink ECS stack..."
$(PYTHON) -m tests.e2e.upstream_apache_flink_ecs.infrastructure_sdk.destroy
# Run fast tests + Prefect cloud E2E
test:
$(PYTHON) -m pytest -v surfaces/cli tests/utils
$(PYTHON) -m tests.e2e.upstream_prefect_ecs_fargate.test_agent_e2e
# Run full test suite (CI/CD)
test-full:
$(PYTHON) -m pytest -v
# Run tests with coverage (parallel via pytest-xdist).
# Keep tests/synthetic excluded here to match GitHub CI; marker filtering alone is
# not enough because some synthetic tests are collected without the synthetic mark.
test-cov:
$(PYTHON) -m pytest -n auto -v $(addprefix --cov=,$(PYTHON_SOURCE_PATHS)) --cov-report=term-missing --ignore=tests/e2e/kubernetes_local_alert_simulation --ignore=tests/synthetic -m "not synthetic"
# Run only the tests relevant to files changed on this branch (local use only).
# Pass ARGS=--dry-run to preview the command without executing it.
test-scope:
$(PYTHON) .github/ci/run_test_scope.py --base main $(ARGS)
# Run the CLI smoke suite against the installed opensre entrypoint.
test-cli-smoke:
$(PYTHON) -m pytest -v tests/cli/test_smoke.py
# Run the live-LLM turn scenario suite sharded across local processes, mirroring
# the CI turn-live job. The suite is IO-bound on LLM calls, so running all shards
# concurrently collapses wall time to ~one shard. Override shard count/subset:
# make test-turn-live ARGS="--shards 4"
# make test-turn-live ARGS="--indexes 0,3"
test-turn-live:
$(PYTHON) .github/ci/run_live_turn_shards.py $(ARGS)
# Run Grafana integration tests
test-grafana:
@echo "Running Grafana integration tests..."
$(PYTHON) -m pytest tests/e2e/grafana_validation/test_grafana_cloud_queries.py -v
# Spin up the local RabbitMQ stack (broker + publisher + slow consumer), wait
# for a backlog to accumulate, then exercise the read-only diagnostic tools
# against the real broker. Used for the screen-video demo; NOT part of CI.
RABBITMQ_COMPOSE = tests/e2e/rabbitmq/docker-compose.yml
rabbitmq-local-up:
@echo "Starting local RabbitMQ stack (broker + publisher + slow consumer)..."
docker compose -f $(RABBITMQ_COMPOSE) up -d
@echo "Waiting for broker to become healthy..."
@until docker compose -f $(RABBITMQ_COMPOSE) ps rabbitmq | grep -q "(healthy)"; do sleep 2; done
@echo "Broker healthy. Letting backlog build for 20s..."
@sleep 20
@echo "Ready."
rabbitmq-local-down:
docker compose -f $(RABBITMQ_COMPOSE) down -v
# Run OpenClaw integration + tool E2E tests (mocked transport, no live OpenClaw needed)
test-openclaw:
$(PYTHON) -m pytest tests/e2e/openclaw/ tests/integrations/openclaw/test_integration.py tests/tools/test_openclaw_mcp_tool.py tests/utils/test_openclaw_delivery.py -v
# Run synthetic OpenClaw investigation scenarios (FixtureOpenClawBackend, no live OpenClaw)
test-openclaw-synthetic:
$(PYTHON) -m tests.synthetic.openclaw.run_suite
# Run Hermes incident-identification suites: Hermes RCA synthetic tests + Hermes e2e.
test-hermes:
$(PYTHON) -m pytest tests/synthetic/hermes_rca tests/e2e/hermes -v
# Deterministic/no-key Hermes RCA synthetic checks only.
test-hermes-synthetic:
$(PYTHON) -m pytest tests/synthetic/hermes_rca -v
# Offline-only Hermes synthetic runner (scenario harness path).
test-hermes-synthetic-only:
$(PYTHON) -m tests.synthetic.hermes_rca.run_suite --offline-only
# Regenerate Hermes adapter tuple catalog.
refresh-hermes-tuples:
$(PYTHON) -m tests.synthetic.hermes_rca.refresh_adapter_tuples
# Run the RabbitMQ integration + tool tests, then invoke the verify command
# against the live broker. Requires the rabbitmq-local-up stack to be running.
test-rabbitmq-real:
@echo "Running mocked RabbitMQ unit + e2e tests..."
$(PYTHON) -m pytest tests/integrations/test_rabbitmq.py tests/tools/test_rabbitmq_*.py tests/e2e/rabbitmq/ -v
@echo ""
@echo "Verifying against the live broker (requires \`make rabbitmq-local-up\`)..."
RABBITMQ_HOST=127.0.0.1 \
RABBITMQ_USERNAME=sre_admin \
RABBITMQ_PASSWORD=sre_password \
RABBITMQ_VHOST=/orders \
$(PYTHON) -c "from integrations.rabbitmq import rabbitmq_config_from_env, validate_rabbitmq_config, get_queue_backlog, get_broker_overview; \
cfg = rabbitmq_config_from_env(); \
print('validate:', validate_rabbitmq_config(cfg)); \
print('overview:', get_broker_overview(cfg)); \
print('backlog:', get_queue_backlog(cfg, max_results=5))"
# Clean up
clean:
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
find . -maxdepth 1 \( -name '.coverage' -o -name '.coverage.*' \) -delete 2>/dev/null || true
rm -rf htmlcov/ 2>/dev/null || true
# Lint code
lint:
$(PYTHON) -m ruff check $(PYTHON_SOURCE_PATHS) tests/
# Check formatting (read-only; CI uses this)
format-check:
$(PYTHON) -m ruff format --check $(PYTHON_SOURCE_PATHS) tests/
# Format code
format:
$(PYTHON) -m ruff format $(PYTHON_SOURCE_PATHS) tests/
# Type check
typecheck:
$(PYTHON) -m mypy $(PYTHON_SOURCE_PATHS)
# Import graph: cycles + layering + forbidden direct edges (one command).
check-imports:
$(PYTHON) .github/ci/check_imports.py
# Deprecated aliases — use ``check-imports`` instead.
check-cycles check-layers: check-imports
# Optional: full transitive layer contracts (when .importlinter.strict exists).
check-imports-strict:
$(PYTHON) .github/ci/check_imports.py --strict
check-layers-strict: check-imports-strict
# Run all checks (lint + format read-only check + types + imports + full tests; mirrors CI quality gates)
check: lint format-check typecheck check-imports test-full
# Show help
help:
@echo "Available commands:"
@echo ""
@echo " EC2 DEPLOY (Docker/ECR — web + gateway)"
@echo " make build-image - Build and push Docker image to ECR (run once per code change)"
@echo " make deploy - Launch EC2 instance using pre-built image (fast, no Docker build)"
@echo " make destroy - Terminate EC2 instance and clean up (keeps ECR image; OPENSRE_DESTROY_PURGE_ECR=1 to also delete it)"
@echo " make test-deploy - Run EC2 deployment e2e tests"
@echo ""
@echo " GATEWAY DEPLOY (systemd, no Docker — gateway only)"
@echo " make bake-gateway - Bake a gateway AMI (run once per code change; saves AMI id locally)"
@echo " make deploy-gateway - Launch gateway EC2 instance from pre-baked AMI (fast)"
@echo " make destroy-gateway - Terminate gateway instance and clean up (set OPENSRE_GATEWAY_DESTROY_PURGE_AMI=1 to also deregister AMI)"
@echo ""
@echo " E2E TEST INFRA (AWS SDK)"
@echo " make deploy-lambda - Deploy Lambda stack (~50s)"
@echo " make deploy-prefect - Deploy Prefect ECS stack (~55s)"
@echo " make deploy-flink - Deploy Flink ECS stack (~90s)"
@echo " make destroy-lambda - Destroy Lambda stack"
@echo " make destroy-prefect - Destroy Prefect ECS stack"
@echo " make destroy-flink - Destroy Flink ECS stack"
@echo ""
@echo " DEMOS"
@echo " make demo - Run Prefect ECS E2E test (default, shows Investigation Trace)"
@echo " make grafana-local-up - Start the local Grafana + Loki stack"
@echo " make grafana-local-seed - Seed failure logs into the local Loki instance"
@echo " make alert-template TEMPLATE=datadog - Print a starter alert JSON template"
@echo " make investigate-alert ALERT=/path/to/alert.json - Run RCA against your own alert payload"
@echo " make verify-integrations - Check local store + .env integrations before running RCA"
@echo " make prefect-demo - Run Prefect ECS Fargate E2E test (alias for demo)"
@echo " make prefect-local-test - Run Prefect ECS local test (CLOUD=1 for ECS)"
@echo " make flink-demo - Run Apache Flink ECS E2E test"
@echo " make cloudwatch-demo - Run CloudWatch demo"
@echo " make datadog-demo - Run Datadog demo (local kind cluster + DD monitor + agent)"
@echo " make crashloop-demo - Run CrashLoopBackOff/OOMKill demo (no k8s needed, DD + Slack)"
@echo " make upstream-downstream - Run upstream/downstream Lambda E2E test"
@echo ""
@echo " KUBERNETES"
@echo " make test-k8s-local - Run Kubernetes local test (kind)"
@echo " make test-k8s - Run Kubernetes test (matches CI)"
@echo " make test-k8s-datadog - Run Kubernetes + Datadog test"
@echo " make chaos-mesh-up - Install Chaos Mesh (Helm; default context kind-tracer-k8s-test)"
@echo " make chaos-mesh-down - Uninstall Chaos Mesh + namespace"
@echo " make chaos-engineering-apply - Apply chaos-demo + crashloop + PodChaos (same context)"
@echo " make chaos-engineering-delete - Remove those workloads (PodChaos first)"
@echo " make chaos-lab-up / chaos-lab-down - Full lab (kind+DD+mesh+baseline; runs python -m tests.chaos_engineering)"
@echo " make chaos-experiment-list / chaos-experiment-up EXPERIMENT=... - Per-experiment apply"
@echo " make deploy-dd-monitors - Deploy Datadog monitors (DD_API_KEY + DD_APP_KEY)"
@echo " make cleanup-dd-monitors - Remove Datadog test monitors"
@echo " make deploy-eks - Deploy EKS cluster + ECR image"
@echo " make destroy-eks - Destroy EKS cluster and resources"
@echo " make test-k8s-eks - Run Kubernetes + Datadog test on EKS"
@echo ""
@echo " LOCAL DEVELOPMENT"
@echo " make install - Install dependencies"
@echo " make onboard - Run the OpenSRE onboarding flow"
@echo " make docs-dev - Start the local documentation preview (requires mint CLI)"
@echo ""
@echo " CLI (tab-completable, run 'opensre -h' for full help)"
@echo " opensre onboard - Interactive setup wizard"
@echo " opensre investigate -i alert.json - Run RCA on an alert payload"
@echo " opensre integrations list - Show configured integrations"
@echo " opensre integrations verify - Verify connectivity"
@echo ""
@echo " TESTING & QUALITY"
@echo " make test - Run fast unit tests + Prefect cloud E2E"
@echo " make test-full - Run full test suite (CI/CD)"
@echo " make test-cov - Run tests with coverage"
@echo " make test-cli-smoke - Run end-to-end CLI smoke tests"
@echo " make test-grafana - Run Grafana integration tests"
@echo " make test-rca - Run all RCA markdown alert tests in tests/e2e/rca/"
@echo " make test-rca FILE=pipeline_error_in_logs - Run a single RCA alert test"
@echo " make test-rds-synthetic - Run the synthetic RDS PostgreSQL RCA suite"
@echo " make test-openclaw - Run OpenClaw integration + e2e tests (skips when openclaw CLI absent)"
@echo " make test-hermes - Run Hermes synthetic + e2e suites"
@echo " make test-hermes-synthetic - Run Hermes RCA synthetic suite only (no-key deterministic path)"
@echo " make download-cloudopsbench-hf - Download Cloud-OpsBench from Hugging Face"
@echo " make test-cloudopsbench - Run the Cloud-OpsBench synthetic RCA suite"
@echo " make clean - Clean up cache files"
@echo " make lint - Lint code with ruff"
@echo " make format-check - Check formatting with ruff (read-only)"
@echo " make format - Format code with ruff"
@echo " make typecheck - Type check with mypy"
@echo " make check-imports - Import cycles, layers, and direct-edge checks"
@echo " make check-layers-strict - Full transitive layer contracts (.importlinter.strict)"
@echo " make check - Run all checks"
@echo " make benchmark - Run benchmark report generation"
@echo " make benchmark-update-readme - Update README from cached benchmark results"
+308
View File
@@ -0,0 +1,308 @@
<div align="center">
<p align="center">
<img src="docs/logo/opensre-logo-white.svg" alt="OpenSRE" width="360" />
</p>
<h1>OpenSRE v0.1: Build Your Own AI SRE Agents</h1>
<p>The open-source framework for AI SRE agents, and the training and evaluation environment they need to improve. Connect the 60+ tools you already run, define your own workflows, and investigate incidents on your own infrastructure.</p>
<p align="center">
<a href="https://github.com/Tracer-Cloud/opensre/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/Tracer-Cloud/opensre/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/Tracer-Cloud/opensre/releases"><img src="https://img.shields.io/badge/status-public%20alpha-orange?style=for-the-badge" alt="Project status: public alpha"></a>
<a href="https://github.com/Tracer-Cloud/opensre/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge" alt="Apache 2.0 License"></a>
<a href="https://discord.gg/7NTpevXf7w"><img src="https://img.shields.io/badge/Discord-Join%20Us-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://greptile.com"><img src="https://img.shields.io/badge/Sponsored%20by-Greptile-27E99F?style=for-the-badge&labelColor=3D3B4F&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMzY3IiBoZWlnaHQ9IjQyMCIgdmlld0JveD0iMCAwIDM2NyA0MjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI%2BCjxwYXRoIGQ9Ik0yNDAuMjY5IDQ5LjgxNTRMMTY2LjgwNCAxMTUuOTYzTDExNS45NjYgMTU5LjQ0TDE4MS4zMzUgMjIwLjU4NUwyNDkuNzg0IDE2Mi4wNDhMMTk2Ljc4IDExMi40N0wyNTMuMDY4IDYxLjc4ODFMMzYyLjYwNSAxNjQuMjQ2TDE3OC43MzkgMzIxLjQ4OUwzLjE0NTAyIDE1Ny4yNDJMMTg3LjAxMSAwTDI0MC4yNjkgNDkuODE1NFoiIGZpbGw9IiNGRkZGRkYiLz4KPHJlY3Qgd2lkdGg9IjIzNi40NTMiIGhlaWdodD0iODMuNDU2NiIgdHJhbnNmb3JtPSJtYXRyaXgoMC43NTQ3MSAtMC42NTYwNTkgMCAxIDE4OC4wMTcgMzM2LjU0NCkiIGZpbGw9IiNGRkZGRkYiLz4KPHJlY3Qgd2lkdGg9IjIzNi40NTMiIGhlaWdodD0iODMuNDU2NiIgdHJhbnNmb3JtPSJtYXRyaXgoMC43MzEzNTQgMC42ODE5OTggMCAxIDAgMTc0Ljk2MikiIGZpbGw9IiNGRkZGRkYiLz4KPC9zdmc%2BCg%3D%3D" alt="Sponsored by Greptile"></a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/25889" target="_blank">
<img
src="https://trendshift.io/api/badge/repositories/25889"
alt="Tracer-Cloud%2Fopensre | Trendshift"
style="height: 30px; width: auto;"
height="30"
/>
</a>
</p>
<p align="center">
<strong>
<a href="https://www.opensre.com/docs/quickstart">Quickstart</a> ·
<a href="https://www.opensre.com/docs">Docs</a> ·
<a href="https://opensre.com/docs/faq">FAQ</a> ·
<a href="https://trust.tracer.cloud/">Security</a>
</strong>
</p>
</div>
---
> 🚧 Public Alpha: Core workflows are usable for early exploration, though not yet fully stable. The project is in active development, and APIs and integrations may evolve
---
## Table of Contents
- [Why OpenSRE?](#why-opensre)
- [Install](#install)
- [Quick Start](#quick-start)
- [Deployment](#deployment)
- [How OpenSRE Works](#how-opensre-works)
- [Benchmark](#benchmark)
- [Capabilities & integrations](#capabilities--integrations)
- [Contributing & development](#contributing--development)
- [Security](#security)
- [Telemetry](#telemetry)
- [License](#license)
- [Citations](#citations)
---
## Why OpenSRE?
When something breaks in production, the evidence is scattered across logs, metrics, traces, runbooks, and Slack threads. OpenSRE is an open-source framework for AI SRE agents that resolve production incidents, built to run on your own infrastructure.
We do that because SWE-bench<sup>1</sup> gave coding agents scalable training data and clear feedback. Production incident response still lacks an equivalent.
Distributed failures are slower, noisier, and harder to simulate and evaluate than local code tasks, which is why AI SRE, and AI for production debugging more broadly, remains unsolved.
OpenSRE is building _that_ missing layer:
> an open reinforcement learning environment for agentic infrastructure incident response, with end-to-end tests and synthetic incident simulations for realistic production failures
We do that by:
- building easy-to-deploy, customizable AI SRE agents for production incident investigation and response
- running scored synthetic RCA suites that check root-cause accuracy, required evidence, and adversarial red herrings [(tests/synthetic)](tests/synthetic/rds_postgres)
- running real-world end-to-end tests across cloud-backed scenarios including Kubernetes, EC2, CloudWatch, Lambda, ECS Fargate, and Flink [(tests/e2e)](tests/e2e)
- keeping semantic test-catalog naming so e2e vs synthetic and local vs cloud boundaries stay obvious [(tests/README.md)](tests/README.md)
Our mission is to build AI SRE agents on top of this, scale it to thousands of realistic infrastructure failure scenarios, and establish OpenSRE as the benchmark and training ground for AI SRE.
<sup>1</sup> https://arxiv.org/abs/2310.06770
---
## Install
The root installer URL auto-detects Unix shell vs PowerShell and installs the latest build from `main`. OpenSRE moves quickly, so `main` is the latest stable version for normal installs.
macOS / Linux:
```bash
curl -fsSL https://install.opensre.com | bash
```
The macOS/Linux installer does not require sudo. If no writable bin directory is already on `PATH`, it installs to `~/.local/bin` and prints the shell command to apply the PATH update.
Equivalent explicit main-channel form:
```bash
curl -fsSL https://install.opensre.com | bash -s -- --main
```
Homebrew:
```bash
brew tap tracer-cloud/tap
brew install tracer-cloud/tap/opensre
```
Windows (PowerShell):
```powershell
irm https://install.opensre.com | iex
```
<!--
```bash
pipx install opensre
``` -->
---
## Quick Start
Configure once, then pick how you want to run investigations:
```bash
opensre onboard
```
**Interactive shell** — with no subcommand, `opensre` starts a REPL (TTY required). Describe incidents in plain language, stream investigations, and use slash commands for session control (`/help`, `/status`, `/cost`, `/sessions`, `/resume`, `/compact`, `/new`, `/exit`), integrations (`/integrations list`, `/integrations verify`), local agent fleet monitoring (`/agents`), and reasoning depth (`/effort` for **OpenAI** and **Codex**`low` through `max`). Ctrl+C cancels an in-flight investigation without losing session state. See **[interactive shell commands](https://www.opensre.com/docs/interactive-shell-commands)** for the full reference.
```bash
opensre
```
**One-shot investigation** — run the agent once against an alert file:
```bash
opensre investigate -i tests/e2e/kubernetes/fixtures/datadog_k8s_alert.json
```
**Remote runtime investigation** — investigate a deployed service by name (live health, logs, and deployment status):
```bash
opensre investigate --service api-backend
```
**Hermes log watch** — tail a Hermes `errors.log`, classify incidents, and optionally alert on Telegram:
```bash
opensre hermes watch
```
Other useful commands:
```bash
opensre integrations setup
opensre agents scan
opensre update
opensre uninstall # remove opensre and all local data
```
---
## Deployment
Two primary AWS EC2 paths and a general hosted option:
- **EC2 (Docker/ECR):** `make build-image` then `make deploy` — runs `opensre-web` and `opensre-gateway` containers on one instance.
- **Gateway (AMI + systemd):** `make bake-gateway` then `make deploy-gateway` — Telegram gateway only, no Docker, baked into a custom AMI.
- **Hosted (Railway / ECS / Vercel):** deploy with the repo `Dockerfile`; set `LLM_PROVIDER` and the matching API key (see [`.env.example`](.env.example)), plus `DATABASE_URI` and `REDIS_URI` if persistence is needed.
**[Full deployment steps and prerequisites → DEPLOYMENT.md](DEPLOYMENT.md)**
---
## How OpenSRE Works
<img
src="https://github.com/user-attachments/assets/936ab1f2-9bda-438d-9897-e8e9cd98e335"
width="1064"
height="568"
alt="opensre-how-it-works-github"
/>
When an alert fires, OpenSRE automatically:
1. **Fetches** the alert context and correlated logs, metrics, traces, and recent deploys
2. **Masks** sensitive identifiers (optional) before external LLM calls
3. **Reasons** across your connected systems to test hypotheses in a tool-calling loop
4. **Generates** a structured investigation report with probable root cause and linked evidence
5. **Suggests** next steps and, optionally, executes remediation actions
6. **Posts** a summary directly to Slack, PagerDuty, or Telegram — no context switching needed
For the current code-level agent architecture after removing the old graph and chain
framework layers, see [AGENTS.md](AGENTS.md).
---
## Benchmark
Regenerate numbers with **`make benchmark`**; refresh this table from cached results via **`make benchmark-update-readme`**. See **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#benchmark)** for details.
<!-- BENCHMARK-START -->
_No benchmark results yet._
<!-- BENCHMARK-END -->
---
## Capabilities & integrations
| | |
| ---------------------------------------- | -------------------------------------------------------------------------------- |
| 🔍 **Structured incident investigation** | Correlated root-cause analysis across logs, metrics, traces, deploys, and config |
| 📋 **Runbook-aware reasoning** | OpenSRE reads your runbooks and applies them automatically |
| 🔗 **Evidence-backed root cause** | Every conclusion is linked to the data behind it |
| 🛡️ **Reversible identifier masking** | Redact pods, clusters, and account IDs before external LLM calls; restore in output |
| 📊 **Session cost & history** | Per-session token tracking (`/cost`) and resumable REPL sessions (`/sessions`) |
| 👥 **Local agent fleet** | Monitor Claude Code, Cursor, Codex, and other coding agents on your machine |
| 🌐 **Remote runtime RCA** | Investigate deployed services by name with live health probes and recent logs |
| 📡 **Hermes log watch** | Tail Hermes error logs, classify incidents, and deliver Telegram alerts |
| 🤖 **Full LLM flexibility** | Bring your own model — Anthropic, OpenAI, Codex, Ollama, Gemini, OpenRouter, NVIDIA NIM, Bedrock |
OpenSRE connects to **60+** tools across LLMs, observability, cloud infrastructure, data platforms, incident management, and MCP. The full matrix (with roadmap links) lives in the **[product docs](https://www.opensre.com/docs)**; a detailed catalog is also maintained in-repo as the project grows.
---
## Integrations
OpenSRE connects to 60+ tools and services across the modern cloud stack, from LLM providers and observability platforms to infrastructure, databases, and incident management.
| Category | Integrations | Roadmap |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **AI / LLM Providers** | Anthropic · OpenAI · OpenAI Codex · Ollama · Google Gemini · OpenRouter · NVIDIA NIM · Bedrock | |
| **Observability** | <img src="docs/assets/icons/grafana.webp" width="16"> Grafana (Loki · Mimir · Tempo · annotations) · <img src="docs/assets/icons/datadog.svg" width="16"> Datadog · Honeycomb · Coralogix · <img src="docs/assets/icons/cloudwatch.png" width="16"> CloudWatch · <img src="docs/assets/icons/sentry.png" width="16"> Sentry · Elasticsearch · Better Stack · Splunk · Victoria Logs · SignOz · OpenObserve · OpenSearch · Azure Monitor · Hermes | [New Relic](https://github.com/Tracer-Cloud/opensre/issues/139) |
| **Infrastructure** | <img src="docs/assets/icons/kubernetes.png" width="16"> Kubernetes · <img src="docs/assets/icons/aws.png" width="16"> AWS (S3 · Lambda · EKS · EC2 · CloudTrail · Bedrock) · <img src="docs/assets/icons/gcp.jpg" width="16"> GCP · <img src="docs/assets/icons/azure.png" width="16"> Azure · ArgoCD · Helm · Jenkins | |
| **Database** | MongoDB · ClickHouse · PostgreSQL · MySQL · MariaDB · MongoDB Atlas · Azure SQL · Snowflake · Redis · RDS · Supabase | |
| **Data Platform** | Apache Airflow · Apache Kafka · Apache Spark · Prefect · RabbitMQ · Dagster | |
| **Dev Tools** | <img src="docs/assets/icons/github.webp" width="16"> GitHub · GitHub MCP · Bitbucket · GitLab | |
| **Incident Management** | <img src="docs/assets/icons/pagerduty.png" width="16"> PagerDuty · Opsgenie · Jira · Alertmanager · incident.io | [Trello](https://github.com/Tracer-Cloud/opensre/issues/361) · [ServiceNow](https://github.com/Tracer-Cloud/opensre/issues/314) · [Linear](https://github.com/Tracer-Cloud/opensre/issues/124) |
| **Communication** | <img src="docs/assets/icons/slack.png" width="16"> Slack · Google Docs · Discord · Telegram · WhatsApp | [Notion](https://github.com/Tracer-Cloud/opensre/issues/286) · [Teams](https://github.com/Tracer-Cloud/opensre/issues/138) · [Confluence](https://github.com/Tracer-Cloud/opensre/issues/313) |
| **Agent Deployment** | <img src="docs/assets/icons/vercel.png" width="16"> Vercel · <img src="docs/assets/icons/aws.png" width="16"> EC2 · <img src="docs/assets/icons/aws.png" width="16"> ECS · Railway | |
| **Protocols** | <img src="docs/assets/icons/mcp.svg" width="16"> MCP · <img src="docs/assets/icons/acp.png" width="16"> ACP · <img src="docs/assets/icons/openclaw.jpg" width="16"> OpenClaw | |
OpenSRE is community-built. Looking for a safe first contribution? Browse [`good first issue`](https://github.com/Tracer-Cloud/opensre/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tickets or see the [Good First Issues guide](docs/good-first-issues/README.md). See **[CONTRIBUTING.md](CONTRIBUTING.md)** for the full workflow.
**Local environment:** **[SETUP.md](SETUP.md)** (all platforms, Windows, MCP/OpenClaw).
**Developing in this repo:** **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)** (install from source, CI parity checks, dev container, benchmark, deployment detail, telemetry reference).
<p>
<a href="https://discord.gg/7NTpevXf7w">
<img src="https://img.shields.io/badge/Join%20our%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord" />
</a>
</p>
<p align="center">
<a href="https://www.star-history.com/?type=date&repos=Tracer-Cloud%2Fopensre">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=Tracer-Cloud/opensre&type=date&theme=dark&legend=top-left&sealed_token=LHlhQArnQVcZZDuHHjU19dJHIIzQx9WzB2xacqhOnA8REEAfcVO94FgGmjAMsR8iiPA3ELR-RmF_t2rtnLLj6ieZt6S4PGbFZ5Ev1HgIrg8KJNkJDYQob_BcV8MtWc2wQnahPyKX6B9PgqvoQxI7E6B6SkW7gXQEq1xLLGDZ2JHvv5b3kr7SYnUpq98y" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=Tracer-Cloud/opensre&type=date&legend=top-left&sealed_token=LHlhQArnQVcZZDuHHjU19dJHIIzQx9WzB2xacqhOnA8REEAfcVO94FgGmjAMsR8iiPA3ELR-RmF_t2rtnLLj6ieZt6S4PGbFZ5Ev1HgIrg8KJNkJDYQob_BcV8MtWc2wQnahPyKX6B9PgqvoQxI7E6B6SkW7gXQEq1xLLGDZ2JHvv5b3kr7SYnUpq98y" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=Tracer-Cloud/opensre&type=date&legend=top-left&sealed_token=LHlhQArnQVcZZDuHHjU19dJHIIzQx9WzB2xacqhOnA8REEAfcVO94FgGmjAMsR8iiPA3ELR-RmF_t2rtnLLj6ieZt6S4PGbFZ5Ev1HgIrg8KJNkJDYQob_BcV8MtWc2wQnahPyKX6B9PgqvoQxI7E6B6SkW7gXQEq1xLLGDZ2JHvv5b3kr7SYnUpq98y" />
</picture>
</a>
</p>
Thanks goes to these amazing people:
<!-- readme: contributors -start -->
<a href="https://github.com/Tracer-Cloud/opensre/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Tracer-Cloud/opensre&max=200" alt="Contributors" />
</a>
<!-- readme: contributors -end -->
---
## Security
OpenSRE is designed with production environments in mind: structured and auditable LLM prompts, local transcript handling by default, and no silent bulk export of raw logs. See **[SECURITY.md](SECURITY.md)** for responsible disclosure.
---
## Telemetry
PostHog (product analytics) and Sentry (errors) are **opt-out**. Quick disable:
```bash
export OPENSRE_NO_TELEMETRY=1
```
**[Full matrix, DSN override, and local event logging → docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#telemetry-and-privacy)**
---
## License
Apache 2.0 — see [LICENSE](LICENSE).
## Citations
<sup>1</sup> https://arxiv.org/abs/2310.06770
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`Tracer-Cloud/opensre`
- 原始仓库:https://github.com/Tracer-Cloud/opensre
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+39
View File
@@ -0,0 +1,39 @@
# Security Policy
OpenSRE is designed for security-sensitive compute environments. We prioritize transparency, least privilege, and minimizing data exposure.
## Trust Center
For our security posture, architecture overview, compliance information, and subprocessors, see:
https://trust.tracer.cloud/
## Reporting a Vulnerability
If you believe youve found a security vulnerability in Tracer, please report it privately.
**Please do not open a public GitHub issue** for security vulnerabilities.
Email: support@opensre.com
### What to include
To help us triage quickly, please include:
- a clear description of the issue
- affected component(s) and version(s)
- steps to reproduce (or a minimal proof-of-concept)
- observed / expected behavior
- impact assessment (what an attacker could achieve)
### Coordinated disclosure
Please avoid public disclosure until weve had a reasonable opportunity to investigate and remediate. Well coordinate timelines with you when appropriate.
## Non-security Support
If you need product help or documentation, use:
- Docs: https://opensre.com/docs
- Book a demo: https://opensre.com
- Email: support@opensre.com
+213
View File
@@ -0,0 +1,213 @@
# Development environment setup
## Prerequisites
- **Python 3.12+** — required by [`pyproject.toml`](pyproject.toml) (`requires-python = ">=3.12"`). CI workflows use **Python 3.13** (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). [`.tool-versions`](.tool-versions) pins Python **3.13**, **uv**, **ruff**, and **mypy** (versions aligned with [`uv.lock`](uv.lock) where applicable) plus Node/pnpm for mise/asdf-style managers — optional; normal flows install **ruff** and **mypy** into `.venv` via **`make install`** / **`uv sync`**.
- Git
- **[uv](https://docs.astral.sh/uv/getting-started/installation/)** — required for `make install` (locked deps from `uv.lock`)
- **Make** — standard on macOS/Linux; Windows options below
## Quick setup (all platforms)
1. Fork and clone:
```bash
git clone https://github.com/YOUR_USERNAME/opensre.git
cd opensre
```
2. Install uv if needed:
- **macOS/Linux:** `curl -LsSf https://astral.sh/uv/install.sh | sh` (or the [uv install guide](https://docs.astral.sh/uv/getting-started/installation/))
- **Windows (PowerShell):** `irm https://astral.sh/uv/install.ps1 | iex`
Or: `winget install --id astral-sh.uv -e`
3. Install dependencies:
```bash
make install
```
Without Make (equivalent to `make install`):
```bash
uv sync --frozen --extra dev
uv run python -m platform.analytics.install
```
4. Verify:
```bash
make lint && make format-check && make typecheck && make test-cov
```
`format-check` is what CI enforces for formatting; include it before opening a PR.
---
## VS Code dev container
1. Install the **Dev Containers** extension in VS Code.
2. Start Docker Desktop, OrbStack, Colima, or another Docker-compatible runtime on the host.
3. Open the repository and run **Dev Containers: Reopen in Container**.
The image is built from [`.devcontainer/Dockerfile`](.devcontainer/Dockerfile) (**Python 3.13**). **`postCreateCommand`** creates `.venv-devcontainer` and runs **`pip install -e '.[dev]'`** (not `uv`). The interpreter VS Code uses is `.venv-devcontainer/bin/python`.
On the host, most contributors use **`make install`** + **`uv run`** instead; both approaches are valid.
---
## Windows-specific setup
Windows does not ship **make**. Pick one path below.
### Option A: Chocolatey (recommended)
1. Open PowerShell **as Administrator**.
2. Install Chocolatey (review the script first):
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
```
3. Install make:
```powershell
choco install make
```
4. Restart the terminal and verify: `make --version`.
### Option B: winget
```powershell
winget install GnuWin32.Make
```
Restart the terminal, then `make --version`.
### Option C: No Make
Run equivalents from the repo root (same shell where `uv` is on `PATH`). Prefer **`make test-cov`** when possible — the full pytest line is in the [`Makefile`](Makefile) under the `test-cov` target (`pytest -n auto`, coverage, and ignores).
```bash
uv sync --frozen --extra dev
uv run python -m platform.analytics.install
uv run ruff check config core gateway integrations platform surfaces tools tests/
uv run ruff format --check config core gateway integrations platform surfaces tools tests/
uv run mypy config core gateway integrations platform surfaces tools
uv run pytest -n auto -v \
--cov=config --cov=core --cov=gateway --cov=integrations \
--cov=platform --cov=surfaces --cov=tools --cov-report=term-missing \
--ignore=tests/e2e/kubernetes_local_alert_simulation \
--ignore=tests/synthetic \
-m "not synthetic"
```
---
## Troubleshooting
### Commands not using the project environment
- Prefer **`uv run <command>`** from the repo root.
- Refresh deps: **`uv sync --frozen --extra dev`**.
### Command not found: python
- Install Python **3.12+** and ensure it is on `PATH` (`python --version`).
### Command not found: uv
- Install uv (links above), then restart the terminal.
### `make install` / `uv sync` fails
- Run commands from the repository root; ensure **`uv.lock`** is present.
- Upgrade uv: **`uv self update`**.
- If the lockfile does not match **`pyproject.toml`**, run **`uv lock`** locally and commit the updated lockfile (or open a PR).
### make: command not found (Windows)
- Install make (above) or use Option C.
### Import errors when running code
- Use **`uv run`** from the repo root.
- Re-run **`uv sync --frozen --extra dev`**.
### `opensre` does not pick up local code edits
`make install` installs this repo in **editable** mode into `.venv`, but another **`opensre`** may appear earlier on **`PATH`** (installer binary, version manager, `~/.local/bin`, etc.).
1. Prefer **`uv run opensre …`** from the repository root.
2. Or prepend the venv: `export PATH="$(pwd)/.venv/bin:$PATH"` (macOS/Linux), then **`hash -r`** / new shell, and confirm **`which opensre`** points at **`<repo>/.venv/bin/opensre`**.
---
## Verify your setup
```bash
make lint && make format-check && make typecheck && make test-cov
```
If those pass, you are ready to develop. Contribution flow: **[CONTRIBUTING.md](CONTRIBUTING.md)**. Deeper contributor topics (benchmark, deployment, telemetry detail): **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)**.
---
## Connecting OpenClaw
OpenSRE no longer exposes a separate `opensre-mcp` server. Instead, OpenSRE connects to the OpenClaw bridge directly to read recent conversation context and write RCA findings back into OpenClaw.
### 1. Configure observability
Run the full wizard once (**recommended**):
```bash
uv run opensre onboard
```
To add or reconfigure a **single** integration non-interactively:
```bash
uv run opensre integrations setup <service>
```
### 2. Configure the OpenClaw bridge
Use the wizard or the direct setup flow:
```bash
uv run opensre integrations setup openclaw
uv run opensre integrations verify openclaw
```
Recommended local settings:
```bash
OPENCLAW_MCP_MODE=stdio
OPENCLAW_MCP_COMMAND=openclaw
OPENCLAW_MCP_ARGS="mcp serve"
```
### 3. Run a test
```bash
uv run opensre investigate -i tests/fixtures/openclaw_test_alert.json
```
### 4. Optional: OpenSRE calls OpenClaw during RCA
```bash
export OPENCLAW_MCP_MODE=stdio
export OPENCLAW_MCP_COMMAND=openclaw
export OPENCLAW_MCP_ARGS="mcp serve"
```
Keep the OpenClaw gateway running while you investigate, then verify:
```bash
opensre integrations verify openclaw
```
+1
View File
@@ -0,0 +1 @@
"""Configuration package for OpenSRE constants and runtime config helpers."""
+678
View File
@@ -0,0 +1,678 @@
"""Global application configuration.
Clerk JWT configuration for both development and production environments.
These are public endpoints and issuer URLs, not secrets.
"""
import os
from collections.abc import Sequence
from dataclasses import dataclass
from difflib import get_close_matches
from enum import Enum
from typing import Literal
from pydantic import Field, ValidationError, field_validator, model_validator
from config.llm_auth.auth_method import (
LLM_AUTH_METHOD_ENV,
effective_llm_provider,
get_configured_llm_auth_method,
)
from config.llm_auth.credentials import status as credential_status
from config.llm_auth.provider_catalog import (
API_KEY_PROVIDER_ENVS,
SUPPORTED_PROVIDER_VALUES,
)
from config.local_env import bootstrap_opensre_env
from config.strict_config import StrictConfigModel
class LLMModelConfig(StrictConfigModel):
"""Configuration for an LLM provider's model variants.
Three tiers, ordered by capability/cost:
- ``reasoning_model`` — highest-capability model used for root-cause
diagnosis and other deep-reasoning steps (e.g. Claude Opus, GPT-5).
- ``classification_model`` — mid-tier model for tasks that need more
reasoning than a fast toolcall model but don't justify reasoning cost
(e.g. interactive-shell intent classification). Sonnet for Anthropic.
- ``toolcall_model`` — lightweight, low-latency model for simple tool
selection / action planning (e.g. Claude Haiku, GPT-5 mini).
"""
reasoning_model: str
classification_model: str
toolcall_model: str
max_tokens: int
class Environment(Enum):
"""Application environment."""
DEVELOPMENT = "development"
PRODUCTION = "production"
class ClerkConfig(StrictConfigModel):
"""Clerk JWT configuration for a specific environment."""
jwks_url: str
issuer: str
CLERK_CONFIG_DEV = ClerkConfig(
jwks_url="https://superb-jackal-75.clerk.accounts.dev/.well-known/jwks.json",
issuer="https://superb-jackal-75.clerk.accounts.dev",
)
CLERK_CONFIG_PROD = ClerkConfig(
jwks_url="https://clerk.tracer.cloud/.well-known/jwks.json",
issuer="https://clerk.tracer.cloud",
)
def get_environment() -> Environment:
"""Get current environment from ENV variable.
Returns:
Environment enum value based on ENV variable.
Defaults to DEVELOPMENT if not set or unrecognized.
"""
env_value = os.getenv("ENV", "development").lower()
if env_value in ("production", "prod"):
return Environment.PRODUCTION
return Environment.DEVELOPMENT
# JWT Configuration
JWT_ALGORITHM = "RS256"
JWKS_CACHE_TTL_SECONDS = 3600
# LLM Model Constants
DEFAULT_MAX_TOKENS = 4096
# Anthropic model constants
ANTHROPIC_REASONING_MODEL = "claude-opus-4-7"
ANTHROPIC_CLASSIFICATION_MODEL = "claude-sonnet-4-6"
ANTHROPIC_TOOLCALL_MODEL = "claude-haiku-4-5-20251001"
# OpenAI model constants
# Default to GPT-5.4 mini for both reasoning and toolcall paths; override via
# OPENAI_REASONING_MODEL / OPENAI_TOOLCALL_MODEL when needed.
OPENAI_REASONING_MODEL = "gpt-5.4-mini"
# Mid-tier mirrors the toolcall (mini) model by default — OpenAI's mini sits
# between full and nano, which matches the "Sonnet-equivalent" classification
# tier well enough; override via OPENAI_CLASSIFICATION_MODEL when needed.
OPENAI_CLASSIFICATION_MODEL = "gpt-5.4-mini"
OPENAI_TOOLCALL_MODEL = "gpt-5.4-mini"
# OpenRouter model constants
OPENROUTER_REASONING_MODEL = "openrouter/auto"
OPENROUTER_CLASSIFICATION_MODEL = "openrouter/auto"
OPENROUTER_TOOLCALL_MODEL = "openrouter/auto"
# DeepSeek model constants
DEEPSEEK_REASONING_MODEL = "deepseek-v4-pro"
DEEPSEEK_CLASSIFICATION_MODEL = "deepseek-v4-flash"
DEEPSEEK_TOOLCALL_MODEL = "deepseek-v4-flash"
# Gemini model constants (Google AI preview IDs; OpenAI-compatible endpoint)
# UNVERIFIED PLACEHOLDER — gemini-3.1-pro-preview / gemini-3.1-flash-lite-preview are
# forward-looking IDs that may not yet exist. Override via GEMINI_REASONING_MODEL env var.
GEMINI_REASONING_MODEL = "gemini-3.1-pro-preview"
GEMINI_CLASSIFICATION_MODEL = "gemini-3-flash-preview"
GEMINI_TOOLCALL_MODEL = "gemini-3.1-flash-lite-preview"
# NVIDIA NIM model constants
# Verified safe defaults from the NVIDIA API Catalog (build.nvidia.com).
# Override via NVIDIA_REASONING_MODEL, NVIDIA_TOOLCALL_MODEL, or NVIDIA_MODEL env vars.
NVIDIA_REASONING_MODEL = "meta/llama-3.1-405b-instruct"
NVIDIA_CLASSIFICATION_MODEL = "meta/llama-3.1-70b-instruct"
NVIDIA_TOOLCALL_MODEL = "meta/llama-3.1-8b-instruct"
# MiniMax model constants
MINIMAX_REASONING_MODEL = "MiniMax-M3"
MINIMAX_CLASSIFICATION_MODEL = "MiniMax-M2.7-highspeed"
MINIMAX_TOOLCALL_MODEL = "MiniMax-M2.7-highspeed"
# Groq model constants
GROQ_REASONING_MODEL = "llama-3.3-70b-versatile"
GROQ_CLASSIFICATION_MODEL = "llama-3.3-70b-versatile"
GROQ_TOOLCALL_MODEL = "llama-3.1-8b-instant"
# Azure OpenAI deployment-name defaults (must match your Azure deployment names).
AZURE_OPENAI_REASONING_MODEL = "gpt-5.4-mini"
AZURE_OPENAI_CLASSIFICATION_MODEL = "gpt-5.4-mini"
AZURE_OPENAI_TOOLCALL_MODEL = "gpt-5.4-mini"
DEFAULT_AZURE_OPENAI_API_VERSION = "2024-10-21"
# Base URLs for OpenAI-compatible providers
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
DEEPSEEK_BASE_URL = "https://api.deepseek.com" # no /v1 — DeepSeek serves the OpenAI-compatible API at the root path
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
NVIDIA_BASE_URL = "https://integrate.api.nvidia.com/v1"
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
GROQ_BASE_URL = "https://api.groq.com/openai/v1"
# Amazon Bedrock model constants (US cross-region inference profile IDs)
BEDROCK_REASONING_MODEL = "us.anthropic.claude-sonnet-4-6"
BEDROCK_CLASSIFICATION_MODEL = "us.anthropic.claude-sonnet-4-6"
BEDROCK_TOOLCALL_MODEL = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
# Ollama local model constants
DEFAULT_OLLAMA_MODEL = "llama3.2"
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
LLMProvider = Literal[
"anthropic",
"openai",
"openrouter",
"deepseek",
"gemini",
"nvidia",
"ollama",
"bedrock",
"minimax",
"groq",
"azure-openai",
"codex",
"cursor",
"claude-code",
"gemini-cli",
"antigravity-cli",
"opencode",
"kimi",
"copilot",
"grok-cli",
"pi",
]
LLM_PROVIDER_API_KEY_ENVS = API_KEY_PROVIDER_ENVS
# Runtime identifiers for ``LLMProvider`` members. Branch on these instead of
# bare string literals when routing on the active provider.
PROVIDER_ANTHROPIC: LLMProvider = "anthropic"
PROVIDER_OPENAI: LLMProvider = "openai"
PROVIDER_BEDROCK: LLMProvider = "bedrock"
PROVIDER_OLLAMA: LLMProvider = "ollama"
def get_configured_llm_provider() -> str:
"""Return the active LLM provider from env/project .env."""
bootstrap_opensre_env(override=False)
return os.getenv("LLM_PROVIDER", "anthropic").strip().lower() or "anthropic"
def get_llm_provider_api_key_env(provider: str | None = None) -> str | None:
"""Return the API-key env var required by an LLM provider, if any."""
provider_name = (provider or get_configured_llm_provider()).strip().lower()
auth_method = get_configured_llm_auth_method(provider_name)
if effective_llm_provider(provider_name, auth_method) != provider_name:
return None
return LLM_PROVIDER_API_KEY_ENVS.get(provider_name)
def get_llm_provider_api_key(provider: str | None = None) -> tuple[str | None, str]:
"""Return an env API key only; Keychain reads are request-scoped now."""
env_var = get_llm_provider_api_key_env(provider)
if env_var is None:
return None, ""
return env_var, os.getenv(env_var, "").strip()
def _llm_api_key_payload(provider: str) -> dict[str, str]:
"""Return no secrets; runtime resolves credentials request-time."""
_ = provider
return {}
def _llm_settings_env_payload(provider: str) -> dict[str, object]:
"""Build the raw env-backed payload used to validate LLM settings."""
return {
"provider": provider,
**_llm_api_key_payload(provider),
"anthropic_reasoning_model": os.getenv(
"ANTHROPIC_REASONING_MODEL", ANTHROPIC_REASONING_MODEL
).strip()
or ANTHROPIC_REASONING_MODEL,
"anthropic_classification_model": os.getenv(
"ANTHROPIC_CLASSIFICATION_MODEL", ANTHROPIC_CLASSIFICATION_MODEL
).strip()
or ANTHROPIC_CLASSIFICATION_MODEL,
"anthropic_toolcall_model": os.getenv(
"ANTHROPIC_TOOLCALL_MODEL", ANTHROPIC_TOOLCALL_MODEL
).strip()
or ANTHROPIC_TOOLCALL_MODEL,
"openai_reasoning_model": os.getenv(
"OPENAI_REASONING_MODEL", OPENAI_REASONING_MODEL
).strip()
or OPENAI_REASONING_MODEL,
"openai_classification_model": os.getenv(
"OPENAI_CLASSIFICATION_MODEL", OPENAI_CLASSIFICATION_MODEL
).strip()
or OPENAI_CLASSIFICATION_MODEL,
"openai_toolcall_model": os.getenv("OPENAI_TOOLCALL_MODEL", OPENAI_TOOLCALL_MODEL).strip()
or OPENAI_TOOLCALL_MODEL,
"openrouter_reasoning_model": os.getenv(
"OPENROUTER_REASONING_MODEL",
os.getenv("OPENROUTER_MODEL", OPENROUTER_REASONING_MODEL),
).strip()
or OPENROUTER_REASONING_MODEL,
"openrouter_classification_model": os.getenv(
"OPENROUTER_CLASSIFICATION_MODEL",
os.getenv("OPENROUTER_MODEL", OPENROUTER_CLASSIFICATION_MODEL),
).strip()
or OPENROUTER_CLASSIFICATION_MODEL,
"openrouter_toolcall_model": os.getenv(
"OPENROUTER_TOOLCALL_MODEL",
os.getenv("OPENROUTER_MODEL", OPENROUTER_TOOLCALL_MODEL),
).strip()
or OPENROUTER_TOOLCALL_MODEL,
"deepseek_reasoning_model": os.getenv(
"DEEPSEEK_REASONING_MODEL",
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_REASONING_MODEL),
).strip()
or DEEPSEEK_REASONING_MODEL,
"deepseek_classification_model": os.getenv(
"DEEPSEEK_CLASSIFICATION_MODEL",
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_CLASSIFICATION_MODEL),
).strip()
or DEEPSEEK_CLASSIFICATION_MODEL,
"deepseek_toolcall_model": os.getenv(
"DEEPSEEK_TOOLCALL_MODEL",
os.getenv("DEEPSEEK_MODEL", DEEPSEEK_TOOLCALL_MODEL),
).strip()
or DEEPSEEK_TOOLCALL_MODEL,
"gemini_reasoning_model": os.getenv(
"GEMINI_REASONING_MODEL",
os.getenv("GEMINI_MODEL", GEMINI_REASONING_MODEL),
).strip()
or GEMINI_REASONING_MODEL,
"gemini_classification_model": os.getenv(
"GEMINI_CLASSIFICATION_MODEL",
os.getenv("GEMINI_MODEL", GEMINI_CLASSIFICATION_MODEL),
).strip()
or GEMINI_CLASSIFICATION_MODEL,
"gemini_toolcall_model": os.getenv(
"GEMINI_TOOLCALL_MODEL",
os.getenv("GEMINI_MODEL", GEMINI_TOOLCALL_MODEL),
).strip()
or GEMINI_TOOLCALL_MODEL,
"nvidia_reasoning_model": os.getenv(
"NVIDIA_REASONING_MODEL",
os.getenv("NVIDIA_MODEL", NVIDIA_REASONING_MODEL),
).strip()
or NVIDIA_REASONING_MODEL,
"nvidia_classification_model": os.getenv(
"NVIDIA_CLASSIFICATION_MODEL",
os.getenv("NVIDIA_MODEL", NVIDIA_CLASSIFICATION_MODEL),
).strip()
or NVIDIA_CLASSIFICATION_MODEL,
"nvidia_toolcall_model": os.getenv(
"NVIDIA_TOOLCALL_MODEL",
os.getenv("NVIDIA_MODEL", NVIDIA_TOOLCALL_MODEL),
).strip()
or NVIDIA_TOOLCALL_MODEL,
"minimax_reasoning_model": os.getenv(
"MINIMAX_REASONING_MODEL",
os.getenv("MINIMAX_MODEL", MINIMAX_REASONING_MODEL),
).strip()
or MINIMAX_REASONING_MODEL,
"minimax_classification_model": os.getenv(
"MINIMAX_CLASSIFICATION_MODEL",
os.getenv("MINIMAX_MODEL", MINIMAX_CLASSIFICATION_MODEL),
).strip()
or MINIMAX_CLASSIFICATION_MODEL,
"minimax_toolcall_model": os.getenv(
"MINIMAX_TOOLCALL_MODEL",
os.getenv("MINIMAX_MODEL", MINIMAX_TOOLCALL_MODEL),
).strip()
or MINIMAX_TOOLCALL_MODEL,
"groq_reasoning_model": os.getenv(
"GROQ_REASONING_MODEL",
os.getenv("GROQ_MODEL", GROQ_REASONING_MODEL),
).strip()
or GROQ_REASONING_MODEL,
"groq_classification_model": os.getenv(
"GROQ_CLASSIFICATION_MODEL",
os.getenv("GROQ_MODEL", GROQ_CLASSIFICATION_MODEL),
).strip()
or GROQ_CLASSIFICATION_MODEL,
"groq_toolcall_model": os.getenv(
"GROQ_TOOLCALL_MODEL",
os.getenv("GROQ_MODEL", GROQ_TOOLCALL_MODEL),
).strip()
or GROQ_TOOLCALL_MODEL,
"azure_openai_base_url": os.getenv("AZURE_OPENAI_BASE_URL", "").strip(),
"azure_openai_api_version": os.getenv(
"AZURE_OPENAI_API_VERSION", DEFAULT_AZURE_OPENAI_API_VERSION
).strip()
or DEFAULT_AZURE_OPENAI_API_VERSION,
"azure_openai_reasoning_model": os.getenv(
"AZURE_OPENAI_REASONING_MODEL",
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_REASONING_MODEL),
).strip()
or AZURE_OPENAI_REASONING_MODEL,
"azure_openai_classification_model": os.getenv(
"AZURE_OPENAI_CLASSIFICATION_MODEL",
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_CLASSIFICATION_MODEL),
).strip()
or AZURE_OPENAI_CLASSIFICATION_MODEL,
"azure_openai_toolcall_model": os.getenv(
"AZURE_OPENAI_TOOLCALL_MODEL",
os.getenv("AZURE_OPENAI_MODEL", AZURE_OPENAI_TOOLCALL_MODEL),
).strip()
or AZURE_OPENAI_TOOLCALL_MODEL,
"bedrock_reasoning_model": os.getenv(
"BEDROCK_REASONING_MODEL", BEDROCK_REASONING_MODEL
).strip()
or BEDROCK_REASONING_MODEL,
"bedrock_classification_model": os.getenv(
"BEDROCK_CLASSIFICATION_MODEL", BEDROCK_CLASSIFICATION_MODEL
).strip()
or BEDROCK_CLASSIFICATION_MODEL,
"bedrock_toolcall_model": os.getenv(
"BEDROCK_TOOLCALL_MODEL", BEDROCK_TOOLCALL_MODEL
).strip()
or BEDROCK_TOOLCALL_MODEL,
"ollama_model": os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip()
or DEFAULT_OLLAMA_MODEL,
"ollama_host": os.getenv("OLLAMA_HOST", DEFAULT_OLLAMA_HOST).strip() or DEFAULT_OLLAMA_HOST,
"max_tokens": os.getenv("LLM_MAX_TOKENS", str(DEFAULT_MAX_TOKENS)),
}
class LLMSettings(StrictConfigModel):
"""Strict runtime configuration for selecting and authenticating an LLM provider."""
provider: LLMProvider = "anthropic"
anthropic_api_key: str = ""
openai_api_key: str = ""
openrouter_api_key: str = ""
deepseek_api_key: str = ""
gemini_api_key: str = ""
nvidia_api_key: str = ""
minimax_api_key: str = ""
groq_api_key: str = ""
azure_openai_api_key: str = ""
azure_openai_base_url: str = ""
azure_openai_api_version: str = DEFAULT_AZURE_OPENAI_API_VERSION
ollama_model: str = DEFAULT_OLLAMA_MODEL
ollama_host: str = DEFAULT_OLLAMA_HOST
anthropic_reasoning_model: str = ANTHROPIC_REASONING_MODEL
anthropic_classification_model: str = ANTHROPIC_CLASSIFICATION_MODEL
anthropic_toolcall_model: str = ANTHROPIC_TOOLCALL_MODEL
openai_reasoning_model: str = OPENAI_REASONING_MODEL
openai_classification_model: str = OPENAI_CLASSIFICATION_MODEL
openai_toolcall_model: str = OPENAI_TOOLCALL_MODEL
openrouter_reasoning_model: str = OPENROUTER_REASONING_MODEL
openrouter_classification_model: str = OPENROUTER_CLASSIFICATION_MODEL
openrouter_toolcall_model: str = OPENROUTER_TOOLCALL_MODEL
deepseek_reasoning_model: str = DEEPSEEK_REASONING_MODEL
deepseek_classification_model: str = DEEPSEEK_CLASSIFICATION_MODEL
deepseek_toolcall_model: str = DEEPSEEK_TOOLCALL_MODEL
gemini_reasoning_model: str = GEMINI_REASONING_MODEL
gemini_classification_model: str = GEMINI_CLASSIFICATION_MODEL
gemini_toolcall_model: str = GEMINI_TOOLCALL_MODEL
nvidia_reasoning_model: str = NVIDIA_REASONING_MODEL
nvidia_classification_model: str = NVIDIA_CLASSIFICATION_MODEL
nvidia_toolcall_model: str = NVIDIA_TOOLCALL_MODEL
minimax_reasoning_model: str = MINIMAX_REASONING_MODEL
minimax_classification_model: str = MINIMAX_CLASSIFICATION_MODEL
minimax_toolcall_model: str = MINIMAX_TOOLCALL_MODEL
groq_reasoning_model: str = GROQ_REASONING_MODEL
groq_classification_model: str = GROQ_CLASSIFICATION_MODEL
groq_toolcall_model: str = GROQ_TOOLCALL_MODEL
azure_openai_reasoning_model: str = AZURE_OPENAI_REASONING_MODEL
azure_openai_classification_model: str = AZURE_OPENAI_CLASSIFICATION_MODEL
azure_openai_toolcall_model: str = AZURE_OPENAI_TOOLCALL_MODEL
bedrock_reasoning_model: str = BEDROCK_REASONING_MODEL
bedrock_classification_model: str = BEDROCK_CLASSIFICATION_MODEL
bedrock_toolcall_model: str = BEDROCK_TOOLCALL_MODEL
max_tokens: int = Field(default=DEFAULT_MAX_TOKENS, gt=0)
@field_validator("ollama_host", mode="before")
@classmethod
def _normalize_ollama_host(cls, value: object) -> str:
host = str(value or DEFAULT_OLLAMA_HOST).strip() or DEFAULT_OLLAMA_HOST
if not host.startswith(("http://", "https://")):
host = f"http://{host}"
return host
@field_validator("azure_openai_base_url", mode="before")
@classmethod
def _normalize_azure_openai_base_url(cls, value: object) -> str:
from core.llm.providers.azure_openai import normalize_azure_openai_base_url
return normalize_azure_openai_base_url(str(value or ""))
@field_validator("provider", mode="before")
@classmethod
def _normalize_provider(cls, value: object) -> str:
provider = str(value or "anthropic").strip().lower() or "anthropic"
valid_providers = SUPPORTED_PROVIDER_VALUES
if provider in valid_providers:
return provider
suggestion = get_close_matches(provider, valid_providers, n=1)
if suggestion:
raise ValueError(
f"Unsupported LLM provider '{provider}'. Did you mean '{suggestion[0]}'?"
)
raise ValueError(
f"Unsupported LLM provider '{provider}'. Expected one of: {', '.join(valid_providers)}."
)
@model_validator(mode="after")
def _require_api_key_for_selected_provider(self) -> "LLMSettings":
if self.provider == "azure-openai" and not self.azure_openai_base_url:
raise ValueError(
"LLM provider 'azure-openai' requires AZURE_OPENAI_BASE_URL to be set."
)
return self
@classmethod
def from_env(cls) -> "LLMSettings":
"""Build validated LLM settings from environment variables."""
bootstrap_opensre_env(override=False)
return cls.model_validate(_llm_settings_env_payload(get_configured_llm_provider()))
@dataclass(frozen=True)
class LLMResolution:
"""Outcome of resolving LLM settings for the configured provider."""
settings: LLMSettings
configured_provider: str
resolved_provider: str
attempted_providers: tuple[str, ...]
missing_key_env: str | None
@property
def fell_back(self) -> bool:
"""True when the active provider differs from the configured one."""
return self.resolved_provider != self.configured_provider
def summary(self) -> str:
"""One-line, user-facing description of the active provider decision."""
return f"Using configured LLM provider '{self.resolved_provider}'."
def resolve_llm_settings_verbose(
fallback_providers: Sequence[str] = (),
) -> LLMResolution:
"""Resolve LLM settings without implicit provider fallback."""
bootstrap_opensre_env(override=False)
_ = fallback_providers
configured_provider = get_configured_llm_provider()
settings = LLMSettings.model_validate(_llm_settings_env_payload(configured_provider))
return LLMResolution(
settings=settings,
configured_provider=configured_provider,
resolved_provider=settings.provider,
attempted_providers=(configured_provider,),
missing_key_env=None,
)
def resolve_llm_settings(
fallback_providers: Sequence[str] = (),
) -> LLMSettings:
"""Resolve LLM settings for the configured provider only."""
return resolve_llm_settings_verbose(fallback_providers).settings
def describe_llm_resolution(
fallback_providers: Sequence[str] = (),
) -> str:
"""Return a human-readable LLM provider resolution report for diagnostics.
Safe to call even when no provider has usable credentials: instead of
raising it reports the missing-credentials condition. Intended for
``/status``, doctor commands, and CI diagnostics so operators no longer need
ad-hoc inline probes to see which provider is actually in use.
"""
try:
resolution = resolve_llm_settings_verbose(fallback_providers)
except ValidationError as exc:
configured = get_configured_llm_provider()
env_var = get_llm_provider_api_key_env(configured)
detail = exc.errors()[0].get("msg", str(exc)) if exc.errors() else str(exc)
lines = [
f"configured provider : {configured}",
"resolved provider : <none — no usable provider credentials>",
]
if env_var:
lines.append(f"required key : {env_var}")
lines.append(f"detail : {detail}")
return "\n".join(lines)
lines = [
f"configured provider : {resolution.configured_provider}",
f"resolved provider : {resolution.resolved_provider}",
f"auth method : {get_configured_llm_auth_method(resolution.resolved_provider)}",
"fell back : no",
f"providers attempted : {', '.join(resolution.attempted_providers)}",
]
auth_provider = effective_llm_provider(
resolution.resolved_provider,
get_configured_llm_auth_method(resolution.resolved_provider),
)
auth_status = credential_status(auth_provider)
lines.append(f"credential status : {auth_status.source} ({auth_status.detail})")
return "\n".join(lines)
def llm_provider_error_context(
fallback_providers: Sequence[str] = (),
) -> str:
"""Return a short bracketed provider context for prefixing error messages.
Never raises — diagnostics must not mask the original error. Returns an
empty string when resolution itself fails so callers can fall back to the
raw provider error untouched.
"""
try:
resolution = resolve_llm_settings_verbose(fallback_providers)
except Exception:
return ""
return f"[LLM provider: {resolution.resolved_provider}]"
def has_credentials_for_active_llm_provider() -> bool:
"""Return prompt-safe auth availability for the configured LLM provider."""
settings = resolve_llm_settings()
auth_status = credential_status(
effective_llm_provider(settings.provider, os.getenv(LLM_AUTH_METHOD_ENV))
)
return auth_status.configured and not auth_status.stale
# LLM Provider Configs
ANTHROPIC_LLM_CONFIG = LLMModelConfig(
reasoning_model=ANTHROPIC_REASONING_MODEL,
classification_model=ANTHROPIC_CLASSIFICATION_MODEL,
toolcall_model=ANTHROPIC_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
OPENAI_LLM_CONFIG = LLMModelConfig(
reasoning_model=OPENAI_REASONING_MODEL,
classification_model=OPENAI_CLASSIFICATION_MODEL,
toolcall_model=OPENAI_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
OPENROUTER_LLM_CONFIG = LLMModelConfig(
reasoning_model=OPENROUTER_REASONING_MODEL,
classification_model=OPENROUTER_CLASSIFICATION_MODEL,
toolcall_model=OPENROUTER_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
DEEPSEEK_LLM_CONFIG = LLMModelConfig(
reasoning_model=DEEPSEEK_REASONING_MODEL,
classification_model=DEEPSEEK_CLASSIFICATION_MODEL,
toolcall_model=DEEPSEEK_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
GROQ_LLM_CONFIG = LLMModelConfig(
reasoning_model=GROQ_REASONING_MODEL,
classification_model=GROQ_CLASSIFICATION_MODEL,
toolcall_model=GROQ_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
AZURE_OPENAI_LLM_CONFIG = LLMModelConfig(
reasoning_model=AZURE_OPENAI_REASONING_MODEL,
classification_model=AZURE_OPENAI_CLASSIFICATION_MODEL,
toolcall_model=AZURE_OPENAI_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
GEMINI_LLM_CONFIG = LLMModelConfig(
reasoning_model=GEMINI_REASONING_MODEL,
classification_model=GEMINI_CLASSIFICATION_MODEL,
toolcall_model=GEMINI_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
NVIDIA_LLM_CONFIG = LLMModelConfig(
reasoning_model=NVIDIA_REASONING_MODEL,
classification_model=NVIDIA_CLASSIFICATION_MODEL,
toolcall_model=NVIDIA_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
MINIMAX_LLM_CONFIG = LLMModelConfig(
reasoning_model=MINIMAX_REASONING_MODEL,
classification_model=MINIMAX_CLASSIFICATION_MODEL,
toolcall_model=MINIMAX_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
BEDROCK_LLM_CONFIG = LLMModelConfig(
reasoning_model=BEDROCK_REASONING_MODEL,
classification_model=BEDROCK_CLASSIFICATION_MODEL,
toolcall_model=BEDROCK_TOOLCALL_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
OLLAMA_LLM_CONFIG = LLMModelConfig(
reasoning_model=DEFAULT_OLLAMA_MODEL,
classification_model=DEFAULT_OLLAMA_MODEL,
toolcall_model=DEFAULT_OLLAMA_MODEL,
max_tokens=DEFAULT_MAX_TOKENS,
)
# Tracer API Configuration
TRACER_BASE_URL_DEV = "https://staging.tracer.cloud"
TRACER_BASE_URL_PROD = "https://app.tracer.cloud"
SLACK_CHANNEL = "tracer-rca-report-alerts"
def get_tracer_base_url() -> str:
"""Get Tracer base URL for current environment."""
return (
TRACER_BASE_URL_PROD if get_environment() == Environment.PRODUCTION else TRACER_BASE_URL_DEV
)
+49
View File
@@ -0,0 +1,49 @@
"""Application-wide constants."""
from __future__ import annotations
from config.constants.investigation import MAX_INVESTIGATION_LOOPS
from config.constants.paths import (
INTEGRATIONS_STORE_PATH,
OPENSRE_HOME_DIR,
OPENSRE_TMP_DIR,
ensure_opensre_tmp_dir,
get_store_path,
)
from config.constants.platform import IS_WINDOWS
from config.constants.posthog import (
DEFAULT_POSTHOG_BOUNCE_THRESHOLD,
DEFAULT_POSTHOG_BOUNCE_WINDOW,
DEFAULT_POSTHOG_TIMEOUT_SECONDS,
DEFAULT_POSTHOG_URL,
POSTHOG_CAPTURE_API_KEY,
POSTHOG_HOST,
)
from config.constants.sentry import (
SENTRY_DSN,
SENTRY_ERROR_SAMPLE_RATE,
SENTRY_IN_APP_INCLUDE,
SENTRY_MAX_BREADCRUMBS,
SENTRY_TRACES_SAMPLE_RATE,
)
__all__ = [
"DEFAULT_POSTHOG_BOUNCE_THRESHOLD",
"DEFAULT_POSTHOG_BOUNCE_WINDOW",
"DEFAULT_POSTHOG_TIMEOUT_SECONDS",
"DEFAULT_POSTHOG_URL",
"INTEGRATIONS_STORE_PATH",
"IS_WINDOWS",
"MAX_INVESTIGATION_LOOPS",
"OPENSRE_HOME_DIR",
"OPENSRE_TMP_DIR",
"POSTHOG_CAPTURE_API_KEY",
"POSTHOG_HOST",
"SENTRY_DSN",
"SENTRY_ERROR_SAMPLE_RATE",
"SENTRY_IN_APP_INCLUDE",
"SENTRY_MAX_BREADCRUMBS",
"SENTRY_TRACES_SAMPLE_RATE",
"ensure_opensre_tmp_dir",
"get_store_path",
]
+12
View File
@@ -0,0 +1,12 @@
"""Constants shared between orchestration routing and investigation stages."""
from __future__ import annotations
from typing import Final
MAX_INVESTIGATION_LOOPS = 20
# Approval tokens auto-expire after this many seconds (5 minutes).
DEFAULT_APPROVAL_EXPIRY_SECONDS: Final[int] = 300
__all__ = ["DEFAULT_APPROVAL_EXPIRY_SECONDS", "MAX_INVESTIGATION_LOOPS"]
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import contextlib
import os
import tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
PROJECT_ROOT = REPO_ROOT
SYNTHETIC_SCENARIOS_DIR = REPO_ROOT / "tests" / "synthetic" / "rds_postgres"
OPENSRE_HOME_DIR = Path.home() / ".opensre"
INTEGRATIONS_STORE_PATH = OPENSRE_HOME_DIR / "integrations.json"
OPENSRE_TMP_DIR = Path(tempfile.gettempdir()) / "opensre"
def get_store_path() -> Path:
override = os.getenv("OPENSRE_WIZARD_STORE_PATH", "").strip()
if override:
return Path(override).expanduser()
return OPENSRE_HOME_DIR / "opensre.json"
def ensure_opensre_tmp_dir() -> Path:
OPENSRE_TMP_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
with contextlib.suppress(OSError):
OPENSRE_TMP_DIR.chmod(0o700)
return OPENSRE_TMP_DIR
__all__ = [
"INTEGRATIONS_STORE_PATH",
"OPENSRE_HOME_DIR",
"OPENSRE_TMP_DIR",
"PROJECT_ROOT",
"REPO_ROOT",
"SYNTHETIC_SCENARIOS_DIR",
"ensure_opensre_tmp_dir",
"get_store_path",
]
+11
View File
@@ -0,0 +1,11 @@
"""Host platform flags shared across the application."""
from __future__ import annotations
import os
IS_WINDOWS: bool = os.name == "nt"
__all__ = [
"IS_WINDOWS",
]
+13
View File
@@ -0,0 +1,13 @@
"""Shared PostHog constants used across analytics and integrations."""
from __future__ import annotations
from typing import Final
POSTHOG_HOST: Final[str] = "https://us.i.posthog.com"
POSTHOG_CAPTURE_API_KEY: Final[str] = "phc_zutpVhmQw7oUmMkbawKNdYCKQWjpfASATtf5ywB75W2"
DEFAULT_POSTHOG_URL: Final[str] = POSTHOG_HOST
DEFAULT_POSTHOG_TIMEOUT_SECONDS: Final[float] = 15.0
DEFAULT_POSTHOG_BOUNCE_THRESHOLD: Final[float] = 0.6
DEFAULT_POSTHOG_BOUNCE_WINDOW: Final[str] = "24h"
+8
View File
@@ -0,0 +1,8 @@
"""Shared prompt-string constants."""
from __future__ import annotations
# Prefilled into the next prompt after a background synthetic test exits non-zero,
# so the user can ask the CLI assistant for a quick RCA explanation. Both the core
# prompt builders and the shell reference it, so it lives in config (the lowest layer).
SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST = "why did it fail?"
+14
View File
@@ -0,0 +1,14 @@
"""Sentry constants for OpenSRE runtime error monitoring."""
from __future__ import annotations
from typing import Final
SENTRY_DSN: Final[str] = (
"https://06d6b2b739eb2267864d12c6cad34e70"
"@o4509281671380992.ingest.us.sentry.io/4511150863482880"
)
SENTRY_ERROR_SAMPLE_RATE: Final[float] = 1.0
SENTRY_TRACES_SAMPLE_RATE: Final[float] = 1.0
SENTRY_MAX_BREADCRUMBS: Final[int] = 100
SENTRY_IN_APP_INCLUDE: Final[tuple[str, ...]] = ("app",)
+270
View File
@@ -0,0 +1,270 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from opentelemetry.sdk.resources import Resource
DEFAULT_INSTANCE_URL = "https://tracerbio.grafana.net"
DEFAULT_LOKI_UID = "grafanacloud-logs"
DEFAULT_TEMPO_UID = "grafanacloud-traces"
DEFAULT_MIMIR_UID = "grafanacloud-prom"
def _get_env(key: str, default: str = "") -> str:
return os.getenv(key, default)
def get_env(key: str, default: str = "") -> str:
load_env()
return _get_env(key, default)
def load_env(env_path: Path | str | None = None, *, override: bool = False) -> None:
if os.getenv("GRAFANA_CONFIG_SKIP_ENV_FILE") == "1":
return
if env_path is None:
env_path = Path.cwd() / ".env"
path = Path(env_path)
if not path.exists():
return
for line in path.read_text().splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or stripped.startswith(";"):
continue
if "=" not in stripped:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip().strip('"').strip("'")
if key and (override or key not in os.environ):
os.environ[key] = value
def get_account_read_token(account_id: str) -> str:
if account_id == "tracerbio":
return get_grafana_read_token()
return get_env(f"GRAFANA_{account_id.upper()}_READ_TOKEN", "")
def get_account_instance_url(account_id: str) -> str:
if account_id == "tracerbio":
return get_grafana_instance_url()
return get_env(f"GRAFANA_{account_id.upper()}_INSTANCE_URL", "")
def get_account_datasource_uids(account_id: str) -> tuple[str, str, str]:
load_env()
if account_id == "tracerbio":
return get_datasource_uids()
prefix = f"GRAFANA_{account_id.upper()}"
loki_uid = _get_env(f"{prefix}_LOKI_DATASOURCE_UID", DEFAULT_LOKI_UID)
tempo_uid = _get_env(f"{prefix}_TEMPO_DATASOURCE_UID", DEFAULT_TEMPO_UID)
mimir_uid = _get_env(f"{prefix}_MIMIR_DATASOURCE_UID", DEFAULT_MIMIR_UID)
return loki_uid, tempo_uid, mimir_uid
def list_account_ids() -> list[str]:
load_env()
accounts = {"tracerbio"}
for key in os.environ:
if key.startswith("GRAFANA_") and key.endswith("_READ_TOKEN"):
account_id = key[len("GRAFANA_") : -len("_READ_TOKEN")].lower()
if account_id:
accounts.add(account_id)
return sorted(accounts)
def get_grafana_read_token() -> str:
return get_env("GRAFANA_READ_TOKEN", "")
def get_grafana_instance_url() -> str:
return get_env("GRAFANA_INSTANCE_URL", DEFAULT_INSTANCE_URL)
def get_datasource_uids() -> tuple[str, str, str]:
load_env()
loki_uid = _get_env("GRAFANA_LOKI_DATASOURCE_UID", DEFAULT_LOKI_UID)
tempo_uid = _get_env("GRAFANA_TEMPO_DATASOURCE_UID", DEFAULT_TEMPO_UID)
mimir_uid = _get_env("GRAFANA_MIMIR_DATASOURCE_UID", DEFAULT_MIMIR_UID)
return loki_uid, tempo_uid, mimir_uid
def get_otlp_endpoint() -> str:
return get_env("GCLOUD_OTLP_ENDPOINT", "")
def get_otlp_auth_header() -> str:
return get_env("GCLOUD_OTLP_AUTH_HEADER", "")
def get_otel_protocol() -> str:
return get_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf")
def get_effective_otlp_endpoint() -> str:
load_env()
return _get_env("OTEL_EXPORTER_OTLP_ENDPOINT") or _get_env("GCLOUD_OTLP_ENDPOINT", "")
def get_otel_exporter_otlp_protocol(default: str = "grpc") -> str:
return get_env("OTEL_EXPORTER_OTLP_PROTOCOL", default)
def get_otel_exporter_otlp_metrics_protocol(default: str = "grpc") -> str:
load_env()
return _get_env(
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
_get_env("OTEL_EXPORTER_OTLP_PROTOCOL", default),
)
def get_otel_exporter_otlp_endpoint(default: str = "") -> str:
return get_env("OTEL_EXPORTER_OTLP_ENDPOINT", default)
def get_otel_exporter_otlp_metrics_endpoint(default: str = "") -> str:
return get_env("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", default)
def get_otel_exporter_otlp_headers(default: str = "") -> str:
return get_env("OTEL_EXPORTER_OTLP_HEADERS", default)
def get_aws_lambda_function_name(default: str = "") -> str:
return get_env("AWS_LAMBDA_FUNCTION_NAME", default)
def parse_otel_headers(headers_str: str | None = None) -> dict[str, str]:
headers_raw = headers_str if headers_str is not None else get_otel_exporter_otlp_headers()
headers: dict[str, str] = {}
if headers_raw:
for pair in headers_raw.split(","):
if "=" in pair:
key, value = pair.split("=", 1)
headers[key.strip()] = value.strip()
return headers
def get_hosted_logs_id() -> str:
return get_env("GCLOUD_HOSTED_LOGS_ID", "")
def get_hosted_logs_url() -> str:
return get_env("GCLOUD_HOSTED_LOGS_URL", "")
def get_hosted_metrics_id() -> str:
return get_env("GCLOUD_HOSTED_METRICS_ID", "")
def get_hosted_metrics_url() -> str:
return get_env("GCLOUD_HOSTED_METRICS_URL", "")
def get_hosted_traces_id() -> str:
return get_env("GCLOUD_HOSTED_TRACES_ID", "")
def get_hosted_traces_url() -> str:
load_env()
traces_url = _get_env("GCLOUD_HOSTED_TRACES_URL_TEMPO") or _get_env(
"GCLOUD_HOSTED_TRACES_URL", ""
)
return traces_url
def get_rw_api_key() -> str:
return get_env("GCLOUD_RW_API_KEY", "")
def _is_grafana_hostname(endpoint: str) -> bool:
hostname = urlparse(endpoint).hostname or ""
return (
hostname == "grafana.net"
or hostname.endswith(".grafana.net")
or hostname == "grafana.com"
or hostname.endswith(".grafana.com")
)
def is_grafana_otlp_endpoint(value: str | None = None) -> bool:
endpoint = value if value is not None else get_effective_otlp_endpoint()
return _is_grafana_hostname(endpoint)
def configure_grafana_cloud(env_file: Path | str | None = None) -> None:
"""
Configure OTLP to send telemetry to Grafana Cloud.
Args:
env_file: Optional path to .env file containing Grafana Cloud credentials.
If provided, loads environment variables from this file.
Raises:
ValueError: If GCLOUD_OTLP_ENDPOINT is not set after loading .env.
Environment variables used:
- GCLOUD_OTLP_ENDPOINT: Grafana Cloud OTLP endpoint (required)
- GCLOUD_OTLP_AUTH_HEADER: Authorization header value (optional but recommended)
"""
load_env(env_file)
endpoint = get_otlp_endpoint()
auth_header = get_otlp_auth_header()
if not endpoint:
raise ValueError("GCLOUD_OTLP_ENDPOINT not set in environment")
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint
os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = "http/protobuf"
os.environ["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = "cumulative"
if auth_header:
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization={auth_header}"
def apply_otel_env_defaults() -> None:
"""Apply OpenTelemetry environment defaults, preferring Grafana Cloud config if available."""
load_env()
gcloud_endpoint = get_otlp_endpoint()
if not get_otel_exporter_otlp_endpoint() and gcloud_endpoint:
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = gcloud_endpoint
gcloud_auth = get_otlp_auth_header()
if not get_otel_exporter_otlp_headers() and gcloud_auth:
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization={gcloud_auth}"
if not get_otel_exporter_otlp_protocol(default="") and gcloud_endpoint:
os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = "http/protobuf"
def validate_grafana_cloud_config() -> bool:
"""Validate that Grafana Cloud configuration is present when using cloud endpoints."""
endpoint = get_effective_otlp_endpoint()
if _is_grafana_hostname(endpoint):
required_values = {
"GCLOUD_HOSTED_METRICS_ID": get_hosted_metrics_id(),
"GCLOUD_HOSTED_METRICS_URL": get_hosted_metrics_url(),
"GCLOUD_HOSTED_LOGS_ID": get_hosted_logs_id(),
"GCLOUD_HOSTED_LOGS_URL": get_hosted_logs_url(),
"GCLOUD_RW_API_KEY": get_rw_api_key(),
"GCLOUD_OTLP_ENDPOINT": get_otlp_endpoint(),
"GCLOUD_OTLP_AUTH_HEADER": get_otlp_auth_header(),
}
missing = [key for key, value in required_values.items() if not value]
if missing:
raise ValueError(
f"Grafana Cloud endpoint detected but missing env vars: {', '.join(missing)}"
)
return True
def build_resource(service_name: str, extra_attributes: dict[str, Any] | None) -> Resource:
attributes: dict[str, Any] = {"service.name": service_name}
if extra_attributes:
attributes.update(extra_attributes)
return Resource.create(attributes)
+79
View File
@@ -0,0 +1,79 @@
"""Config-owned storage helpers for LLM provider auth metadata."""
from config.llm_auth.auth_method import (
API_KEY_AUTH_METHOD,
LLM_AUTH_METHOD_ENV,
OAUTH_AUTH_METHOD,
OAUTH_BACKEND_PROVIDER_BY_PROVIDER,
OAUTH_PROVIDER_BY_BACKEND_PROVIDER,
canonical_llm_provider,
effective_llm_provider,
get_configured_llm_auth_method,
normalize_llm_auth_method,
supports_oauth_auth_method,
)
from config.llm_auth.credentials import (
CredentialResolution,
CredentialSource,
CredentialStatus,
MissingLLMCredentialError,
delete,
has_api_key_env_status,
require_for_request,
resolve_api_key_env_for_request,
resolve_for_request,
save_api_key,
source_for_api_key_env,
status,
verify,
)
from config.llm_auth.provider_catalog import (
API_KEY_PROVIDER_ENVS,
KEYLESS_PROVIDER_VALUES,
PROVIDER_SPECS,
SUPPORTED_PROVIDER_VALUES,
ProviderSpec,
provider_spec,
)
from config.llm_auth.records import (
delete_provider_auth_record,
provider_auth_record_name,
resolve_provider_auth_record,
save_provider_auth_record,
)
__all__ = [
"API_KEY_PROVIDER_ENVS",
"API_KEY_AUTH_METHOD",
"CredentialResolution",
"CredentialSource",
"CredentialStatus",
"KEYLESS_PROVIDER_VALUES",
"LLM_AUTH_METHOD_ENV",
"MissingLLMCredentialError",
"OAUTH_AUTH_METHOD",
"OAUTH_BACKEND_PROVIDER_BY_PROVIDER",
"OAUTH_PROVIDER_BY_BACKEND_PROVIDER",
"PROVIDER_SPECS",
"ProviderSpec",
"SUPPORTED_PROVIDER_VALUES",
"canonical_llm_provider",
"delete",
"delete_provider_auth_record",
"effective_llm_provider",
"get_configured_llm_auth_method",
"has_api_key_env_status",
"normalize_llm_auth_method",
"provider_auth_record_name",
"provider_spec",
"require_for_request",
"resolve_provider_auth_record",
"resolve_api_key_env_for_request",
"resolve_for_request",
"save_api_key",
"save_provider_auth_record",
"source_for_api_key_env",
"status",
"supports_oauth_auth_method",
"verify",
]
+75
View File
@@ -0,0 +1,75 @@
"""LLM provider auth-method selection.
The public provider remains the vendor name (for example ``openai``), while
some auth methods use a provider-specific runtime backend under the hood.
"""
from __future__ import annotations
import os
from typing import Literal
LLM_AUTH_METHOD_ENV = "LLM_AUTH_METHOD"
LLMAuthMethod = Literal["api_key", "oauth"]
API_KEY_AUTH_METHOD: LLMAuthMethod = "api_key"
OAUTH_AUTH_METHOD: LLMAuthMethod = "oauth"
OAUTH_BACKEND_PROVIDER_BY_PROVIDER: dict[str, str] = {
"openai": "codex",
"anthropic": "claude-code",
}
OAUTH_PROVIDER_BY_BACKEND_PROVIDER: dict[str, str] = {
backend: provider for provider, backend in OAUTH_BACKEND_PROVIDER_BY_PROVIDER.items()
}
def normalize_llm_auth_method(value: str | None) -> LLMAuthMethod:
"""Return a supported auth method, defaulting to API-key auth."""
normalized = (value or "").strip().lower()
if normalized == OAUTH_AUTH_METHOD:
return OAUTH_AUTH_METHOD
return API_KEY_AUTH_METHOD
def supports_oauth_auth_method(provider: str) -> bool:
"""Whether onboarding exposes OAuth for this public provider."""
return provider.strip().lower() in OAUTH_BACKEND_PROVIDER_BY_PROVIDER
def canonical_llm_provider(provider: str) -> str:
"""Map legacy OAuth backend provider values to their public provider."""
normalized_provider = provider.strip().lower()
return OAUTH_PROVIDER_BY_BACKEND_PROVIDER.get(normalized_provider, normalized_provider)
def get_configured_llm_auth_method(provider: str | None = None) -> LLMAuthMethod:
"""Return the active auth method from env, with legacy CLI compatibility."""
normalized_provider = (provider or os.getenv("LLM_PROVIDER") or "").strip().lower()
if normalized_provider in OAUTH_PROVIDER_BY_BACKEND_PROVIDER:
return OAUTH_AUTH_METHOD
return normalize_llm_auth_method(os.getenv(LLM_AUTH_METHOD_ENV))
def effective_llm_provider(provider: str, auth_method: str | None = None) -> str:
"""Map a public provider/auth pair to the runtime provider implementation."""
normalized_provider = provider.strip().lower()
method = normalize_llm_auth_method(auth_method)
if method == OAUTH_AUTH_METHOD:
return OAUTH_BACKEND_PROVIDER_BY_PROVIDER.get(normalized_provider, normalized_provider)
return normalized_provider
__all__ = [
"API_KEY_AUTH_METHOD",
"LLM_AUTH_METHOD_ENV",
"LLMAuthMethod",
"OAUTH_AUTH_METHOD",
"OAUTH_BACKEND_PROVIDER_BY_PROVIDER",
"OAUTH_PROVIDER_BY_BACKEND_PROVIDER",
"canonical_llm_provider",
"effective_llm_provider",
"get_configured_llm_auth_method",
"normalize_llm_auth_method",
"supports_oauth_auth_method",
]
+480
View File
@@ -0,0 +1,480 @@
"""Prompt-safe LLM auth status and request-scoped credential resolution."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Literal
from config.llm_auth.provider_catalog import (
API_KEY_PROVIDER_ENVS,
ProviderSpec,
provider_spec,
require_provider_spec,
)
from config.llm_auth.records import (
delete_provider_auth_record,
resolve_provider_auth_record,
save_provider_auth_record,
save_provider_auth_record_values,
)
CredentialSource = Literal[
"env",
"keyring",
"metadata",
"cli",
"ambient",
"local",
"none",
"unknown",
]
class MissingLLMCredentialError(RuntimeError):
"""Raised when a selected provider lacks request-time credentials."""
@dataclass(frozen=True)
class CredentialStatus:
"""Prompt-safe status for a provider auth path."""
provider: str
configured: bool
source: CredentialSource
verified: bool
stale: bool
detail: str
@dataclass(frozen=True)
class CredentialResolution:
"""Request-time credential resolution for one provider."""
provider: str
api_key: str
source: CredentialSource
detail: str
@property
def ok(self) -> bool:
return bool(self.api_key) or self.source in {"cli", "ambient", "local"}
def __repr__(self) -> str:
redacted = "<set>" if self.api_key else "<empty>"
return (
"CredentialResolution("
f"provider={self.provider!r}, api_key={redacted}, "
f"source={self.source!r}, detail={self.detail!r})"
)
def _bool_record_value(record: dict[str, str], key: str, default: bool) -> bool:
raw = record.get(key)
if raw is None:
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
def _normalize_source(raw: str | None, *, fallback: CredentialSource) -> CredentialSource:
value = (raw or "").strip().lower()
allowed = {"env", "keyring", "metadata", "cli", "ambient", "local", "none", "unknown"}
return value if value in allowed else fallback # type: ignore[return-value]
def _env_value(env_var: str) -> str:
return os.getenv(env_var, "").strip()
def _source_status(provider: str, source: CredentialSource, detail: str) -> CredentialStatus:
return CredentialStatus(
provider=provider,
configured=source not in {"none", "unknown"},
source=source,
verified=source not in {"metadata", "unknown"},
stale=False,
detail=detail,
)
def _record_status(spec: ProviderSpec, record: dict[str, str]) -> CredentialStatus:
source = _normalize_source(record.get("source"), fallback="metadata")
stale = _bool_record_value(record, "stale", False)
verified = _bool_record_value(record, "verified", not stale)
detail = record.get("detail") or (
f"{spec.api_key_env} was previously saved; run `opensre auth verify {spec.value}` "
"to confirm it is still available."
)
return CredentialStatus(
provider=spec.value,
configured=True,
source=source if source != "keyring" else "metadata",
verified=verified and not stale,
stale=stale,
detail=detail,
)
def status(provider: str) -> CredentialStatus:
"""Return prompt-safe provider auth status.
This function must not read Keychain secrets. It may inspect environment,
non-secret metadata, CLI adapter probes, and ambient/local config markers.
"""
spec = provider_spec(provider)
if spec is None:
return CredentialStatus(
provider=provider.strip().lower(),
configured=False,
source="unknown",
verified=False,
stale=False,
detail=f"Unsupported LLM provider: {provider}",
)
if spec.credential_kind == "api_key":
if spec.api_key_env and _env_value(spec.api_key_env):
return _source_status(
spec.value,
"env",
f"{spec.api_key_env} is set in the environment.",
)
record = resolve_provider_auth_record(spec.value)
if record:
return _record_status(spec, record)
return CredentialStatus(
provider=spec.value,
configured=False,
source="none",
verified=False,
stale=False,
detail=f"{spec.api_key_env} is not configured.",
)
if spec.credential_kind == "cli":
record = resolve_provider_auth_record(spec.value)
record_source = _normalize_source(record.get("source"), fallback="metadata")
stale = _bool_record_value(record, "stale", False)
verified = _bool_record_value(record, "verified", False)
if record and verified and not stale:
return CredentialStatus(
provider=spec.value,
configured=True,
source="cli" if record_source in {"metadata", "unknown", "none"} else record_source,
verified=True,
stale=False,
detail=record.get("detail") or f"{spec.label} auth metadata is present.",
)
try:
from integrations.llm_cli.registry import get_cli_provider_registration
reg = get_cli_provider_registration(spec.value)
if reg is None:
return CredentialStatus(
spec.value,
False,
"none",
False,
False,
"No CLI adapter registered.",
)
probe = reg.adapter_factory().detect()
except Exception as exc:
return CredentialStatus(
spec.value,
False,
"unknown",
False,
False,
f"CLI auth status could not be checked: {exc}",
)
configured = probe.installed and probe.logged_in is True
source: CredentialSource = "cli" if configured else "none"
return CredentialStatus(
provider=spec.value,
configured=configured,
source=source,
verified=configured,
stale=False,
detail=probe.detail,
)
if spec.credential_kind == "ambient":
region = os.getenv("AWS_REGION", "").strip() or os.getenv("AWS_DEFAULT_REGION", "").strip()
return CredentialStatus(
provider=spec.value,
configured=bool(region),
source="ambient" if region else "none",
verified=bool(region),
stale=False,
detail=(
f"AWS region is configured ({region}); Bedrock uses the AWS credential chain."
if region
else "AWS_REGION or AWS_DEFAULT_REGION is not set."
),
)
if spec.credential_kind == "local":
host = os.getenv("OLLAMA_HOST", "").strip() or "http://localhost:11434"
return CredentialStatus(
provider=spec.value,
configured=True,
source="local",
verified=True,
stale=False,
detail=f"Ollama host: {host}.",
)
return CredentialStatus(spec.value, False, "unknown", False, False, "Unknown auth kind.")
def _mark_stale(spec: ProviderSpec, detail: str) -> None:
record = resolve_provider_auth_record(spec.value)
if not record:
return
save_provider_auth_record_values(
spec.value,
{
**record,
"source": record.get("source") or "metadata",
"detail": detail,
"verified": "false",
"stale": "true",
},
)
def resolve_for_request(provider: str) -> CredentialResolution:
"""Resolve request-time auth for exactly one selected provider."""
spec = require_provider_spec(provider)
if spec.credential_kind == "api_key":
env_value = _env_value(spec.api_key_env)
if env_value:
return CredentialResolution(
provider=spec.value,
api_key=env_value,
source="env",
detail=f"{spec.api_key_env} resolved from environment.",
)
import keyring.errors
from config.llm_keyring import keyring_is_disabled, read_keychain_secret
if not keyring_is_disabled():
try:
key = read_keychain_secret(spec.api_key_env)
except keyring.errors.KeyringError as exc:
# The backend itself couldn't be reached (e.g. no D-Bus/Secret
# Service session in this process) — this is not evidence the
# credential is missing, so leave any previously-verified
# metadata untouched instead of marking it stale.
return CredentialResolution(
provider=spec.value,
api_key="",
source="unknown",
detail=(
f"Could not reach the system keychain to check {spec.api_key_env}: "
f"{exc}. Retry once the keychain is reachable."
),
)
if key:
save_provider_auth_record(
provider=spec.value,
auth_name=spec.value,
kind="api_key",
source="keyring",
detail=f"{spec.api_key_env} stored in the system keychain.",
verified=True,
stale=False,
env_var=spec.api_key_env,
)
return CredentialResolution(
provider=spec.value,
api_key=key,
source="keyring",
detail=f"{spec.api_key_env} resolved from secure local storage.",
)
detail = (
f"Missing credential for LLM provider '{spec.value}'. Set {spec.api_key_env} "
f"or run `opensre auth login {spec.value}`."
)
_mark_stale(spec, detail)
return CredentialResolution(spec.value, "", "none", detail)
if spec.credential_kind == "cli":
return CredentialResolution(
provider=spec.value,
api_key="",
source="cli",
detail=f"{spec.label} uses vendor CLI authentication.",
)
if spec.credential_kind == "ambient":
return CredentialResolution(
provider=spec.value,
api_key="",
source="ambient",
detail=f"{spec.label} uses ambient credentials.",
)
if spec.credential_kind == "local":
return CredentialResolution(
provider=spec.value,
api_key="",
source="local",
detail=f"{spec.label} uses local runtime configuration.",
)
return CredentialResolution(spec.value, "", "unknown", "Unsupported provider auth kind.")
def require_for_request(provider: str) -> CredentialResolution:
"""Resolve request-time auth or raise an actionable error."""
resolution = resolve_for_request(provider)
if not resolution.ok:
raise MissingLLMCredentialError(resolution.detail)
return resolution
def resolve_api_key_env_for_request(env_var: str) -> str:
"""Resolve one API-key env var through the request-time provider boundary."""
normalized = env_var.strip()
for provider, provider_env in API_KEY_PROVIDER_ENVS.items():
if provider_env == normalized:
return resolve_for_request(provider).api_key
from config.llm_keyring import resolve_llm_api_key
return resolve_llm_api_key(normalized)
def save_api_key(provider: str, value: str, *, detail: str | None = None) -> None:
"""Store an OpenSRE-managed API key and refresh prompt-safe metadata."""
spec = require_provider_spec(provider)
if not spec.uses_open_sre_api_key:
raise ValueError(f"{spec.value} does not use an OpenSRE-managed API key")
from config.llm_keyring import save_llm_api_key
save_llm_api_key(spec.api_key_env, value)
save_provider_auth_record(
provider=spec.value,
auth_name=spec.value,
kind="api_key",
source="keyring",
detail=detail or f"{spec.api_key_env} stored in the system keychain.",
verified=True,
stale=False,
env_var=spec.api_key_env,
)
def delete(provider: str) -> None:
"""Delete OpenSRE-managed provider auth metadata and API key when applicable."""
spec = require_provider_spec(provider)
if spec.uses_open_sre_api_key:
from config.llm_keyring import delete_llm_api_key
delete_llm_api_key(spec.api_key_env)
delete_provider_auth_record(spec.value)
def verify(provider: str) -> CredentialStatus:
"""Intentionally check request-time credentials and update metadata."""
resolution = resolve_for_request(provider)
if resolution.ok:
return CredentialStatus(
provider=resolution.provider,
configured=True,
source=resolution.source,
verified=True,
stale=False,
detail=resolution.detail,
)
return status(provider)
def source_for_api_key_env(env_var: str) -> CredentialSource:
"""Prompt-safe source lookup for legacy env-var-based callers."""
normalized = env_var.strip()
if _env_value(normalized):
return "env"
for provider, provider_env in API_KEY_PROVIDER_ENVS.items():
if provider_env == normalized:
provider_status = status(provider)
return provider_status.source if provider_status.configured else "none"
return "none"
def has_api_key_env_status(env_var: str) -> bool:
"""Prompt-safe availability check for legacy env-var-based callers."""
return source_for_api_key_env(env_var) != "none"
def llm_api_key_source(env_var: str) -> str:
"""Return where an LLM credential resolves from: ``env``, ``keyring``, or ``none``."""
import keyring
import keyring.errors
from config.llm_keyring import (
_KEYRING_SERVICE,
keyring_is_disabled,
macos_keychain_item_exists,
)
prompt_safe_source = source_for_api_key_env(env_var)
if prompt_safe_source != "none":
return prompt_safe_source
if env_var.strip() in set(API_KEY_PROVIDER_ENVS.values()):
return "none"
if os.getenv(env_var, "").strip():
return "env"
if keyring_is_disabled():
return "none"
item_exists = macos_keychain_item_exists(env_var)
if item_exists is not None:
return "keyring" if item_exists else "none"
try:
if (keyring.get_password(_KEYRING_SERVICE, env_var) or "").strip():
return "keyring"
except keyring.errors.KeyringError:
return "none"
return "none"
def has_llm_api_key(env_var: str) -> bool:
"""Return True when an API key is available from env or secure local storage."""
from config.llm_keyring import (
keyring_is_disabled,
macos_keychain_item_exists,
resolve_llm_api_key,
)
if has_api_key_env_status(env_var):
return True
if env_var.strip() in set(API_KEY_PROVIDER_ENVS.values()):
return False
if os.getenv(env_var, "").strip():
return True
if keyring_is_disabled():
return False
item_exists = macos_keychain_item_exists(env_var)
if item_exists is not None:
return item_exists
return bool(resolve_llm_api_key(env_var))
__all__ = [
"CredentialResolution",
"CredentialSource",
"CredentialStatus",
"MissingLLMCredentialError",
"delete",
"has_api_key_env_status",
"has_llm_api_key",
"llm_api_key_source",
"require_for_request",
"resolve_api_key_env_for_request",
"resolve_for_request",
"save_api_key",
"source_for_api_key_env",
"status",
"verify",
]
+266
View File
@@ -0,0 +1,266 @@
"""Canonical LLM provider auth and model-selection metadata."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
CredentialKind = Literal["api_key", "cli", "ambient", "local"]
@dataclass(frozen=True)
class ProviderSpec:
"""Provider facts shared by config, auth, wizard, and runtime checks."""
value: str
label: str
credential_kind: CredentialKind
api_key_env: str = ""
model_env: str = ""
legacy_model_env: str | None = None
toolcall_model_env: str | None = None
classification_model_env: str | None = None
cli_model_env: str | None = None
endpoint_env: str = ""
api_version_env: str = ""
allow_custom_models: bool = False
@property
def uses_open_sre_api_key(self) -> bool:
return self.credential_kind == "api_key" and bool(self.api_key_env)
PROVIDER_SPECS: tuple[ProviderSpec, ...] = (
ProviderSpec(
value="anthropic",
label="Anthropic API key",
credential_kind="api_key",
api_key_env="ANTHROPIC_API_KEY",
model_env="ANTHROPIC_REASONING_MODEL",
legacy_model_env="ANTHROPIC_MODEL",
toolcall_model_env="ANTHROPIC_TOOLCALL_MODEL",
classification_model_env="ANTHROPIC_CLASSIFICATION_MODEL",
),
ProviderSpec(
value="openai",
label="OpenAI API key",
credential_kind="api_key",
api_key_env="OPENAI_API_KEY",
model_env="OPENAI_REASONING_MODEL",
legacy_model_env="OPENAI_MODEL",
toolcall_model_env="OPENAI_TOOLCALL_MODEL",
classification_model_env="OPENAI_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="openrouter",
label="OpenRouter",
credential_kind="api_key",
api_key_env="OPENROUTER_API_KEY",
model_env="OPENROUTER_REASONING_MODEL",
legacy_model_env="OPENROUTER_MODEL",
toolcall_model_env="OPENROUTER_TOOLCALL_MODEL",
classification_model_env="OPENROUTER_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="deepseek",
label="DeepSeek",
credential_kind="api_key",
api_key_env="DEEPSEEK_API_KEY",
model_env="DEEPSEEK_REASONING_MODEL",
legacy_model_env="DEEPSEEK_MODEL",
toolcall_model_env="DEEPSEEK_TOOLCALL_MODEL",
classification_model_env="DEEPSEEK_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="gemini",
label="Google Gemini API key",
credential_kind="api_key",
api_key_env="GEMINI_API_KEY",
model_env="GEMINI_REASONING_MODEL",
legacy_model_env="GEMINI_MODEL",
toolcall_model_env="GEMINI_TOOLCALL_MODEL",
classification_model_env="GEMINI_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="nvidia",
label="NVIDIA NIM",
credential_kind="api_key",
api_key_env="NVIDIA_API_KEY",
model_env="NVIDIA_REASONING_MODEL",
legacy_model_env="NVIDIA_MODEL",
toolcall_model_env="NVIDIA_TOOLCALL_MODEL",
classification_model_env="NVIDIA_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="minimax",
label="MiniMax",
credential_kind="api_key",
api_key_env="MINIMAX_API_KEY",
model_env="MINIMAX_REASONING_MODEL",
legacy_model_env="MINIMAX_MODEL",
toolcall_model_env="MINIMAX_TOOLCALL_MODEL",
classification_model_env="MINIMAX_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="groq",
label="Groq API key",
credential_kind="api_key",
api_key_env="GROQ_API_KEY",
model_env="GROQ_REASONING_MODEL",
legacy_model_env="GROQ_MODEL",
toolcall_model_env="GROQ_TOOLCALL_MODEL",
classification_model_env="GROQ_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="azure-openai",
label="Azure OpenAI",
credential_kind="api_key",
api_key_env="AZURE_OPENAI_API_KEY",
model_env="AZURE_OPENAI_REASONING_MODEL",
legacy_model_env="AZURE_OPENAI_MODEL",
toolcall_model_env="AZURE_OPENAI_TOOLCALL_MODEL",
classification_model_env="AZURE_OPENAI_CLASSIFICATION_MODEL",
endpoint_env="AZURE_OPENAI_BASE_URL",
api_version_env="AZURE_OPENAI_API_VERSION",
allow_custom_models=True,
),
ProviderSpec(
value="bedrock",
label="Amazon Bedrock (IAM auth)",
credential_kind="ambient",
model_env="BEDROCK_REASONING_MODEL",
toolcall_model_env="BEDROCK_TOOLCALL_MODEL",
classification_model_env="BEDROCK_CLASSIFICATION_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="ollama",
label="Ollama (local)",
credential_kind="local",
api_key_env="OLLAMA_HOST",
model_env="OLLAMA_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="codex",
label="OpenAI Codex CLI",
credential_kind="cli",
model_env="CODEX_MODEL",
cli_model_env="CODEX_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="cursor",
label="Cursor Agent CLI",
credential_kind="cli",
model_env="CURSOR_MODEL",
cli_model_env="CURSOR_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="claude-code",
label="Anthropic Claude Code CLI",
credential_kind="cli",
model_env="CLAUDE_CODE_MODEL",
cli_model_env="CLAUDE_CODE_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="gemini-cli",
label="Google Gemini CLI",
credential_kind="cli",
model_env="GEMINI_CLI_MODEL",
cli_model_env="GEMINI_CLI_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="antigravity-cli",
label="Google Antigravity CLI",
credential_kind="cli",
model_env="ANTIGRAVITY_CLI_MODEL",
cli_model_env="ANTIGRAVITY_CLI_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="opencode",
label="OpenCode CLI",
credential_kind="cli",
model_env="OPENCODE_MODEL",
cli_model_env="OPENCODE_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="kimi",
label="Kimi Code CLI",
credential_kind="cli",
model_env="KIMI_MODEL",
cli_model_env="KIMI_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="copilot",
label="GitHub Copilot CLI",
credential_kind="cli",
model_env="COPILOT_MODEL",
cli_model_env="COPILOT_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="grok-cli",
label="xAI Grok Build CLI",
credential_kind="cli",
model_env="GROK_CLI_MODEL",
cli_model_env="GROK_CLI_MODEL",
allow_custom_models=True,
),
ProviderSpec(
value="pi",
label="Pi CLI (pi.dev, BYOK multi-provider)",
credential_kind="cli",
model_env="PI_MODEL",
cli_model_env="PI_MODEL",
allow_custom_models=True,
),
)
PROVIDER_BY_VALUE: dict[str, ProviderSpec] = {spec.value: spec for spec in PROVIDER_SPECS}
SUPPORTED_PROVIDER_VALUES: tuple[str, ...] = tuple(spec.value for spec in PROVIDER_SPECS)
API_KEY_PROVIDER_ENVS: dict[str, str] = {
spec.value: spec.api_key_env for spec in PROVIDER_SPECS if spec.uses_open_sre_api_key
}
KEYLESS_PROVIDER_VALUES: frozenset[str] = frozenset(
spec.value for spec in PROVIDER_SPECS if not spec.uses_open_sre_api_key
)
def provider_spec(provider: str) -> ProviderSpec | None:
"""Return the provider spec for *provider*, if supported."""
return PROVIDER_BY_VALUE.get(provider.strip().lower())
def require_provider_spec(provider: str) -> ProviderSpec:
"""Return the provider spec or raise ``KeyError`` for unsupported providers."""
spec = provider_spec(provider)
if spec is None:
raise KeyError(provider)
return spec
__all__ = [
"API_KEY_PROVIDER_ENVS",
"CredentialKind",
"KEYLESS_PROVIDER_VALUES",
"PROVIDER_BY_VALUE",
"PROVIDER_SPECS",
"ProviderSpec",
"SUPPORTED_PROVIDER_VALUES",
"provider_spec",
"require_provider_spec",
]
+199
View File
@@ -0,0 +1,199 @@
"""Metadata-file-backed LLM provider auth records.
The records in this file are intentionally non-secret. They exist so status
commands can report local auth state without touching Keychain secrets.
"""
from __future__ import annotations
import json
import os
import tempfile
from collections.abc import Mapping
from contextlib import suppress
from datetime import UTC, datetime
from pathlib import Path
from filelock import FileLock
from config.constants import OPENSRE_HOME_DIR
_VERSION = 1
_AUTH_RECORD_PREFIX = "provider-auth:"
_LOCK_TIMEOUT_SECONDS = 10.0
def _auth_metadata_path() -> Path:
override = os.getenv("OPENSRE_LLM_AUTH_METADATA_PATH", "").strip()
if override:
return Path(override).expanduser()
return OPENSRE_HOME_DIR / "llm-auth.json"
def _lock_path(path: Path) -> Path:
return path.with_suffix(path.suffix + ".lock")
def _utc_now() -> str:
return datetime.now(UTC).replace(microsecond=0).isoformat()
def _ensure_parent(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
with suppress(OSError):
path.parent.chmod(0o700)
def _empty_store() -> dict[str, object]:
return {"version": _VERSION, "providers": {}}
def _load_unlocked(path: Path) -> dict[str, object]:
if not path.exists():
return _empty_store()
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return _empty_store()
if not isinstance(data, dict):
return _empty_store()
providers = data.get("providers")
if not isinstance(providers, dict):
providers = {}
return {"version": _VERSION, "providers": providers}
def _write_unlocked(path: Path, data: Mapping[str, object]) -> None:
_ensure_parent(path)
fd, tmp_name = tempfile.mkstemp(
prefix=f".{path.name}.",
suffix=".tmp",
dir=str(path.parent),
text=True,
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, sort_keys=True)
fh.write("\n")
os.chmod(tmp_name, 0o600)
os.replace(tmp_name, path)
with suppress(OSError):
path.chmod(0o600)
finally:
if os.path.exists(tmp_name):
os.unlink(tmp_name)
def provider_auth_record_name(provider: str) -> str:
"""Return the stable metadata record name for one LLM provider."""
normalized = provider.strip().lower()
if not normalized:
raise ValueError("provider must not be empty")
return f"{_AUTH_RECORD_PREFIX}{normalized}"
def _normalize_record(values: Mapping[str, object]) -> dict[str, str]:
return {
str(key).strip(): str(value).strip()
for key, value in values.items()
if str(key).strip() and str(value).strip()
}
def save_provider_auth_record(
*,
provider: str,
auth_name: str,
kind: str,
source: str,
detail: str,
verified: bool = True,
stale: bool = False,
env_var: str = "",
) -> None:
"""Persist non-token auth metadata for a provider."""
provider_value = provider.strip().lower()
path = _auth_metadata_path()
_ensure_parent(path)
with FileLock(str(_lock_path(path)), timeout=_LOCK_TIMEOUT_SECONDS):
data = _load_unlocked(path)
providers = data.setdefault("providers", {})
if not isinstance(providers, dict):
providers = {}
data["providers"] = providers
providers[provider_value] = _normalize_record(
{
"provider": provider_value,
"auth_name": auth_name,
"kind": kind,
"source": source,
"detail": detail,
"env_var": env_var,
"verified": str(bool(verified)).lower(),
"stale": str(bool(stale)).lower(),
"updated_at": _utc_now(),
}
)
_write_unlocked(path, data)
def save_provider_auth_record_values(provider: str, values: Mapping[str, object]) -> None:
"""Persist an already-shaped non-secret provider auth record."""
provider_value = provider.strip().lower()
path = _auth_metadata_path()
_ensure_parent(path)
with FileLock(str(_lock_path(path)), timeout=_LOCK_TIMEOUT_SECONDS):
data = _load_unlocked(path)
providers = data.setdefault("providers", {})
if not isinstance(providers, dict):
providers = {}
data["providers"] = providers
record = _normalize_record({"provider": provider_value, **dict(values)})
record.setdefault("updated_at", _utc_now())
providers[provider_value] = record
_write_unlocked(path, data)
def resolve_provider_auth_record(provider: str) -> dict[str, str]:
"""Resolve non-token auth metadata for a provider without reading secrets."""
provider_value = provider.strip().lower()
path = _auth_metadata_path()
if not path.exists():
return {}
with FileLock(str(_lock_path(path)), timeout=_LOCK_TIMEOUT_SECONDS):
data = _load_unlocked(path)
providers = data.get("providers")
if not isinstance(providers, dict):
return {}
record = providers.get(provider_value)
if not isinstance(record, dict):
return {}
return {
str(key): str(value)
for key, value in record.items()
if isinstance(key, str) and isinstance(value, str)
}
def delete_provider_auth_record(provider: str) -> None:
"""Delete provider auth metadata."""
provider_value = provider.strip().lower()
path = _auth_metadata_path()
if not path.exists():
return
_ensure_parent(path)
with FileLock(str(_lock_path(path)), timeout=_LOCK_TIMEOUT_SECONDS):
data = _load_unlocked(path)
providers = data.get("providers")
if isinstance(providers, dict):
providers.pop(provider_value, None)
_write_unlocked(path, data)
__all__ = [
"delete_provider_auth_record",
"provider_auth_record_name",
"resolve_provider_auth_record",
"save_provider_auth_record",
"save_provider_auth_record_values",
]

Some files were not shown because too many files have changed in this diff Show More