A safe bridge between AI assistants and your computer
Project description
Solveig
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?
Solveig requires your approval for every file read, write, or command execution. It uses Shellcheck to validate commands and catch dangerous patterns. However, you're ultimately responsible for reviewing and denying any request you don't understand.
This is still a tool that connects an AI to your computer - always review what it wants to do.
๐ญ 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:
- Hook plugins: Use
@before()or@after()decorators to validate or process existing requirements (file/command operations) - Requirement plugins: Create new operation types that the LLM can request - things like database queries, API calls, directory trees, or specialized file operations
- Plugin tests: Add comprehensive test suites in
tests/plugins/test_my_plugin.py
Adding a hook plugin:
- Create a file in
solveig/plugins/hooks/my_plugin.py - Use decorators:
@before(requirements=(CommandRequirement,)),@after(), both, or neither - Add tests in
tests/plugins/test_my_plugin.pyfollowing the existing patterns - Plugins auto-load when Solveig starts - no configuration needed!
Adding a requirement plugin:
- Create a new requirement class in
solveig/schema/requirements/my_requirement.py - Extend the base
Requirementclass and implement_actually_solve()method - Add the new requirement type to
solveig/schema/requirements/__init__.py - Create corresponding result class in
solveig/schema/results/my_result.py - Update the LLM system prompt examples to show the new capability
- 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.config import SolveigConfig
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: SolveigConfig, requirement: CommandRequirement):
"""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 solveig.config import SolveigConfig
from solveig.plugins.hooks import after
from solveig.plugins.exceptions import ProcessingError
from solveig.schema.requirements import ReadRequirement, WriteRequirement
from solveig.schema.results import ReadResult, WriteResult
@after(requirements=(ReadRequirement, WriteRequirement))
def anonymize_paths(config: SolveigConfig, requirement: ReadRequirement|WriteRequirement, result: ReadResult|WriteResult):
"""Anonymize file paths in results before sending to LLM."""
try:
original_path = 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'] = anonymous_path
Create a new requirement type: Directory tree listing
# solveig/schema/requirements/tree.py
from pathlib import Path
from typing import TYPE_CHECKING
from .base import Requirement, validate_non_empty_path
if TYPE_CHECKING:
from solveig.interface import SolveigInterface
from solveig.schema.results import TreeResult
class TreeRequirement(Requirement):
"""Generate a directory tree listing showing file structure."""
path: str = Field(..., validator=validate_non_empty_path)
max_depth: int = Field(default=3, ge=1, le=10)
show_hidden: bool = Field(default=False)
def _actually_solve(self, config, interface: "SolveigInterface") -> "TreeResult":
from solveig.schema.results import TreeResult
abs_path = Path(self.path).expanduser().resolve()
# Generate tree structure
tree_lines = self._generate_tree(abs_path, self.max_depth, self.show_hidden)
return TreeResult(
requirement=self,
accepted=True,
path=abs_path,
tree_output="\n".join(tree_lines),
total_files=len([line for line in tree_lines if "๐" in line]),
total_dirs=len([line for line in tree_lines if "๐" in line])
)
def _generate_tree(self, path: Path, max_depth: int, show_hidden: bool) -> list[str]:
"""Generate tree structure lines."""
lines = [f"๐ {path.name}/"]
def _walk_dir(current_path: Path, prefix: str, depth: int):
if depth >= max_depth:
return
try:
entries = list(current_path.iterdir())
if not show_hidden:
entries = [e for e in entries if not e.name.startswith('.')]
entries.sort(key=lambda x: (x.is_file(), x.name.lower()))
for i, entry in enumerate(entries):
is_last = i == len(entries) - 1
current_prefix = "โโโ " if is_last else "โโโ "
next_prefix = prefix + (" " if is_last else "โ ")
if entry.is_dir():
lines.append(f"{prefix}{current_prefix}๐ {entry.name}/")
_walk_dir(entry, next_prefix, depth + 1)
else:
lines.append(f"{prefix}{current_prefix}๐ {entry.name}")
except PermissionError:
lines.append(f"{prefix}โโโ โ Permission denied")
_walk_dir(path, "", 0)
return lines
# solveig/schema/results/tree.py
from pathlib import Path
from .base import RequirementResult
class TreeResult(RequirementResult):
path: str | Path
tree_output: str
total_files: int = 0
total_dirs: int = 0
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:
- Formatting:
black .- Ensures consistent code style - Linting:
ruff check .- Catches potential bugs and code quality issues - Type checking:
mypy solveig/ scripts/ --ignore-missing-imports- Validates type hints - 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
Unit Tests (tests/unit/):
- Mock all I/O and side-effect operations (file system, user interface, external commands)
- Use minimal mocking: Mock only the lowest-level behaviors while keeping high-level logic unmocked
- Mock framework provides utility methods for easy test setup (mock filesystem with directory structure)
- Test real object serialization and business logic with actual Pydantic models
- Config tests use
cli_argsto 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 to avoid interactive prompts
- Test full stack from requirement parsing to actual file operations
- Marked with
@pytest.mark.no_file_mockingto bypass file system mocks
Mocking Strategy:
- File operations: Mock
solveig.utils.filemethods for filesystem interactions - User interface: Mock input/output methods while preserving all display logic and formatting
- External commands: Mock subprocess calls and command execution
- Real objects: Never mock requirement/result objects - test actual Pydantic serialization
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
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file solveig-0.2.13.tar.gz.
File metadata
- Download URL: solveig-0.2.13.tar.gz
- Upload date:
- Size: 87.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d5f06c4f9ea75eeba8795443a850dddbb65c5103efb397b37a4358989852891
|
|
| MD5 |
03d499e7006c0ee18d0676aa3f5ebb50
|
|
| BLAKE2b-256 |
601c7ec8392e067b46e37d330a5610f83a3c800c01e37561dbd6f8d447bd6873
|
Provenance
The following attestation bundles were made for solveig-0.2.13.tar.gz:
Publisher:
cd.yml on FranciscoSilveira/solveig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solveig-0.2.13.tar.gz -
Subject digest:
0d5f06c4f9ea75eeba8795443a850dddbb65c5103efb397b37a4358989852891 - Sigstore transparency entry: 442134508
- Sigstore integration time:
-
Permalink:
FranciscoSilveira/solveig@1cf24bb2b94b9b3aa0546488a47205d59d07eb75 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/FranciscoSilveira
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@1cf24bb2b94b9b3aa0546488a47205d59d07eb75 -
Trigger Event:
push
-
Statement type:
File details
Details for the file solveig-0.2.13-py3-none-any.whl.
File metadata
- Download URL: solveig-0.2.13-py3-none-any.whl
- Upload date:
- Size: 84.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
593e4a48d6a0ff46a34db6c8aba04c827c244ede74a46eec4877c320b744f738
|
|
| MD5 |
4c5bf176efdbb6a36fdce73c99120851
|
|
| BLAKE2b-256 |
d8a1396fe64473e1bd6c810738e9e8dced48d330f24e92c407d931b8037a7ea0
|
Provenance
The following attestation bundles were made for solveig-0.2.13-py3-none-any.whl:
Publisher:
cd.yml on FranciscoSilveira/solveig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
solveig-0.2.13-py3-none-any.whl -
Subject digest:
593e4a48d6a0ff46a34db6c8aba04c827c244ede74a46eec4877c320b744f738 - Sigstore transparency entry: 442134530
- Sigstore integration time:
-
Permalink:
FranciscoSilveira/solveig@1cf24bb2b94b9b3aa0546488a47205d59d07eb75 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/FranciscoSilveira
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@1cf24bb2b94b9b3aa0546488a47205d59d07eb75 -
Trigger Event:
push
-
Statement type: