Skip to main content

A safe bridge between AI assistants and your computer

Project description

Solveig

PyPI CI codecov Python 3.12+ License: GPL v3 Code style: black

demo

A safe bridge between AI assistants and your computer.

Solveig transforms any LLM into a practical assistant that can read files and run commandsโ€”with your explicit approval for every operation. No more copying and pasting between your terminal and ChatGPT.

๐Ÿ”’ Safe โ€ข Comprehensive test suite โ€ข Secure file API โ€ข Command validation
๐Ÿš€ Useful โ€ข Works with any OpenAI-compatible API โ€ข Handles real tasks efficiently
๐Ÿงฉ Extensible โ€ข Drop-in plugin system โ€ข Easy to customize and extend


๐Ÿš€ Quick start

# Install from source:
git clone https://github.com/FranciscoSilveira/solveig.git
cd solveig
pip install -e .

# Or install from PyPI:
pip install solveig

# Run a local model:
solveig -u "http://localhost:5001/v1" "Tell me a joke"

# Run from a remote API like OpenRouter:
solveig -u "https://openrouter.ai/api/v1" -k "<API_KEY>" -m "moonshotai/kimi-k2:free" "Summarize my day"


โ“ FAQ

What is Solveig?

A terminal AI helper that can request file access and run commands with your explicit approval.

Is Solveig an LLM like ChatGPT?

No, it's a safe interface between LLM services and your computer. It can use ChatGPT, Claude, or any OpenAI-compatible service.

Why use Solveig instead of a plain LLM?

LLMs can only work with what you manually provide. If ChatGPT needs a file or command output, you have to copy-paste it yourself. Solveig lets the LLM request exactly what it needs, and you just approve or deny each request.

Why use Solveig over other LLMโ€‘toโ€‘shell assistants?

Solveig focuses on preventing dangerous operations through explicit user consent and validation. It prefers direct file access over arbitrary commands when possible, and validates commands with Shellcheck before execution.

Is Solveig safe to run?

Mostly:

  • Solveig is unable to read file contents, run commands or send back anything unless you give it explicit consent
  • Interacts with files and tools through requirements, enforcing user control and allowing easy validation
  • Validates shell commands before they're even requested through the included shellcheck plugin (requires installing CLI tool shellcheck)
  • Open-source project, proper CI with 200+ test suite with 90%+ coverage and extensive scenario focus

This is still a tool that connects an AI to your terminal - always review what it wants to do.

How does Solveig work?

Solveig creates a conversation with an LLM using the initial prompt and establishes a loop where the LLM asks for requirements and you choose whether to execute them and send back their results.

Most AI CLI assistants rely only on running Bash obtained from a model, which can be a shaky foundation for a security product. Instead, Solveig focuses on providing a safe interface for most behavior that bypasses shell commands, allowing for proper inspection and interface displaying. Basically, it's much easier to validate a read/write requirement for a file than validating a cat command to read or an echo pipe to write the same file.

All core filesystem operations are covered by requirements, and you can extend this by adding new requirement plugins or interacting with requirements through hook plugins.

Why are there 2 kinds of plugins?

You can extend Solveig in any of 2 ways:

  • By adding a new requirement, representing a new thing the LLM can request
  • By adding a hook that captures the requirement before or after it's been processed

Requirements follow a simple interface with 3 methods and return a corresponding Result class:

from typing import Literal

from solveig.interface import SolveigInterface
from solveig.schema.requirements.base import Requirement
from solveig.schema.results.base import RequirementResult
from solveig.plugins.schema import register_requirement


class MyResult(RequirementResult):
    """Example requirement result."""
    response: str | None = None


@register_requirement
class MyRequirement(Requirement):
    """Example requirement."""
    title: Literal["myreq"] = "myreq"

    def create_error_result(self, error_message: str, accepted: bool) -> MyResult:
        """Create a result with an error."""
        return MyResult(
            requirement=self,
            accepted=accepted,
            error=error_message,
        )

    @classmethod
    def get_description(cls) -> str:
        """Return requirement description, using this format: name(args): description."""
        return (
            "myreq(name): description of what this requirement does"
        )

    def actually_solve(self, config, interface: SolveigInterface) -> MyResult:
        """Solve the requirement and return the result."""
        user_response = interface.ask_user("What to send back")
        return MyResult(
            requirement=self,
            accepted=False,
            response=user_response
        )


# Fix possible forward typing references
MyResult.model_rebuild()

Hooks meanwhile latch onto existing (or new) requirements, either before or after they run

You can add a requirement by itself, a hook for a new or existing requirement, or a combination of both. Some examples:

  • MySQL plugin
    • SQLRequirement: runs a query on database
  • Web Search
    • SearchRequirement: performs a HTTP request the URL
    • @before hook: validates the URL against a blacklist/whitelist before running request
  • Anonymize Paths
    • @after hook: privacy-focused plugin that converts expanded paths to fake ones (/home/user-name -> home/jdoe) in results

๐ŸŽญ Examples

These were obtained with Kimi K2 on OpenRouter.

click to expand:

Do any of the files in ~/Sync look dangerous?
โ”€โ”€โ”€ User โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Reply:
 > Do any of the files in ~/Sync look dangerous?

โ ‡ Waiting... (Ctrl+C to stop)

โ”€โ”€โ”€ Assistant โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ  I'll check the contents of your ~/Sync directory to identify any potentially dangerous files. Let me start by reading the directory structure and then examine the files within.
[ Requirements (1) ]
  [ Read (1) ]
    โ  First, let me read the ~/Sync directory to see what files are present
    ๐Ÿ—  ~/Sync  (/home/francisco/Sync)

โ”€โ”€โ”€ User โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
[ Results (1) ]
  [ Read ]
    โ  First, let me read the ~/Sync directory to see what files are present
    ๐Ÿ—  ~/Sync  (/home/francisco/Sync)
    โ”Œโ”€โ”€โ”€ Metadata โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ ๐Ÿ— /home/francisco/Sync | owner_name=francisco | group_name=francisco | path=/home/francisco/Sync | size=4096 | modified_time=Wed ... โ”‚
    โ”‚ โ”œโ”€๐Ÿ— d1                                                                                                                               โ”‚
    โ”‚ โ”œโ”€๐Ÿ—Ž dev.sh                                                                                                                           โ”‚
    โ”‚ โ”œโ”€๐Ÿ—Ž f1                                                                                                                               โ”‚
    โ”‚ โ”œโ”€๐Ÿ—Ž hello.py                                                                                                                         โ”‚
    โ”‚ โ”œโ”€๐Ÿ—Ž j1.json                                                                                                                          โ”‚
    โ”‚ โ”œโ”€๐Ÿ—Ž run.sh.bak                                                                                                                       โ”‚
    โ”‚ โ””โ”€๐Ÿ—Ž stuff.txt                                                                                                                        โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    ?  Allow sending metadata? [y/N]: y
  
?  >  

โ  Waiting... (Ctrl+C to stop)

โ”€โ”€โ”€ Assistant โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ  I can see several files in your ~/Sync directory. Let me examine the contents of the potentially executable files (shell scripts and Python files) to check if they contain any dangerous commands.
[ Requirements (3) ]
  [ Read (3) ]
    โ  Checking the dev.sh shell script for potentially dangerous commands
    ๐Ÿ—Ž  ~/Sync/dev.sh  (/home/francisco/Sync/dev.sh)
    โ  Checking the hello.py Python script for potentially dangerous code
    ๐Ÿ—Ž  ~/Sync/hello.py  (/home/francisco/Sync/hello.py)
    โ  Checking the run.sh.bak backup shell script for potentially dangerous commands
    ๐Ÿ—Ž  ~/Sync/run.sh.bak  (/home/francisco/Sync/run.sh.bak)


๐Ÿงฉ Plugins

Solveig has an extensible plugin system that automatically discovers and loads plugins on startup.

Plugin Types:

  1. Hook plugins: Use @before() or @after() decorators to validate or process existing requirements (file/command operations)
  2. Requirement plugins: Create new operation types that the LLM can request - things like database queries, API calls, directory trees, or specialized file operations
  3. Plugin tests: Add comprehensive test suites in tests/plugins/test_my_plugin.py

Adding a hook plugin:

  1. Create a file in solveig/plugins/hooks/my_plugin.py
  2. Use decorators: @before(requirements=(CommandRequirement,)), @after(), both, or neither
  3. Add tests in tests/plugins/test_my_plugin.py following the existing patterns
  4. Plugins auto-load when Solveig starts - no configuration needed!

Adding a requirement plugin:

  1. Create a new requirement class in solveig/schema/requirements/my_requirement.py
  2. Extend the base Requirement class and implement _actually_solve() method
  3. Add the new requirement type to solveig/schema/requirements/__init__.py
  4. Create corresponding result class in solveig/schema/results/my_result.py
  5. Update the LLM system prompt examples to show the new capability
  6. Add comprehensive tests for both success and failure cases

Check out solveig/plugins/hooks/shellcheck.py and tests/plugins/test_shellcheck.py for complete hook examples. The existing requirement types in solveig/schema/requirements/ show patterns for implementing new operations.

Examples:

click to expand:

Block dangerous commands with custom patterns
from solveig.plugins.hooks import before
from solveig.plugins.exceptions import SecurityError
from solveig.schema.requirements import CommandRequirement

@before(requirements=(CommandRequirement,))
def block_dangerous_commands(config, interface, requirement):
    """Block commands that could be dangerous to system security."""
    dangerous_patterns = [
        "sudo chmod 777",
        "wget http://",  # Block HTTP downloads
        "curl http://",
        "dd if=",        # Block disk operations
    ]
    
    for pattern in dangerous_patterns:
        if pattern in requirement.command:
            raise SecurityError(f"Blocked dangerous command pattern: {pattern}")
Anonymize all paths before sending to LLM
import re
from pathlib import Path
from solveig.plugins.hooks import after
from solveig.plugins.exceptions import ProcessingError
from solveig.schema.requirements import ReadRequirement, WriteRequirement

@after(requirements=(ReadRequirement, WriteRequirement))
def anonymize_paths(config, interface, requirement, result):
    """Anonymize file paths in results before sending to LLM."""
    try:
        original_path = str(result.metadata.path)
    except:
        return
    anonymous_path = re.sub(r"/home/\w+", "/home/jdoe", original_path)
    anonymous_path = re.sub(r"^([A-Z]:\\Users\\)[^\\]+", r"\1JohnDoe", anonymous_path, flags=re.IGNORECASE)
    result.metadata.path = Path(anonymous_path)
Create a new requirement type: Directory tree listing
# solveig/plugins/requirements/tree.py
from pathlib import Path
from pydantic import Field

from solveig.schema.requirements.base import Requirement, validate_non_empty_path
from solveig.schema.results.base import RequirementResult
from solveig.interface import SolveigInterface


class MyResult(RequirementResult):
    path: str | Path
    accepted: bool
    
class MyRequirement(Requirement):
    """Generate a directory tree listing showing file structure."""
    
    # Use descriptive fields, with a description string that the LLM will have access to,
    # as well as validation methods
    path: str = Field(..., description="The path to look from", validator=validate_non_empty_path)
    show_hidden: bool = Field(description="Look at hidden files and directories", default=False)
    
    def _actually_solve(self, config, interface: "SolveigInterface") -> "TreeResult":
        # Access the config, use the interface, solve the requirement and return a result
        try:
            if config.plugins.myplugin.is_active:
                return MyResult(
                    requirement=self,
                    accepted=interface.ask_yes_no("Proceed?"),
                    path=self.path
                )
        except Exception as e:
            return MyResult(
                requirement=self,
                accepted=False,
                path=self.path,
                error=str(e)
            )

Then update solveig/schema/requirements/__init__.py and solveig/schema/results/__init__.py to export the new classes, and add examples to the system prompt showing the LLM how to use TreeRequirement.


๐Ÿค Contributing

We use modern Python tooling to maintain code quality and consistency:

Development Tools

All code is automatically checked on main and develop branches:

  1. Formatting: black . - Ensures consistent code style
  2. Linting: ruff check . - Catches potential bugs and code quality issues
  3. Type checking: mypy solveig/ scripts/ --ignore-missing-imports - Validates type hints
  4. Testing: pytest - Runs full test suite with coverage reporting

Testing Philosophy

Solveig follows strict testing guidelines to ensure reliability and safety:

Test Coverage Requirements

  • Success and failure paths: Every feature must test both successful execution and error conditions
  • Mock only when necessary: Mock only low-level I/O behavior with potential side effects
  • No untested code paths: All business logic, error handling, and user interactions must be tested

Testing Architecture

Test Safety Philosophy: Unit tests must achieve high coverage while being completely safe to run. Our mocking approach ensures tests never touch real files, run real commands, or require user interaction.

Core Mocking Infrastructure:

  • **MockClient
  • MockFilesystem: Elaborate wrapper around @patch() calls that simulates complete file operations
  • MockInterface: Wrapper around @patch() calls for user input/output without actual terminal interaction
  • Plugin isolation: Tests call filter_hooks() with specific configs to ensure plugin state isolation
  • Automatic mocking: conftest.py automatically applies mocks via @pytest.fixture(autouse=True)

Unit Tests (tests/unit/):

  • Mock all I/O and side-effect operations (file system, user interface, external commands)
  • Tests like TestReadRequirement.test_successful_reads_with_mock_fs() prove mock isolation by creating files at paths like /test/readable.txt that don't exist on the real filesystem
  • Config tests use cli_args to bypass reading sys.argv and pass mock values without complex patching

Integration Tests (tests/integration/):

  • Allow real file I/O operations using temporary directories
  • Mock only user interactions and LLM responses to avoid interactive prompts
  • Test complete conversation flows with MockLLMClient (thin wrapper around @patch())

The apparent complexity serves a critical purpose: achieving 87%+ coverage while guaranteeing tests cannot damage your system or require manual intervention.

Mock Filesystem Safety Proof: Tests like TestReadRequirement.test_successful_reads_with_mock_fs() prove our mock filesystem works by creating files at paths like /test/readable.txt that don't exist on the real filesystem, then successfully reading them through the requirement system. The fact that these tests pass demonstrates that our mock filesystem is intercepting all file operations, ensuring no real files are touched during unit testing.

Running Tests

# Install with testing dependencies:
pip install -e .[dev]

# Unit tests only
python -m pytest tests/unit/ -v

# Integration tests only  
python -m pytest tests/integration/ -v

# Specific test class
python -m pytest tests/unit/test_main.py::TestInitializeConversation -v

# Run all checks locally (same as CI) 
black . && ruff check . && mypy solveig/ scripts/ --ignore-missing-imports && pytest ./tests/ --cov=solveig --cov=scripts --cov-report=term-missing -vv

# Running mock client (works with config)
python -m tests.mocks.run_with_mock_client

Test Organization

tests/
โ”œโ”€โ”€ unit/           # Unit tests
โ”œโ”€โ”€ integration/    # Integration tests
โ”œโ”€โ”€ mocks/          # Mock implementations
โ””โ”€โ”€ plugins/        # Plugin-specific tests

๐Ÿ“ˆ Roadmap

Next Steps:

  • Enhanced command validation with Semgrep static analysis
  • Second-opinion LLM validation for generated commands
  • Improve test coverage
  • API integration for Claude/Gemini

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

solveig-0.3.0.tar.gz (86.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

solveig-0.3.0-py3-none-any.whl (82.7 kB view details)

Uploaded Python 3

File details

Details for the file solveig-0.3.0.tar.gz.

File metadata

  • Download URL: solveig-0.3.0.tar.gz
  • Upload date:
  • Size: 86.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for solveig-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e5f890d4bdf4caedc17201400c450636d8f43ffe5d514b356d8125a7233413a9
MD5 7377f2a8660bf992b489b7f6e3a8b8c1
BLAKE2b-256 bc3272c050b7cb7dcecda490e2065331f884e94dea1194d58d37d720e42bbdf1

See more details on using hashes here.

Provenance

The following attestation bundles were made for solveig-0.3.0.tar.gz:

Publisher: cd.yml on FranciscoSilveira/solveig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file solveig-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: solveig-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 82.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for solveig-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 718e5d401033efe94e7a5bb0e7f82846682901569bfbf0214ce7892ffbbc76e9
MD5 0d105305fcb5d52761af57c15457d0fb
BLAKE2b-256 f0c960b4a5e6f294a44e3720d2798b930714d0ce3554bc6c37b973bdffd9223d

See more details on using hashes here.

Provenance

The following attestation bundles were made for solveig-0.3.0-py3-none-any.whl:

Publisher: cd.yml on FranciscoSilveira/solveig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page