mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Fix structured settings environment projection
Canonicalize structured model settings before publishing the process environment so UI-shaped JSON objects remain readable by runtime consumers. Add seam and endpoint regression coverage for disk and environment projections. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
526b0563d7
commit
f9dd323737
3 changed files with 60 additions and 1 deletions
|
|
@ -952,6 +952,21 @@ def get_mcp_tool_timeout_sec() -> int:
|
|||
return parsed if parsed > 0 else int(SETTINGS_DEFAULTS["MCP_TOOL_TIMEOUT_SEC"])
|
||||
|
||||
|
||||
def _serialize_env_setting(key: str, value: object) -> str:
|
||||
"""Project the incoming settings shape without leaking Python repr syntax.
|
||||
|
||||
Settings writers accept structured values from the UI before the persistence
|
||||
prologue canonicalizes them for disk. The process projection must carry the
|
||||
same JSON-shaped value, otherwise ``str(dict)`` produces single-quoted text
|
||||
that strict runtime readers cannot parse.
|
||||
"""
|
||||
if key in (MODEL_ACCOUNTS_KEY, MODEL_CONTEXT_WINDOWS_KEY):
|
||||
return normalize_model_role_options(key, value)[1]
|
||||
if isinstance(value, (dict, list)):
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
return str(value)
|
||||
|
||||
|
||||
def apply_settings_to_env(settings: dict, *, environ=None) -> None:
|
||||
"""Push settings into environment variables for supervisor modules."""
|
||||
with _settings_integrity.SETTINGS_ENV_LOCK:
|
||||
|
|
@ -974,7 +989,7 @@ def apply_settings_to_env(settings: dict, *, environ=None) -> None:
|
|||
if val is None or val == "":
|
||||
environ.pop(k, None)
|
||||
else:
|
||||
environ[k] = str(val)
|
||||
environ[k] = _serialize_env_setting(k, val)
|
||||
# Reviewer-model floors moved into the structured-slot projection (6.1):
|
||||
from ouroboros.reviewer_slot_config import project_reviewer_slots_into_env
|
||||
project_reviewer_slots_into_env(environ=environ)
|
||||
|
|
|
|||
|
|
@ -348,6 +348,28 @@ def test_generic_settings_save_validates_and_canonicalizes_available_subagents(
|
|||
assert '"name"' not in canonical
|
||||
|
||||
|
||||
def test_generic_settings_save_projects_model_role_objects_as_json(
|
||||
monkeypatch, isolated_settings,
|
||||
):
|
||||
from ouroboros import config as cfg
|
||||
from ouroboros.gateway import settings as settings_mod
|
||||
|
||||
app = _settings_app(monkeypatch, isolated_settings)
|
||||
monkeypatch.setattr(settings_mod, "_apply_settings_to_env", cfg.apply_settings_to_env)
|
||||
response = TestClient(app).post("/api/settings", json={
|
||||
"OUROBOROS_MODEL_ACCOUNTS": {"main": "", "fallback": ["work", ""]},
|
||||
"OUROBOROS_MODEL_CONTEXT_WINDOWS": {"main": 131072, "fallback": [0, 65536]},
|
||||
})
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
stored = json.loads(isolated_settings.read_text(encoding="utf-8"))
|
||||
assert json.loads(stored["OUROBOROS_MODEL_ACCOUNTS"]) == {
|
||||
"fallback": ["work", ""], "main": "",
|
||||
}
|
||||
assert json.loads(os.environ["OUROBOROS_MODEL_ACCOUNTS"])["main"] == ""
|
||||
assert json.loads(os.environ["OUROBOROS_MODEL_CONTEXT_WINDOWS"])["main"] == 131072
|
||||
|
||||
|
||||
def test_generic_settings_save_rejects_malformed_available_subagents_without_write(
|
||||
monkeypatch, isolated_settings,
|
||||
):
|
||||
|
|
|
|||
|
|
@ -150,6 +150,28 @@ def test_a_disk_authored_key_is_not_projected_back_out_of_a_silent_file(
|
|||
assert os.environ["OUROBOROS_CONTEXT_MODE"] == "max"
|
||||
|
||||
|
||||
def test_structured_model_settings_keep_json_shape_in_environment(isolated_settings):
|
||||
"""UI-shaped model role objects stay parseable after process projection."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
projected = {}
|
||||
cfg.apply_settings_to_env({
|
||||
"OUROBOROS_MODEL_ACCOUNTS": {"main": "", "fallback": ["work", ""]},
|
||||
"OUROBOROS_MODEL_CONTEXT_WINDOWS": {"main": 131072, "fallback": [0, 65536]},
|
||||
}, environ=projected)
|
||||
|
||||
assert json.loads(projected["OUROBOROS_MODEL_ACCOUNTS"]) == {
|
||||
"fallback": ["work", ""], "main": "",
|
||||
}
|
||||
assert json.loads(projected["OUROBOROS_MODEL_CONTEXT_WINDOWS"]) == {
|
||||
"fallback": [0, 65536], "main": 131072,
|
||||
}
|
||||
parsed, _ = cfg.normalize_model_role_options(
|
||||
cfg.MODEL_ACCOUNTS_KEY, projected["OUROBOROS_MODEL_ACCOUNTS"])
|
||||
assert parsed["fallback"] == ["work", ""]
|
||||
assert "'main'" not in projected["OUROBOROS_MODEL_ACCOUNTS"]
|
||||
|
||||
|
||||
def test_install_time_facts_are_disk_only_in_both_directions(isolated_settings, monkeypatch):
|
||||
"""`ENDPOINT_AUTHORED_SETTINGS` is stricter than the ratchets: those project
|
||||
once the file carries them, these never leave disk at all. An environment
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue