Skip to content
Merged
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ src/synthorg/
persistence/ # Operational data persistence — pluggable PersistenceBackend protocol, SQLite initial, SettingsRepository (namespaced settings CRUD) (see Memory & Persistence design page)
observability/ # Structured logging, correlation tracking, log sinks
providers/ # LLM provider abstraction (LiteLLM adapter)
settings/ # Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (8 namespaces), Fernet encryption for sensitive values, config bridge, validation, registry, change notifications via message bus
settings/ # Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (8 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus
definitions/ # Per-namespace setting definitions (company, providers, memory, budget, security, coordination, observability, backup)
security/ # SecOps agent, rule engine (soft-allow/hard-deny, fail-closed), audit log, output scanner, output scan response policies (redact/withhold/log-only/autonomy-tiered), risk classifier, risk tier classifier, action type registry, ToolInvoker security integration, progressive trust (4 strategies: disabled/weighted/per-category/milestone), autonomy levels (presets, resolver, change strategy), timeout policies (park/resume)
templates/ # Pre-built company templates, personality presets, and builder
Expand Down
2 changes: 1 addition & 1 deletion docs/design/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ and retry guidance.
- **Budget Panel**: Spending charts, per-agent breakdown (projections/alerts planned)
- **Meeting Logs**: Placeholder — coming soon
- **Artifact Browser**: Placeholder — coming soon
- **Settings**: Runtime-editable configuration via DB-backed settings persistence (8 namespaces: company, providers, memory, budget, security, coordination, observability, backup). 4-layer resolution (DB > env > YAML > code defaults), Fernet encryption for sensitive values, REST API (GET/PUT/DELETE + schema endpoints for dynamic UI generation), change notifications via message bus
- **Settings**: Runtime-editable configuration via DB-backed settings persistence (8 namespaces: company, providers, memory, budget, security, coordination, observability, backup). 4-layer resolution (DB > env > YAML > code defaults), Fernet encryption for sensitive values, REST API (GET/PUT/DELETE + schema endpoints for dynamic UI generation), change notifications via message bus. `ConfigResolver` provides typed composed reads for API controllers (assembles full Pydantic config models from individually resolved settings, using `asyncio.TaskGroup` for parallel resolution)

### Human Roles

Expand Down
6 changes: 2 additions & 4 deletions src/synthorg/api/controllers/autonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ async def get_autonomy(
Current autonomy level info.
"""
app_state: AppState = state.app_state
config = app_state.config.config
level = config.autonomy.level
level = await app_state.config_resolver.get_autonomy_level()
return ApiResponse(
data=AutonomyLevelResponse(
agent_id=agent_id,
Expand Down Expand Up @@ -103,8 +102,7 @@ async def update_autonomy(
Updated autonomy level info.
"""
app_state: AppState = state.app_state
config = app_state.config.config
current_level = config.autonomy.level
current_level = await app_state.config_resolver.get_autonomy_level()
requested_level = data.level

logger.info(
Expand Down
3 changes: 2 additions & 1 deletion src/synthorg/api/controllers/budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ async def get_budget_config(
Budget config envelope.
"""
app_state: AppState = state.app_state
return ApiResponse(data=app_state.config.budget)
budget = await app_state.config_resolver.get_budget_config()
return ApiResponse(data=budget)

@get("/records")
async def list_cost_records(
Expand Down
5 changes: 4 additions & 1 deletion src/synthorg/api/controllers/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ async def get_company(
Company configuration envelope.
"""
app_state: AppState = state.app_state
company_name = await app_state.config_resolver.get_str(
"company", "company_name"
)
config = app_state.config
data: dict[str, Any] = {
"company_name": config.company_name,
"company_name": company_name,
"agents": [a.model_dump(mode="json") for a in config.agents],
"departments": [d.model_dump(mode="json") for d in config.departments],
}
Expand Down
6 changes: 3 additions & 3 deletions src/synthorg/api/controllers/coordination.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ async def coordinate_task(
_validate_task_id(task_id)
task = await self._get_task(app_state, task_id)
agents = await self._resolve_agents(app_state, data, task_id)
context = self._build_context(app_state, task, agents, data)
context = await self._build_context(app_state, task, agents, data)

_publish_ws_event(
request,
Expand Down Expand Up @@ -196,7 +196,7 @@ async def _get_task(
raise NotFoundError(msg)
return task

def _build_context(
async def _build_context(
self,
app_state: AppState,
task: Task,
Expand All @@ -208,7 +208,7 @@ def _build_context(
DecompositionContext,
)

coord_config = app_state.config.coordination.to_coordination_config(
coord_config = await app_state.config_resolver.get_coordination_config(
max_concurrency_per_wave=data.max_concurrency_per_wave,
fail_fast=data.fail_fast,
)
Expand Down
8 changes: 6 additions & 2 deletions src/synthorg/api/controllers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from litestar import Controller, delete, get, put
from litestar.datastructures import State # noqa: TC002
from litestar.exceptions import ClientException, NotFoundException
from litestar.exceptions import (
ClientException,
InternalServerException,
NotFoundException,
)
from pydantic import BaseModel, ConfigDict, Field

from synthorg.api.dto import ApiResponse
Expand Down Expand Up @@ -187,7 +191,7 @@ async def update_setting(
key=key,
)
msg = "Internal error processing sensitive setting"
raise ClientException(msg, status_code=500) from None
raise InternalServerException(msg) from None
return ApiResponse(data=entry)

@delete(
Expand Down
17 changes: 17 additions & 0 deletions src/synthorg/api/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from synthorg.observability import get_logger
from synthorg.observability.events.api import API_APP_STARTUP, API_SERVICE_UNAVAILABLE
from synthorg.persistence.protocol import PersistenceBackend # noqa: TC001
from synthorg.settings.resolver import ConfigResolver
from synthorg.settings.service import SettingsService # noqa: TC001

logger = get_logger(__name__)
Expand Down Expand Up @@ -52,6 +53,7 @@ class AppState:
"_agent_registry",
"_approval_gate",
"_auth_service",
"_config_resolver",
"_coordinator",
"_cost_tracker",
"_meeting_orchestrator",
Expand Down Expand Up @@ -100,6 +102,11 @@ def __init__( # noqa: PLR0913
self._meeting_orchestrator = meeting_orchestrator
self._meeting_scheduler = meeting_scheduler
self._settings_service = settings_service
self._config_resolver: ConfigResolver | None = (
ConfigResolver(settings_service=settings_service, config=config)
if settings_service is not None
else None
)
self._ticket_store = WsTicketStore()
self.startup_time = startup_time

Expand Down Expand Up @@ -236,6 +243,16 @@ def has_settings_service(self) -> bool:
"""Check whether the settings service is configured."""
return self._settings_service is not None

@property
def has_config_resolver(self) -> bool:
"""Check whether the config resolver is configured."""
return self._config_resolver is not None

@property
def config_resolver(self) -> ConfigResolver:
"""Return the cached config resolver or raise 503."""
return self._require_service(self._config_resolver, "config_resolver")

@property
def has_auth_service(self) -> bool:
"""Check whether the auth service is already configured."""
Expand Down
2 changes: 2 additions & 0 deletions src/synthorg/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
)
from synthorg.settings.models import SettingDefinition, SettingEntry, SettingValue
from synthorg.settings.registry import SettingsRegistry, get_registry
from synthorg.settings.resolver import ConfigResolver

__all__ = [
"ConfigResolver",
"SettingDefinition",
"SettingEntry",
"SettingLevel",
Expand Down
60 changes: 60 additions & 0 deletions src/synthorg/settings/definitions/budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,63 @@
yaml_path="budget.auto_downgrade.threshold",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.BUDGET,
key="reset_day",
type=SettingType.INTEGER,
default="1",
description="Day of month when budget resets (1-28)",
group="Limits",
level=SettingLevel.ADVANCED,
min_value=1,
max_value=28,
yaml_path="budget.reset_day",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.BUDGET,
key="alert_warn_at",
type=SettingType.INTEGER,
default="75",
description="Budget usage percent that triggers a warning alert",
group="Alerts",
level=SettingLevel.ADVANCED,
min_value=0,
max_value=100,
yaml_path="budget.alerts.warn_at",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.BUDGET,
key="alert_critical_at",
type=SettingType.INTEGER,
default="90",
description="Budget usage percent that triggers a critical alert",
group="Alerts",
level=SettingLevel.ADVANCED,
min_value=0,
max_value=100,
yaml_path="budget.alerts.critical_at",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.BUDGET,
key="alert_hard_stop_at",
type=SettingType.INTEGER,
default="100",
description="Budget usage percent that triggers a hard stop",
group="Alerts",
level=SettingLevel.ADVANCED,
min_value=0,
max_value=100,
yaml_path="budget.alerts.hard_stop_at",
)
)
10 changes: 5 additions & 5 deletions src/synthorg/settings/definitions/company.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@
namespace=SettingNamespace.COMPANY,
key="autonomy_level",
type=SettingType.ENUM,
default="supervised",
default="semi",
description="Default company-wide autonomy level",
group="General",
enum_values=(
"full_autonomy",
"full",
"semi",
"supervised",
"approval_required",
"human_in_the_loop",
"locked",
),
yaml_path="config.autonomy_level",
yaml_path="config.autonomy.level",
)
)

Expand Down
42 changes: 39 additions & 3 deletions src/synthorg/settings/definitions/coordination.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,57 @@
"decentralized",
"context_dependent",
),
yaml_path="coordination.default_topology",
yaml_path="coordination.topology",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.COORDINATION,
key="max_wave_size",
key="max_concurrency_per_wave",
type=SettingType.INTEGER,
default="5",
description="Maximum number of agents in a single execution wave",
group="General",
level=SettingLevel.ADVANCED,
min_value=1,
max_value=50,
yaml_path="coordination.max_wave_size",
yaml_path="coordination.max_concurrency_per_wave",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.COORDINATION,
key="fail_fast",
type=SettingType.BOOLEAN,
default="false",
description="Stop on first wave failure instead of continuing",
group="General",
yaml_path="coordination.fail_fast",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.COORDINATION,
key="enable_workspace_isolation",
type=SettingType.BOOLEAN,
default="true",
description="Create isolated workspaces for multi-agent execution",
group="General",
yaml_path="coordination.enable_workspace_isolation",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.COORDINATION,
key="base_branch",
type=SettingType.STRING,
default="main",
description="Git branch for workspace isolation",
group="General",
yaml_path="coordination.base_branch",
)
)
Loading
Loading