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