Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ 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, 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)
settings/ # Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 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 (api, 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
tools/ # Tool registry, built-in tools (file_system/, git, sandbox/, code_runner), git clone SSRF prevention (git_url_validator), MCP bridge (mcp/), role-based access, approval tool (request_human_approval)
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. `ConfigResolver` provides typed composed reads for API controllers (assembles full Pydantic config models from individually resolved settings, using `asyncio.TaskGroup` for parallel resolution)
- **Settings**: Runtime-editable configuration via DB-backed settings persistence (9 namespaces: api, 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
21 changes: 21 additions & 0 deletions src/synthorg/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,27 @@ async def _safe_shutdown(
)


# ── 2-Phase Initialisation ────────────────────────────────────────
#
# Phase 1 (construct): Litestar bakes middleware, CORS, and routes
# into the app at construction time — these read directly from
# RootConfig and are immutable after construction. Bootstrap-only
# settings (server_host, server_port, api_prefix, cors_allowed_origins,
# rate_limit_exclude_paths, auth_exclude_paths) are therefore NOT
# resolved through SettingsService.
#
# Phase 2 (on_startup): After persistence connects and migrations
# run, SettingsService + ConfigResolver become available. Runtime-
# editable settings (rate_limit_max_requests, rate_limit_time_unit,
# jwt_expiry_minutes, min_password_length) are resolved through
# ConfigResolver.get_api_config() by consumers that need current
# values post-startup.
Comment on lines +484 to +489
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The auth settings called “runtime-editable” never reach the active AuthService.

This note says jwt_expiry_minutes and min_password_length resolve post-startup, but _init_persistence() still constructs AuthService directly from app_state.config.api.auth. With the current flow, pre-existing DB overrides for those fields are ignored unless the auth service is built from ConfigResolver.get_api_config() (or otherwise refreshed from settings after startup).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/app.py` around lines 484 - 489, The AuthService is being
constructed in _init_persistence() using static app_state.config.api.auth so
runtime-editable settings like jwt_expiry_minutes and min_password_length from
SettingsService/ConfigResolver never take effect; modify the startup flow so
AuthService is created (or re-initialized) using the resolved runtime config
from ConfigResolver.get_api_config() after persistence/migrations complete
(Phase 2), or add a post-startup refresh step that calls
ConfigResolver.get_api_config() and updates/replaces the AuthService instance
with values from that result; ensure references to app_state.config.api.auth are
replaced or synchronized with the ConfigResolver.get_api_config() output so
AuthService uses the active runtime settings.

#
# Note: Litestar's rate-limit middleware reads max_requests and
# time_unit at construction; runtime DB changes are visible only
# to code calling get_api_config(), not to the middleware itself.


def create_app( # noqa: PLR0913
*,
config: RootConfig | None = None,
Expand Down
2 changes: 2 additions & 0 deletions src/synthorg/settings/definitions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

from synthorg.settings.definitions import (
api,
backup,
budget,
company,
Expand All @@ -16,6 +17,7 @@
)

__all__ = [
"api",
"backup",
"budget",
"company",
Expand Down
159 changes: 159 additions & 0 deletions src/synthorg/settings/definitions/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""API namespace setting definitions.

Registers 10 settings covering server, CORS, rate limiting, and
authentication. Four are runtime-editable; six are bootstrap-only
(``restart_required=True``) because Litestar bakes middleware and
CORS into the application at construction time.
"""

from synthorg.settings.enums import SettingLevel, SettingNamespace, SettingType
from synthorg.settings.models import SettingDefinition
from synthorg.settings.registry import get_registry

_r = get_registry()

# ── Server (bootstrap-only) ──────────────────────────────────────

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="server_host",
type=SettingType.STRING,
default="127.0.0.1",
description="Server bind address",
group="Server",
restart_required=True,
yaml_path="api.server.host",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="server_port",
type=SettingType.INTEGER,
default="8000",
description="Server bind port",
group="Server",
restart_required=True,
min_value=1,
max_value=65535,
yaml_path="api.server.port",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="api_prefix",
type=SettingType.STRING,
default="/api/v1",
description="URL prefix for all API routes",
group="Server",
level=SettingLevel.ADVANCED,
restart_required=True,
yaml_path="api.api_prefix",
)
Comment on lines +45 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These bootstrap keys are registered in the settings DB chain, but the current startup path never reads them from settings.

create_app() still builds the router, CORS config, and auth/rate-limit middleware from RootConfig.api, and the new note in src/synthorg/api/app.py explicitly says api_prefix, cors_allowed_origins, rate_limit_exclude_paths, and auth_exclude_paths are not resolved through SettingsService. With the current flow, DB edits to these keys will show up in the Settings UI but still won’t affect the next app start unless you add a pre-construction bridge from the registry back into startup config.

Also applies to: 61-71, 103-114, 147-158

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/settings/definitions/api.py` around lines 45 - 56, The
registered API settings (SettingDefinition entries under SettingNamespace.API
such as "api_prefix", "cors_allowed_origins", "rate_limit_exclude_paths", and
"auth_exclude_paths") are never read during startup because create_app() still
constructs router/CORS/auth/rate-limit from RootConfig.api; update the startup
flow to load these values from the SettingsService before building the app.
Concretely: add a pre-construction step in create_app() (or its caller) to query
SettingsService for the keys registered in api settings and merge/override
RootConfig.api (or pass a resolved config object) so that functions that build
the router, CORS config, and middleware consume the resolved values; ensure the
code path that builds routes in src/synthorg/api/app.py uses the resolved
settings rather than RootConfig.api to honor DB changes to api_prefix,
cors_allowed_origins, rate_limit_exclude_paths, and auth_exclude_paths.

)

# ── CORS (bootstrap-only) ────────────────────────────────────────

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="cors_allowed_origins",
type=SettingType.JSON,
default='["http://localhost:5173"]',
description="Origins permitted to make cross-origin requests",
group="CORS",
restart_required=True,
yaml_path="api.cors.allowed_origins",
)
Comment on lines +62 to +71
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This setting is missing the yaml_path property. Without it, this setting cannot be configured from the main YAML file and will fall back to an environment variable or the hardcoded default. This seems like an oversight for an important security configuration.

    SettingDefinition(
        namespace=SettingNamespace.API,
        key="cors_allowed_origins",
        type=SettingType.JSON,
        default='["http://localhost:5173"]',
        description="Origins permitted to make cross-origin requests",
        group="CORS",
        restart_required=True,
        yaml_path="api.cors.allowed_origins",
    )

)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Rate Limiting (exclude_paths: bootstrap-only) ────────────────

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="rate_limit_max_requests",
type=SettingType.INTEGER,
default="100",
description="Maximum requests per time window",
group="Rate Limiting",
min_value=1,
max_value=10000,
yaml_path="api.rate_limit.max_requests",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="rate_limit_time_unit",
type=SettingType.ENUM,
default="minute",
description="Rate limit time window",
group="Rate Limiting",
enum_values=("second", "minute", "hour", "day"),
yaml_path="api.rate_limit.time_unit",
)
Comment on lines +76 to +100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These two settings are still restart-only in practice.

The new note in src/synthorg/api/app.py says Litestar captures max_requests and time_unit at construction and never rereads ConfigResolver.get_api_config(). So, even after the broader bootstrap-resolution gap is fixed, these definitions need restart_required=True unless the middleware is rebuilt on change.

🔧 Minimal metadata fix
 _r.register(
     SettingDefinition(
         namespace=SettingNamespace.API,
         key="rate_limit_max_requests",
         type=SettingType.INTEGER,
         default="100",
         description="Maximum requests per time window",
         group="Rate Limiting",
+        restart_required=True,
         min_value=1,
         max_value=10000,
         yaml_path="api.rate_limit.max_requests",
     )
 )
@@
 _r.register(
     SettingDefinition(
         namespace=SettingNamespace.API,
         key="rate_limit_time_unit",
         type=SettingType.ENUM,
         default="minute",
         description="Rate limit time window",
         group="Rate Limiting",
+        restart_required=True,
         enum_values=("second", "minute", "hour", "day"),
         yaml_path="api.rate_limit.time_unit",
     )
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/settings/definitions/api.py` around lines 76 - 100, The two
SettingDefinition entries for keys "rate_limit_max_requests" and
"rate_limit_time_unit" must be marked restart-only because Litestar captures
max_requests/time_unit at app construction and won't re-read ConfigResolver;
update the SettingDefinition for namespace SettingNamespace.API with key
"rate_limit_max_requests" and the one with key "rate_limit_time_unit" to include
restart_required=True so changes require an application restart (i.e., add
restart_required=True to those SettingDefinition constructors).

)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="rate_limit_exclude_paths",
type=SettingType.JSON,
default='["/api/v1/health"]',
description="Paths excluded from rate limiting",
group="Rate Limiting",
level=SettingLevel.ADVANCED,
restart_required=True,
yaml_path="api.rate_limit.exclude_paths",
)
Comment on lines +104 to +114
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This setting is missing the yaml_path property. Without it, this setting cannot be configured from the main YAML file and will fall back to an environment variable or the hardcoded default.

    SettingDefinition(
        namespace=SettingNamespace.API,
        key="rate_limit_exclude_paths",
        type=SettingType.JSON,
        default='["/api/v1/health"]',
        description="Paths excluded from rate limiting",
        group="Rate Limiting",
        level=SettingLevel.ADVANCED,
        restart_required=True,
        yaml_path="api.rate_limit.exclude_paths",
    )

)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# ── Authentication (exclude_paths: bootstrap-only) ───────────────

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="jwt_expiry_minutes",
type=SettingType.INTEGER,
default="1440",
description="JWT token lifetime in minutes",
group="Authentication",
min_value=1,
max_value=10080,
yaml_path="api.auth.jwt_expiry_minutes",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="min_password_length",
type=SettingType.INTEGER,
default="12",
description="Minimum password length for setup and password change",
group="Authentication",
min_value=12,
max_value=128,
yaml_path="api.auth.min_password_length",
)
)

_r.register(
SettingDefinition(
namespace=SettingNamespace.API,
key="auth_exclude_paths",
type=SettingType.JSON,
default="[]",
description="Paths excluded from authentication middleware",
group="Authentication",
level=SettingLevel.ADVANCED,
restart_required=True,
yaml_path="api.auth.exclude_paths",
)
Comment on lines +148 to +158
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This setting is missing the yaml_path property. Without it, this setting cannot be configured from the main YAML file and will fall back to an environment variable or the hardcoded default. This seems like an oversight for an important security configuration.

    SettingDefinition(
        namespace=SettingNamespace.API,
        key="auth_exclude_paths",
        type=SettingType.JSON,
        default="[]",
        description="Paths excluded from authentication middleware",
        group="Authentication",
        level=SettingLevel.ADVANCED,
        restart_required=True,
        yaml_path="api.auth.exclude_paths",
    )

)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/synthorg/settings/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SettingNamespace(StrEnum):
can be edited at runtime via the settings API.
"""

API = "api"
COMPANY = "company"
PROVIDERS = "providers"
MEMORY = "memory"
Expand Down
66 changes: 66 additions & 0 deletions src/synthorg/settings/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from synthorg.settings.errors import SettingNotFoundError

if TYPE_CHECKING:
from synthorg.api.config import ApiConfig
from synthorg.budget.config import BudgetAlertConfig, BudgetConfig
from synthorg.config.schema import RootConfig
from synthorg.core.enums import AutonomyLevel
Expand Down Expand Up @@ -328,6 +329,71 @@ async def get_budget_config(self) -> BudgetConfig:
},
)

async def get_api_config(self) -> ApiConfig:
"""Assemble an ``ApiConfig`` with runtime-editable overrides.

Resolves the four runtime-editable API settings (rate-limit
max requests, rate-limit time unit, JWT expiry, min password
length) and merges them onto the YAML-loaded base config.

Bootstrap-only settings (``server_host``, ``server_port``,
``api_prefix``, ``cors_allowed_origins``,
``rate_limit_exclude_paths``, ``auth_exclude_paths``) are
**not** resolved — they are baked into the Litestar app at
construction and require a restart to take effect.

Uses ``asyncio.TaskGroup`` to resolve all settings in parallel.

Returns:
An ``ApiConfig`` with DB/env overrides applied to the
runtime-editable fields.

Raises:
SettingNotFoundError: If a required API setting is
missing from the registry.
ValueError: If a resolved value cannot be parsed.
"""
from synthorg.api.config import RateLimitTimeUnit # noqa: PLC0415

base = self._config.api

try:
async with asyncio.TaskGroup() as tg:
t_max_req = tg.create_task(
self.get_int("api", "rate_limit_max_requests")
)
t_time_unit = tg.create_task(
self.get_enum("api", "rate_limit_time_unit", RateLimitTimeUnit)
)
t_jwt_exp = tg.create_task(self.get_int("api", "jwt_expiry_minutes"))
t_min_pw = tg.create_task(self.get_int("api", "min_password_length"))
except ExceptionGroup as eg:
logger.warning(
SETTINGS_FETCH_FAILED,
namespace="api",
key="_composed",
error_count=len(eg.exceptions),
exc_info=True,
)
raise eg.exceptions[0] from eg

return base.model_copy(
update={
"rate_limit": base.rate_limit.model_copy(
update={
"max_requests": t_max_req.result(),
"time_unit": t_time_unit.result(),
},
),
"auth": base.auth.model_copy(
update={
"jwt_expiry_minutes": t_jwt_exp.result(),
"min_password_length": t_min_pw.result(),
},
),
},
)

async def get_coordination_config(
self,
*,
Expand Down
Empty file.
Loading
Loading