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:
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user