feat(config): directory bootstrap and config manager

- config/schema.py: Pydantic v2 models — no API keys, only provider_id/model/base_url
- config/manager.py: ruamel.yaml round-trip load/save, chmod 600 enforced on write
- config/dirs.py: bootstrap() creates ~/.pyra/ tree, vault sentinel checked every startup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-17 12:49:22 +02:00
parent a96b540234
commit ae565b0d68
3 changed files with 124 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
from pathlib import Path
from pyra.security.boundaries import PyraSecurityError, check_vault_lock
from pyra.utils.paths import ensure_dir, pyra_home, safe_chmod
_MEMORY_INDEX_TEMPLATE = """\
# Memory Index
| File | Category | Last Modified |
|------|----------|---------------|
_Auto-maintained by Pyra. Do not edit manually._
"""
_USER_PROFILE_TEMPLATE = """\
# User Profile
_Add information about yourself here. Pyra will use this context in every session._
## Preferences
## Background
## Goals
"""
def bootstrap() -> None:
"""Create ~/.pyra/ tree and verify vault sentinel. Called at every startup."""
home = pyra_home()
ensure_dir(home, 0o700)
ensure_dir(home / "memory" / "user", 0o700)
ensure_dir(home / "memory" / "context", 0o700)
ensure_dir(home / "memory" / "knowledge", 0o700)
ensure_dir(home / "skills" / "bash", 0o700)
ensure_dir(home / "skills" / "powershell", 0o700)
ensure_dir(home / "skills" / "python", 0o700)
ensure_dir(home / "vault" / "secrets", 0o700)
_create_vault_lock(home / "vault" / ".vault_lock")
check_vault_lock()
_create_if_missing(home / "memory" / "MEMORY_INDEX.md", _MEMORY_INDEX_TEMPLATE, 0o600)
_create_if_missing(home / "memory" / "user" / "profile.md", _USER_PROFILE_TEMPLATE, 0o600)
config = home / "config.yaml"
if config.exists():
safe_chmod(config, 0o600)
def _create_vault_lock(lock_path: Path) -> None:
if not lock_path.exists():
lock_path.touch(mode=0o400)
safe_chmod(lock_path, 0o400)
def _create_if_missing(path: Path, content: str, mode: int) -> None:
if not path.exists():
path.write_text(content)
safe_chmod(path, mode)
+39
View File
@@ -0,0 +1,39 @@
import os
from pathlib import Path
from ruamel.yaml import YAML
from pyra.config.schema import PyraConfig
from pyra.utils.paths import pyra_home, safe_chmod
_yaml = YAML()
_yaml.default_flow_style = False
_yaml.width = 80
_CONFIG_PATH = pyra_home() / "config.yaml"
def config_path() -> Path:
return _CONFIG_PATH
def load_config() -> PyraConfig:
if not _CONFIG_PATH.exists():
raise FileNotFoundError(
"Pyra is not configured. Run 'pyra setup' to get started."
)
with _CONFIG_PATH.open("r") as fh:
data = _yaml.load(fh)
return PyraConfig.model_validate(data)
def save_config(cfg: PyraConfig) -> None:
_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
data = cfg.model_dump()
with _CONFIG_PATH.open("w") as fh:
_yaml.dump(data, fh)
safe_chmod(_CONFIG_PATH, 0o600)
def config_exists() -> bool:
return _CONFIG_PATH.exists()
+24
View File
@@ -0,0 +1,24 @@
from pydantic import BaseModel, Field
class ProviderConfig(BaseModel):
provider_id: str
model: str
base_url: str | None = None
class MemoryConfig(BaseModel):
max_tokens_in_context: int = 4000
auto_load: bool = True
class SecurityConfig(BaseModel):
injection_detection: bool = True
log_injections: bool = True
class PyraConfig(BaseModel):
version: int = 1
ai: ProviderConfig
memory: MemoryConfig = Field(default_factory=MemoryConfig)
security: SecurityConfig = Field(default_factory=SecurityConfig)