mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-04 04:55:30 +00:00
v7next F1: domain D12 - settings vocabulary split from tip bytes, launcher seam half landed
Module side: config.py (1598) hands its vocabulary to the five oracle leaves, re-emitted from TIP bytes with the transplant proof green on every span: settings_defaults (361; SHARED leaf emitted FULL from BOTH parents per the ledger - config.py rows 840-854 + provider_models.py rows 3238-3241, final --check against the concatenated parents), settings_scales (111), model_slots (113), review_model_routes (131), runtime_limits (192); the facade shrinks 1598 -> 883 with the oracle's re-export import block. The drift-probe falsified the reference as a copy source on 7 spans (SETTINGS_DEFAULTS, RETIRED_SETTING_KEYS, ENDPOINT_AUTHORED_SETTINGS, OPENROUTER_REVIEW_DEFAULTS, get_websearch_timeout_sec, get_search_code_wall_sec, get_max_subagent_depth) plus one structural reshape: upstream's tuple statement also binds the unrowed MAX_SUBAGENT_DEPTH_HARD_CAP, which rides into runtime_limits and is re-exported (F5 row needed; the tool's undeclared-top-level gate has a Tuple-target blind spot - span proofs green, documented). provider_models.py is touched by span removal + the re-export import ONLY (D02 module). The reference's D04 retirement of the SOFT/HARD timeout knobs is upstream-diverged and NOT replayed. launcher_onboarding.py lands the launcher HALF of the approved D03 settings seam (rows 918-920): zero upstream drift since merge-base, reference bytes verbatim - startup stops persisting the pre-server provider normalization; the server-side mirror stays for the D11 lane. The config.py in-place seam rows 914/916 (normalize_settings_raw / serialize_settings) are HOT-DEFERRED: upstream rewrote the read path through the new settings_integrity module, so a verbatim replay would revert it; their pin test_settings_read_seam.py defers with the machinery. Test side: test_config_extraction.py + test_settings_env_on_disk.py transplanted (adaptations: the tuple-bound cap in the owner inventory, one literal re-pinned to tip's ENDPOINT_AUTHORED_SETTINGS, the provider_models clause narrowed to this tree - the no-config-import half returns with D02); the onboarding pins follow the launcher delta (row 920 rename, wizard and server_runtime launcher clauses; every server clause keeps tip bytes). Lossless: 19==19 / 63==63 / 28==28 test functions on the touched suites, one ledger-mandated rename. Two path-keyed mirrors updated (prompt-cache TTL definition-sites, hotreload key-paths set). CI-shape battery on the final tree: parallel 11807 tests 0 failed (3 skipped) rc=0; serial 618 tests 0 failed (9 skipped) rc=0; -m size_ratchet 5 passed (official manifest regeneration = byte no-op); ruff --select F clean; HEAD held through every pytest run. Ledger corrections: entries 13-19 in docs/v7next/LEDGER_CORRECTIONS.md. (cherry picked from commit 3a6a0344eabb9b933352c5fc32cabb0a79b186e5)
This commit is contained in:
parent
22c1473117
commit
a44815215f
16 changed files with 1557 additions and 876 deletions
|
|
@ -357,3 +357,85 @@ with evidence, found lane by lane. Applied to the campaign's carried ledger at F
|
|||
facade for now), and the reference's extra HOT_CODE_PATHS row for
|
||||
ouroboros/tools/extension_dispatch.py (nothing moved there on this tree —
|
||||
adding it is an oracle delta beyond relocation parity).
|
||||
## From the D12 lane (base d830cdba, 2026-08-30)
|
||||
13. Split rows 855-867 (settings_scales), 868-879 (model_slots), 880-886
|
||||
(review_model_routes) — RE-PROVEN against tip bytes: every span of the three
|
||||
reference leaves is ast=tokens=bytes=True against
|
||||
`git show HEAD:ouroboros/config.py` (drift-probe first, exit 0); the leaves
|
||||
landed from tip bytes and differ from the reference only in BETWEEN-SPAN
|
||||
comments upstream rewrote inside config.py (EFFORT_SCALE header now names
|
||||
exact-route request-wire recovery; the PROMPT_CACHE_TTL comment rewrapped) —
|
||||
carried from tip, since the span proof is blind to inter-span comment lines.
|
||||
14. Shared-leaf rows 840-846/852-854 (config.py) + 3238-3241 (provider_models.py)
|
||||
-> settings_defaults.py — BYTE-FALSIFIED as a copy source on 4 of 12 spans,
|
||||
transform still valid: upstream rewrote SETTINGS_DEFAULTS (advisory slot is
|
||||
the routed id `anthropic/claude-sonnet-5`, `CLAUDE_CODE_MODEL` retired,
|
||||
MAX_SUBAGENT_DEPTH default 2->3, `OUROBOROS_SOFT/HARD_TIMEOUT_SEC` live
|
||||
again with a display-only note, plus new PRESENCE/SUBAGENTS/CLAUDEXOR/
|
||||
REVIEW_NATIVE_* keys), RETIRED_SETTING_KEYS (upstream itself retired only
|
||||
PLAN_TASK_SWARM_HEARTBEAT_STALE_SEC and kept SOFT/HARD live — the
|
||||
reference's D04 retirement of those two knobs is DIVERGENT-SUPERSEDED and
|
||||
must be re-derived in its own return, not replayed), ENDPOINT_AUTHORED_
|
||||
SETTINGS (+OUROBOROS_SUBAGENT_PRESET_RECEIPT) and OPENROUTER_REVIEW_DEFAULTS
|
||||
(routed advisory id + comment). Leaf emitted FULL from BOTH parents (the
|
||||
shared-leaf convention: drift-probe per parent separately; the final
|
||||
transplant --check runs against the two parents concatenated into one
|
||||
upstream source so every span is verified in a single exit-0 report).
|
||||
provider_models.py was touched ONLY by span removal + the settings_defaults
|
||||
re-export import; its call-time `from ouroboros.config import ...` imports
|
||||
are tip truth (D02-owned) and stand.
|
||||
15. Split rows 887-912 (runtime_limits) — 3 spans byte-falsified by upstream
|
||||
drift (get_websearch_timeout_sec docstring; get_search_code_wall_sec now
|
||||
routes through _clamped_number_setting; get_max_subagent_depth reads the
|
||||
named cap), all re-emitted from tip bytes. STRUCTURAL: upstream reshaped
|
||||
`MAX_ACTIVE_SUBAGENTS_HARD_CAP = 500` into the tuple statement
|
||||
`MAX_ACTIVE_SUBAGENTS_HARD_CAP, MAX_SUBAGENT_DEPTH_HARD_CAP = 500, 10`;
|
||||
the UNROWED twin (consumed by ouroboros/tools/control_delegation.py via
|
||||
config) rides the rowed statement into runtime_limits and the facade
|
||||
re-exports both — the carried ledger must mint its row at F5. Tool note:
|
||||
the hardened --check flags this one statement as `assignment to <complex
|
||||
target>` under undeclared_top_level even though BOTH bound names are
|
||||
requested symbols (Tuple-target blind spot; every span proof in the same
|
||||
report is green, leaf_invariants=[]) — the one lane gate that exits 2 with
|
||||
a proven false-positive cause; the tool wants Tuple support at F5.
|
||||
16. Rows 918-920 (launcher_onboarding, semantic delta D03/settings seam,
|
||||
launcher half) — RE-PROVEN applicable and LANDED: the module is
|
||||
byte-identical between tip and merge-base (zero upstream drift), so the
|
||||
reference bytes apply verbatim; the pin renamed per row 920. The SERVER
|
||||
half of the same seam (rows 1080-1081, server.py lifespan) is NOT landed —
|
||||
server.py keeps the tip guarded write and its old pin; it returns with the
|
||||
D11 lane. Two unrowed oracle test adaptations were mirrored because they
|
||||
pin exactly this delta and go red without it: test_onboarding_wizard.py::
|
||||
test_the_launcher_onboarding_module_authors_no_onboarding_settings
|
||||
(reference bytes) and tests/test_server_runtime.py (launcher clause ->
|
||||
`"save_settings(" not in launcher_host`; the server clause KEEPS the tip
|
||||
guard-string assertion, diverging from the reference's both-sides form
|
||||
until D11 lands).
|
||||
17. Rows 913-917 (the rest of the D03 settings seam: config.py
|
||||
normalize_settings_raw/serialize_settings, gateway/owner_settings digest +
|
||||
locked update, packaged_cli writer) — HOT-DEFERRED: upstream rewrote
|
||||
load_settings_lock_held's read path through the NEW post-cutoff
|
||||
settings_integrity module (read_settings_json_verified /
|
||||
SettingsIntegrityError raise-through), which the reference does not have;
|
||||
replaying the reference seam verbatim would revert the integrity feature
|
||||
(the re-prove-trap class, entry 3). The whole seam machinery re-derives
|
||||
against tip bytes in its own return; its pin tests/test_settings_read_seam.py
|
||||
(a DOMAIN_MAP D12 pin) defers WITH the machinery — not transplanted by this
|
||||
lane.
|
||||
18. Pin adaptations recorded: test_settings_env_on_disk.py re-pinned one
|
||||
literal to tip bytes (ENDPOINT_AUTHORED_SETTINGS gains
|
||||
OUROBOROS_SUBAGENT_PRESET_RECEIPT — same upstream train as entry 14);
|
||||
test_config_extraction.py gains MAX_SUBAGENT_DEPTH_HARD_CAP in the owner
|
||||
inventory, Tuple-target parsing in its _top_level_names helper, and a
|
||||
narrowed provider_models clause (the reference's "no ouroboros.config
|
||||
import anywhere" + top-level model_slots import clauses type against the
|
||||
reference's D02 rework of provider_models and return with the D02 lane;
|
||||
the surviving clauses pin no IMPORT-TIME config read and leaf-object
|
||||
identity of both moved literals).
|
||||
19. settings_integrity.py — NEW upstream module (post-cutoff, absent from the
|
||||
reference and the merge base), already D12 in scripts/v7next_domains.toml;
|
||||
no ledger rows; upstream bytes stand. Non-split D12 modules re-proven:
|
||||
colab_bootstrap.py / onboarding_wizard.py / secret_masking.py /
|
||||
update_channels.py byte-identical across tip==ref==merge-base;
|
||||
settings_setup_contract.py / subscription_install_presets.py pure upstream
|
||||
drift (ref==merge-base, zero v7 delta) — upstream bytes stand.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,6 @@ import logging
|
|||
from ouroboros.config import (
|
||||
apply_settings_to_env as _apply_settings_to_env,
|
||||
load_settings,
|
||||
save_settings,
|
||||
)
|
||||
from ouroboros.server_runtime import apply_runtime_provider_defaults, has_startup_ready_provider
|
||||
|
||||
|
|
@ -28,20 +27,15 @@ def prepare_first_run_settings() -> tuple[dict, bool]:
|
|||
launcher shows once it is healthy. The onboarding SURFACE is not rendered
|
||||
here: the live server serves it (``present_first_run_onboarding``).
|
||||
"""
|
||||
settings, provider_defaults_changed, _provider_default_keys = apply_runtime_provider_defaults(load_settings())
|
||||
# Persist the pre-server normalization ONLY for an install that already has a
|
||||
# settings file. On a FRESH install this save would CREATE the file before the
|
||||
# owner's first onboarding save, and every fresh-install proof is gated on
|
||||
# that freshness — safety-light authorship, install-time agent presets (a
|
||||
# local-first launch with LOCAL_MODEL_SOURCE in the environment reaches here
|
||||
# with changed=True and would silently lose Light). Nothing is dropped: the
|
||||
# completion save persists the same normalization, and the managed server
|
||||
# keeps the mirror-image guard in its lifespan.
|
||||
from ouroboros.config import SETTINGS_PATH as _settings_path
|
||||
|
||||
if provider_defaults_changed and _settings_path.exists():
|
||||
# Owner-process boundary: a first-run/provider save may elevate runtime mode.
|
||||
save_settings(settings, allow_elevation=True)
|
||||
settings, _provider_defaults_changed, _provider_default_keys = apply_runtime_provider_defaults(load_settings())
|
||||
# The normalization is APPLIED, not persisted. Startup is a read, and a read that
|
||||
# rewrites the file it read is how a normalization becomes an owner decision: the
|
||||
# fresh-install case already had to be carved out of this save (it would create
|
||||
# settings.json before the owner's own onboarding write and lose safety-light
|
||||
# authorship and the install-time agent presets), which is the same objection in a
|
||||
# narrower dress. Nothing is dropped, because nothing here was the only place the
|
||||
# normalization happens: every reader re-derives it (`/api/settings`, `/onboarding`,
|
||||
# the onboarding host, the plan-review script), and the completion save persists it.
|
||||
_apply_settings_to_env(settings)
|
||||
return settings, not has_startup_ready_provider(settings)
|
||||
|
||||
|
|
|
|||
113
ouroboros/model_slots.py
Normal file
113
ouroboros/model_slots.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
"""Ouroboros — model slot resolution.
|
||||
|
||||
The Main/Heavy/Light/Vision/Consciousness/deep-review slots and the ordered
|
||||
cross-model fallback chain, resolved from the environment with the shipped
|
||||
defaults as the floor, plus the rename-alias migration that keeps a slot the
|
||||
owner customized under its former key. Imported by ``provider_models`` as well
|
||||
as by ``config``, which is why it holds no settings-file knowledge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from ouroboros.settings_defaults import SETTINGS_DEFAULTS
|
||||
|
||||
|
||||
def _parse_model_list(value: str) -> list[str]:
|
||||
return [item.strip() for item in str(value or "").split(",") if item.strip()]
|
||||
|
||||
|
||||
def _main_model() -> str:
|
||||
return (
|
||||
str(os.environ.get("OUROBOROS_MODEL", "") or "").strip()
|
||||
or str(SETTINGS_DEFAULTS["OUROBOROS_MODEL"])
|
||||
)
|
||||
|
||||
|
||||
def get_light_model() -> str:
|
||||
"""Light slot; empty falls back to Main (heavy/consciousness stay empty->main)."""
|
||||
return str(os.environ.get("OUROBOROS_MODEL_LIGHT", "") or "").strip() or _main_model()
|
||||
|
||||
|
||||
def get_heavy_model() -> str:
|
||||
"""Return the heavy (strong acting/coding) lane slot; empty falls back to
|
||||
OUROBOROS_MODEL. Renamed from the legacy code slot."""
|
||||
return str(os.environ.get("OUROBOROS_MODEL_HEAVY", "") or "").strip() or _main_model()
|
||||
|
||||
|
||||
def get_vision_model() -> str:
|
||||
"""Return the vision/caption model slot; empty falls back to OUROBOROS_MODEL."""
|
||||
return str(os.environ.get("OUROBOROS_MODEL_VISION", "") or "").strip() or _main_model()
|
||||
|
||||
|
||||
def get_image_input_mode() -> str:
|
||||
raw = str(os.environ.get("OUROBOROS_IMAGE_INPUT_MODE", SETTINGS_DEFAULTS["OUROBOROS_IMAGE_INPUT_MODE"]) or "").strip().lower()
|
||||
return raw if raw in {"auto", "caption", "inline", "off"} else "auto"
|
||||
|
||||
|
||||
def parse_fallback_chain() -> list[str]:
|
||||
"""Parse the raw ordered cross-model fallback chain — SSOT for every consumer
|
||||
(resilience walk, pricing categorization, credentialed-model resolution).
|
||||
|
||||
Reads OUROBOROS_MODEL_FALLBACKS, then the legacy singular OUROBOROS_MODEL_FALLBACK
|
||||
(env-only back-compat). No dedup, no active-model drop, and NO SETTINGS_DEFAULTS
|
||||
injection: an EXPLICITLY empty Fallbacks slot means "no cross-model fallback". The
|
||||
shipped default reaches a default install through apply_settings_to_env."""
|
||||
raw = (
|
||||
str(os.environ.get("OUROBOROS_MODEL_FALLBACKS", "") or "").strip()
|
||||
or str(os.environ.get("OUROBOROS_MODEL_FALLBACK", "") or "").strip()
|
||||
)
|
||||
return [m.strip() for m in _parse_model_list(raw) if str(m or "").strip()]
|
||||
|
||||
|
||||
def get_fallback_models(active_model: str = "") -> list[str]:
|
||||
"""Return the ordered cross-model resilience CHAIN (deduped, with the active model
|
||||
removed so a benchmark all-slots-one-model setup collapses the chain to a no-op)."""
|
||||
out: list[str] = []
|
||||
seen = set()
|
||||
active = str(active_model or "").strip()
|
||||
for m in parse_fallback_chain():
|
||||
if m and m != active and m not in seen:
|
||||
seen.add(m)
|
||||
out.append(m)
|
||||
return out
|
||||
|
||||
|
||||
# v6.39 slot rename-alias migration (same shape as the retention-key rename):
|
||||
# OUROBOROS_MODEL_CODE -> _HEAVY, USE_LOCAL_CODE -> USE_LOCAL_HEAVY,
|
||||
# OUROBOROS_MODEL_FALLBACK -> _FALLBACKS.
|
||||
_LEGACY_SLOT_RENAMES = (
|
||||
("OUROBOROS_MODEL_CODE", "OUROBOROS_MODEL_HEAVY"),
|
||||
("OUROBOROS_VISION_MODEL", "OUROBOROS_MODEL_VISION"),
|
||||
("USE_LOCAL_CODE", "USE_LOCAL_HEAVY"),
|
||||
("OUROBOROS_MODEL_FALLBACK", "OUROBOROS_MODEL_FALLBACKS"),
|
||||
)
|
||||
|
||||
|
||||
def migrate_legacy_slot_keys(settings: dict) -> dict:
|
||||
"""In-place settings migration, applied BEFORE defaults are merged.
|
||||
|
||||
Preserves a stored value (never orphans an owner customization), then drops the legacy
|
||||
key. Shared SSOT for every settings entry point (load_settings AND the Colab builder).
|
||||
Order matters: the singular scope-review pin is promoted HERE, before ``SETTINGS_DEFAULTS``
|
||||
supplies the plural that WINS in get_scope_review_models."""
|
||||
for _old, _new in _LEGACY_SLOT_RENAMES:
|
||||
if _new not in settings and _old in settings:
|
||||
settings[_new] = settings[_old]
|
||||
settings.pop(_old, None)
|
||||
_pin = str(settings.get("OUROBOROS_SCOPE_REVIEW_MODEL") or "").strip()
|
||||
if _pin and not str(settings.get("OUROBOROS_SCOPE_REVIEW_MODELS") or "").strip():
|
||||
settings["OUROBOROS_SCOPE_REVIEW_MODELS"] = _pin
|
||||
return settings
|
||||
|
||||
|
||||
def get_consciousness_model() -> str:
|
||||
"""Return the high-horizon background-consciousness model slot."""
|
||||
return str(os.environ.get("OUROBOROS_MODEL_CONSCIOUSNESS", "") or "").strip() or _main_model()
|
||||
|
||||
|
||||
def get_deep_self_review_model() -> str:
|
||||
"""Return the configured deep self-review model slot."""
|
||||
return (str(os.environ.get("OUROBOROS_MODEL_DEEP_SELF_REVIEW", "") or "").strip()
|
||||
or str(SETTINGS_DEFAULTS["OUROBOROS_MODEL_DEEP_SELF_REVIEW"]))
|
||||
|
|
@ -7,6 +7,8 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
|
||||
from ouroboros.settings_defaults import OPENROUTER_DEFAULTS, OPENROUTER_REVIEW_DEFAULTS # noqa: F401
|
||||
|
||||
# MiniMax exposes the same OpenAI-compatible API on two regional hosts. Keep the
|
||||
# mapping centralized so transport, capability evidence, and settings diagnostics
|
||||
# fingerprint the exact endpoint selected by the owner.
|
||||
|
|
@ -297,32 +299,6 @@ def provider_credential_plan(
|
|||
}
|
||||
|
||||
|
||||
# Shipped router profile. Keeping the root-loop role policy beside the direct
|
||||
# provider profiles gives onboarding, runtime defaults, and tests one vocabulary
|
||||
# instead of repeating model ids across those surfaces.
|
||||
OPENROUTER_DEFAULTS = {
|
||||
"main": "google/gemini-3.7-flash",
|
||||
"heavy": "",
|
||||
"light": "openai/gpt-5.6-luna",
|
||||
"vision": "",
|
||||
"consciousness": "",
|
||||
"fallback": "openai/gpt-5.6-luna",
|
||||
"deep_self_review": "openai/gpt-5.6-sol-pro",
|
||||
}
|
||||
|
||||
OPENROUTER_REVIEW_DEFAULTS = {
|
||||
"triad": (
|
||||
"google/gemini-3.7-flash",
|
||||
"openai/gpt-5.6-terra",
|
||||
"anthropic/claude-opus-5",
|
||||
),
|
||||
"scope": ("openai/gpt-5.6-terra",),
|
||||
# Routed catalog id (the retired Claude-SDK spelling migrated same-model);
|
||||
# without provider credentials the advisory gate records an audited bypass.
|
||||
"advisory": "anthropic/claude-sonnet-5",
|
||||
}
|
||||
|
||||
|
||||
OPENAI_DIRECT_DEFAULTS = {
|
||||
"main": "openai::gpt-5.6-terra",
|
||||
"heavy": "",
|
||||
|
|
|
|||
131
ouroboros/review_model_routes.py
Normal file
131
ouroboros/review_model_routes.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Ouroboros — the reviewer model lists a review lane actually runs.
|
||||
|
||||
Triad and scope review each resolve a configured comma list into the models the
|
||||
lane will call, honouring a local-only Main route and rewriting the list when the
|
||||
install has exactly one direct provider credentialed. Also the reviewer-quorum
|
||||
rule shared by every review family.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from ouroboros.model_slots import _main_model, _parse_model_list
|
||||
from ouroboros.provider_models import (
|
||||
compute_direct_review_models_fallback,
|
||||
local_only_review_route_env,
|
||||
migrate_model_value,
|
||||
)
|
||||
from ouroboros.settings_defaults import SETTINGS_DEFAULTS
|
||||
|
||||
_DIRECT_PROVIDER_REVIEW_RUNS = 3
|
||||
|
||||
|
||||
def _exclusive_direct_remote_provider_env() -> str:
|
||||
has_openrouter = bool(str(os.environ.get("OPENROUTER_API_KEY", "") or "").strip())
|
||||
has_openai = bool(str(os.environ.get("OPENAI_API_KEY", "") or "").strip())
|
||||
has_anthropic = bool(str(os.environ.get("ANTHROPIC_API_KEY", "") or "").strip())
|
||||
has_minimax = bool(str(os.environ.get("MINIMAX_API_KEY", "") or "").strip())
|
||||
has_legacy_base = bool(str(os.environ.get("OPENAI_BASE_URL", "") or "").strip())
|
||||
has_compatible = bool(str(os.environ.get("OPENAI_COMPATIBLE_BASE_URL", "") or "").strip())
|
||||
has_cloudru = bool(str(os.environ.get("CLOUDRU_FOUNDATION_MODELS_API_KEY", "") or "").strip())
|
||||
has_gigachat = bool(str(os.environ.get("GIGACHAT_CREDENTIALS", "") or "").strip()) or (
|
||||
bool(str(os.environ.get("GIGACHAT_USER", "") or "").strip())
|
||||
and bool(str(os.environ.get("GIGACHAT_PASSWORD", "") or "").strip())
|
||||
)
|
||||
# OpenRouter / legacy OpenAI base / OpenAI-compatible all route through the
|
||||
# OpenRouter-style stack, so their presence means "not an exclusive direct
|
||||
# provider". Among the registered direct providers, return one only when
|
||||
# exactly one is configured.
|
||||
if has_openrouter or has_legacy_base or has_compatible:
|
||||
return ""
|
||||
direct = [name for name, present in (
|
||||
("openai", has_openai), ("anthropic", has_anthropic), ("minimax", has_minimax),
|
||||
("cloudru", has_cloudru), ("gigachat", has_gigachat),
|
||||
) if present]
|
||||
return direct[0] if len(direct) == 1 else ""
|
||||
|
||||
|
||||
def direct_provider_review_models_fallback(provider: str) -> list[str]:
|
||||
"""Return the exact review-models list a direct-provider fallback emits."""
|
||||
if provider not in ("openai", "anthropic", "minimax", "cloudru", "gigachat"):
|
||||
return []
|
||||
main_model = str(
|
||||
os.environ.get("OUROBOROS_MODEL", SETTINGS_DEFAULTS["OUROBOROS_MODEL"]) or ""
|
||||
).strip()
|
||||
main_model = migrate_model_value(provider, main_model)
|
||||
user_light_raw = str(os.environ.get("OUROBOROS_MODEL_LIGHT", "") or "").strip()
|
||||
return compute_direct_review_models_fallback(
|
||||
provider,
|
||||
main_model,
|
||||
user_light_raw,
|
||||
review_runs=_DIRECT_PROVIDER_REVIEW_RUNS,
|
||||
)
|
||||
|
||||
|
||||
def adaptive_quorum(n_slots: int) -> int:
|
||||
"""Reviewer-quorum SSOT for an ARBITRARY configured slot count, reused by
|
||||
triad/scope/plan/skill/acceptance review. One configured reviewer needs 1 (a loud
|
||||
single_reviewer_no_diversity degraded mode), 2 need both, 3+ keep the classic 2-of-N
|
||||
majority. DISTINCT from "configured >= quorum but fewer responded", which stays a loud
|
||||
infra quorum FAILURE at the call site."""
|
||||
return 2 if n_slots >= 3 else max(1, n_slots)
|
||||
|
||||
|
||||
def get_review_models() -> list[str]:
|
||||
"""Return the configured pre-commit review model list."""
|
||||
default_str = SETTINGS_DEFAULTS["OUROBOROS_REVIEW_MODELS"]
|
||||
models_str = os.environ.get("OUROBOROS_REVIEW_MODELS", default_str) or default_str
|
||||
models = _parse_model_list(models_str)
|
||||
models = [_main_model()] * max(1, len(models)) if local_only_review_route_env() else models
|
||||
provider = _exclusive_direct_remote_provider_env()
|
||||
if not provider:
|
||||
return models
|
||||
|
||||
main_model = str(os.environ.get("OUROBOROS_MODEL", SETTINGS_DEFAULTS["OUROBOROS_MODEL"]) or "").strip()
|
||||
main_model = migrate_model_value(provider, main_model)
|
||||
provider_prefix = f"{provider}::"
|
||||
if not main_model.startswith(provider_prefix):
|
||||
return models
|
||||
|
||||
migrated = [migrate_model_value(provider, model) for model in models]
|
||||
if not migrated or any(not model.startswith(provider_prefix) for model in migrated):
|
||||
# Auto-expand to the [main]*N stochastic fallback ONLY when nothing usable is
|
||||
# configured (empty, or foreign models in an exclusive direct-provider setup). An
|
||||
# explicit provider-matching list is honored exactly, duplicates included.
|
||||
return direct_provider_review_models_fallback(provider)
|
||||
return migrated
|
||||
|
||||
|
||||
def get_review_enforcement() -> str:
|
||||
"""Return the configured pre-commit review enforcement mode."""
|
||||
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_REVIEW_ENFORCEMENT"])
|
||||
raw = (os.environ.get("OUROBOROS_REVIEW_ENFORCEMENT", default_val) or default_val).strip().lower()
|
||||
return raw if raw in {"advisory", "blocking"} else default_val
|
||||
|
||||
|
||||
def get_scope_review_models() -> list[str]:
|
||||
"""Return configured scope reviewer slots, preserving duplicate model IDs."""
|
||||
default_str = str(SETTINGS_DEFAULTS["OUROBOROS_SCOPE_REVIEW_MODELS"])
|
||||
raw = os.environ.get("OUROBOROS_SCOPE_REVIEW_MODELS", "") or ""
|
||||
if not raw.strip():
|
||||
raw = os.environ.get("OUROBOROS_SCOPE_REVIEW_MODEL", default_str) or default_str
|
||||
models = _parse_model_list(raw)
|
||||
singular = str(os.environ.get("OUROBOROS_SCOPE_REVIEW_MODEL", SETTINGS_DEFAULTS["OUROBOROS_SCOPE_REVIEW_MODEL"]) or "").strip()
|
||||
if not models and singular:
|
||||
models = [singular]
|
||||
if not models:
|
||||
models = _parse_model_list(default_str)
|
||||
models = [_main_model()] * max(1, len(models)) if local_only_review_route_env() else models
|
||||
provider = _exclusive_direct_remote_provider_env()
|
||||
if not provider:
|
||||
return models
|
||||
migrated = [migrate_model_value(provider, model) for model in models]
|
||||
provider_prefix = f"{provider}::"
|
||||
if migrated and all(model.startswith(provider_prefix) for model in migrated):
|
||||
return migrated
|
||||
migrated_singular = migrate_model_value(provider, singular or SETTINGS_DEFAULTS["OUROBOROS_SCOPE_REVIEW_MODEL"])
|
||||
if migrated_singular.startswith(provider_prefix):
|
||||
return [migrated_singular]
|
||||
fallback = direct_provider_review_models_fallback(provider)
|
||||
return fallback[:1] if fallback else migrated
|
||||
192
ouroboros/runtime_limits.py
Normal file
192
ouroboros/runtime_limits.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
"""Ouroboros — the numeric runtime knobs and their clamps.
|
||||
|
||||
Worker count, task liveness windows, per-call ceilings, reviewer and acceptance
|
||||
budgets, subagent caps and delegation windows. Every one of them is an
|
||||
environment-or-default scalar clamped into a documented band, so a typo falls
|
||||
back to the shipped value instead of disabling a rail.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from ouroboros.settings_defaults import (
|
||||
PACING_INTERVAL_DEFAULT_SEC,
|
||||
SETTINGS_DEFAULTS,
|
||||
SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC,
|
||||
)
|
||||
|
||||
|
||||
def _clamped_number_setting(key: str, *, low, high=float("inf"), cast=float):
|
||||
"""Env-or-default numeric setting clamped to [low, high]; a typo falls back to the
|
||||
shipped default. SSOT for the clamped scalar getters below — the seven of them were
|
||||
byte-identical except for key, caster and bounds (P7 DRY)."""
|
||||
try:
|
||||
value = cast(os.environ.get(key, "") or SETTINGS_DEFAULTS[key])
|
||||
except (TypeError, ValueError):
|
||||
value = cast(SETTINGS_DEFAULTS[key])
|
||||
return max(low, min(value, high))
|
||||
|
||||
|
||||
def _bounded_positive_int_setting(key: str, *, default: int, hard_max: int, min_value: int = 1) -> int:
|
||||
"""Bounded int setting; below ``min_value`` it is a typo and falls back to ``default``. Only
|
||||
subagent depth passes 0 — there an explicit 0 is a real owner choice, not unset (owner Q26)."""
|
||||
raw = os.environ.get(key, SETTINGS_DEFAULTS.get(key, default))
|
||||
try:
|
||||
parsed = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
parsed = default
|
||||
if parsed < min_value:
|
||||
parsed = default
|
||||
return max(min_value, min(parsed, hard_max))
|
||||
|
||||
|
||||
def get_max_workers() -> int:
|
||||
return _clamped_number_setting("OUROBOROS_MAX_WORKERS", low=1, cast=int)
|
||||
|
||||
|
||||
def get_task_idle_timeout_sec() -> int:
|
||||
"""Idle window before a task is eligible for an activity-based stop: it has made
|
||||
no REAL progress (its own last_progress_at) AND has no progressing subtree for
|
||||
this long. The periodic 30s process heartbeat is liveness, NOT progress."""
|
||||
return _clamped_number_setting("OUROBOROS_TASK_IDLE_TIMEOUT_SEC", low=60, cast=int)
|
||||
|
||||
|
||||
def get_task_abs_ceiling_sec() -> int:
|
||||
"""Absolute wall-clock backstop per task, independent of activity — the only hard
|
||||
time axis (budget/cost is the other, separate hard axis). A productively-waiting
|
||||
orchestrator survives to this ceiling instead of a flat 1800s wall-clock kill."""
|
||||
return _clamped_number_setting("OUROBOROS_TASK_ABS_CEILING_SEC", low=300, cast=int)
|
||||
|
||||
|
||||
def get_per_call_timeout_ceiling_sec() -> int:
|
||||
"""SSOT ceiling for an explicit per-call run_command/run_script timeout_sec
|
||||
(and the outer tool-execution cap that accommodates it)."""
|
||||
return _clamped_number_setting("OUROBOROS_PER_CALL_TIMEOUT_CEILING_SEC", low=1, cast=int)
|
||||
|
||||
|
||||
def get_restart_drain_max_sec() -> int:
|
||||
return _clamped_number_setting(
|
||||
"OUROBOROS_RESTART_DRAIN_MAX_SEC", low=0, cast=lambda v: int(float(v)))
|
||||
|
||||
|
||||
def get_safety_max_tokens() -> int:
|
||||
"""Output-token budget for safety-supervisor LLM calls (parse-bug fix)."""
|
||||
return _clamped_number_setting("OUROBOROS_SAFETY_MAX_TOKENS", low=256, high=16384, cast=int)
|
||||
|
||||
|
||||
def get_safety_call_timeout_sec() -> float:
|
||||
"""Transport timeout for safety-supervisor LLM calls (prevents indefinite hang)."""
|
||||
return _clamped_number_setting("OUROBOROS_SAFETY_CALL_TIMEOUT_SEC", low=5.0, high=600.0)
|
||||
|
||||
|
||||
def get_websearch_timeout_sec() -> float:
|
||||
"""Per-attempt transport timeout for provider-backed web_search calls."""
|
||||
return _clamped_number_setting("OUROBOROS_WEBSEARCH_TIMEOUT_SEC", low=30.0, high=3600.0)
|
||||
|
||||
|
||||
def get_llm_transport_read_timeout_sec() -> float:
|
||||
"""Default httpx read/write timeout for no_proxy LLM clients (v6.54.3, D).
|
||||
|
||||
The DEAD-SOCKET bound, not a latency target; explicit per-call timeouts win."""
|
||||
return _clamped_number_setting("OUROBOROS_LLM_TRANSPORT_READ_TIMEOUT_SEC", low=60.0, high=7200.0)
|
||||
|
||||
|
||||
def get_acceptance_review_est_sec() -> float:
|
||||
"""Estimated duration of one acceptance review/improvement pass (v6.54.4)."""
|
||||
return _clamped_number_setting("OUROBOROS_ACCEPTANCE_REVIEW_EST_SEC", low=10.0, high=3600.0)
|
||||
|
||||
|
||||
def get_acceptance_reserve_pct() -> int:
|
||||
"""Default finalization-reserve percentage of the total budget (v6.54.4)."""
|
||||
return _clamped_number_setting("OUROBOROS_ACCEPTANCE_RESERVE_PCT", low=0, high=50, cast=int)
|
||||
|
||||
|
||||
def get_plan_task_deadline_min_sec() -> float:
|
||||
"""Minimum useful deadline-scaled planning-swarm window (v6.54.3, 1.5)."""
|
||||
return _clamped_number_setting("OUROBOROS_PLAN_TASK_DEADLINE_MIN_SEC", low=30.0, high=3600.0)
|
||||
|
||||
|
||||
def get_vision_caption_timeout_sec() -> int:
|
||||
return _clamped_number_setting("OUROBOROS_VISION_CAPTION_TIMEOUT_SEC", low=1, cast=int)
|
||||
|
||||
|
||||
def get_pacing_interval_sec(settings: Optional[dict] = None) -> int:
|
||||
"""Intrinsic self-pacing checkpoint cadence in seconds (0 disables)."""
|
||||
raw = os.environ.get("OUROBOROS_PACING_INTERVAL_SEC")
|
||||
if raw is None and isinstance(settings, dict):
|
||||
raw = settings.get("OUROBOROS_PACING_INTERVAL_SEC")
|
||||
try:
|
||||
parsed = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
parsed = int(PACING_INTERVAL_DEFAULT_SEC)
|
||||
return max(0, parsed)
|
||||
|
||||
|
||||
def get_supervisor_liveness_deadline_sec(settings: Optional[dict] = None) -> int:
|
||||
"""Supervisor-loop stall deadline in seconds (0 disables the watchdog)."""
|
||||
raw = os.environ.get("OUROBOROS_SUPERVISOR_LIVENESS_DEADLINE_SEC")
|
||||
if raw is None and isinstance(settings, dict):
|
||||
raw = settings.get("OUROBOROS_SUPERVISOR_LIVENESS_DEADLINE_SEC")
|
||||
try:
|
||||
parsed = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
parsed = int(SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC)
|
||||
return max(0, parsed)
|
||||
|
||||
|
||||
def get_post_task_evolution_budget_usd() -> float:
|
||||
"""Optional per-window USD budget for post-task evolution (0 = use the
|
||||
existing EVOLUTION_BUDGET_RESERVE / TOTAL_BUDGET gating only)."""
|
||||
return _clamped_number_setting("OUROBOROS_POST_TASK_EVOLUTION_BUDGET_USD", low=0.0)
|
||||
|
||||
|
||||
# Per-root active-child ceiling (v6.82: 50->500) and absolute host-visible nesting ceiling, used by supervisor gates and ARCHITECTURE §7.
|
||||
MAX_ACTIVE_SUBAGENTS_HARD_CAP, MAX_SUBAGENT_DEPTH_HARD_CAP = 500, 10
|
||||
|
||||
|
||||
def get_max_active_subagents_per_root() -> int:
|
||||
return _bounded_positive_int_setting(
|
||||
"OUROBOROS_MAX_ACTIVE_SUBAGENTS_PER_ROOT",
|
||||
default=int(SETTINGS_DEFAULTS["OUROBOROS_MAX_ACTIVE_SUBAGENTS_PER_ROOT"]),
|
||||
hard_max=MAX_ACTIVE_SUBAGENTS_HARD_CAP,
|
||||
)
|
||||
|
||||
|
||||
def get_max_subagent_depth() -> int:
|
||||
"""Structural nesting cap; 0 = NO delegation at all (every child refused, root tasks still
|
||||
run). Before v6.79.0 a configured 0 was silently rewritten to 2, so "no-swarm" delegated."""
|
||||
return _bounded_positive_int_setting(
|
||||
"OUROBOROS_MAX_SUBAGENT_DEPTH",
|
||||
default=int(SETTINGS_DEFAULTS["OUROBOROS_MAX_SUBAGENT_DEPTH"]),
|
||||
hard_max=MAX_SUBAGENT_DEPTH_HARD_CAP,
|
||||
min_value=0,
|
||||
)
|
||||
|
||||
|
||||
# delegate_wait's ToolEntry per-call timeout (above it a configured ceiling buys a
|
||||
# KILLED call, not a longer wait; pinned by test) and the hard max WINDOW per call
|
||||
# (F5): 1800 < 2100 (kill) < 2400 (lease) — decoupled, a raised timeout never widens it.
|
||||
DELEGATE_WAIT_CEILING_SEC = 2100
|
||||
DELEGATE_WAIT_WINDOW_MAX_SEC = 1800
|
||||
|
||||
|
||||
def get_delegate_wait_max_sec() -> int:
|
||||
"""delegate_wait window ceiling: the setting NARROWS, never widens past 1800."""
|
||||
return _clamped_number_setting(
|
||||
"OUROBOROS_DELEGATE_WAIT_MAX_SEC", low=1, high=DELEGATE_WAIT_WINDOW_MAX_SEC, cast=int)
|
||||
|
||||
|
||||
def get_delegate_wait_sec() -> int:
|
||||
"""Default WINDOW one ``delegate_wait`` call holds — not a quiet cutoff: the
|
||||
wait holds, returns its advances, and bounds the nanny's mailbox absence."""
|
||||
return _clamped_number_setting(
|
||||
"OUROBOROS_DELEGATE_WAIT_SEC", low=1, high=get_delegate_wait_max_sec(), cast=int)
|
||||
|
||||
|
||||
def get_search_code_wall_sec() -> float:
|
||||
"""Total wall-clock budget (seconds) for ONE search_code call — bounds both the rg
|
||||
directory walk and the batched rg loop so a scan over a very large root cannot run
|
||||
unbounded. Env/setting: ``OUROBOROS_SEARCH_CODE_WALL_SEC`` (floored at 5s)."""
|
||||
return _clamped_number_setting("OUROBOROS_SEARCH_CODE_WALL_SEC", low=5.0)
|
||||
361
ouroboros/settings_defaults.py
Normal file
361
ouroboros/settings_defaults.py
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
"""Ouroboros — the settings vocabulary.
|
||||
|
||||
What settings exist, the values a fresh install ships, which keys a release has
|
||||
retired, and which keys never travel between disk and the environment. Data and
|
||||
derivations only: nothing here reads or writes settings.json.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ouroboros.update_channels import UPDATE_SETTINGS_DEFAULTS
|
||||
|
||||
FINALIZATION_GRACE_DEFAULT_SEC = 120
|
||||
# Owner finalization outer cap starts at the stop request; grace starts at control delivery
|
||||
# (the loop's mailbox drain). No summary by this cap -> honest custody cancel.
|
||||
OWNER_STOP_OUTER_CAP_SEC = 600
|
||||
# Cadence for intrinsic self-pacing checkpoints when a task has NO deadline_at (headless benchmark runs). Advisory only — surfaces elapsed/rounds/cost for self-pacing; 0 disables.
|
||||
PACING_INTERVAL_DEFAULT_SEC = 600
|
||||
# Supervisor-loop liveness deadline (WS3, v6.34.0): a watchdog thread flags the main supervisor loop STALLED if it has not ticked within this many seconds (healthy tick ~0.5s, real wedges only). 0 disables.
|
||||
SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC = 90
|
||||
|
||||
|
||||
# Shipped router profile. Keeping the root-loop role policy beside the direct
|
||||
# provider profiles gives onboarding, runtime defaults, and tests one vocabulary
|
||||
# instead of repeating model ids across those surfaces.
|
||||
OPENROUTER_DEFAULTS = {
|
||||
"main": "google/gemini-3.7-flash",
|
||||
"heavy": "",
|
||||
"light": "openai/gpt-5.6-luna",
|
||||
"vision": "",
|
||||
"consciousness": "",
|
||||
"fallback": "openai/gpt-5.6-luna",
|
||||
"deep_self_review": "openai/gpt-5.6-sol-pro",
|
||||
}
|
||||
|
||||
OPENROUTER_REVIEW_DEFAULTS = {
|
||||
"triad": (
|
||||
"google/gemini-3.7-flash",
|
||||
"openai/gpt-5.6-terra",
|
||||
"anthropic/claude-opus-5",
|
||||
),
|
||||
"scope": ("openai/gpt-5.6-terra",),
|
||||
# Routed catalog id (the retired Claude-SDK spelling migrated same-model);
|
||||
# without provider credentials the advisory gate records an audited bypass.
|
||||
"advisory": "anthropic/claude-sonnet-5",
|
||||
}
|
||||
|
||||
|
||||
# Settings defaults
|
||||
SETTINGS_DEFAULTS = {**UPDATE_SETTINGS_DEFAULTS,
|
||||
"OPENROUTER_API_KEY": "",
|
||||
"OPENAI_API_KEY": "",
|
||||
"OPENAI_BASE_URL": "",
|
||||
"OPENAI_COMPATIBLE_API_KEY": "",
|
||||
"OPENAI_COMPATIBLE_BASE_URL": "",
|
||||
"CLOUDRU_FOUNDATION_MODELS_API_KEY": "",
|
||||
"CLOUDRU_FOUNDATION_MODELS_BASE_URL": "https://foundation-models.api.cloud.ru/v1",
|
||||
"GIGACHAT_CREDENTIALS": "",
|
||||
"GIGACHAT_USER": "",
|
||||
"GIGACHAT_PASSWORD": "",
|
||||
"GIGACHAT_SCOPE": "GIGACHAT_API_PERS",
|
||||
"GIGACHAT_BASE_URL": "https://api.giga.chat/v1",
|
||||
"GIGACHAT_VERIFY_SSL_CERTS": "true",
|
||||
"GIGACHAT_PROFANITY_CHECK": "",
|
||||
"ANTHROPIC_API_KEY": "",
|
||||
"MINIMAX_API_KEY": "",
|
||||
"MINIMAX_REGION": "",
|
||||
"OUROBOROS_NETWORK_PASSWORD": "",
|
||||
"OUROBOROS_SERVER_HOST": "127.0.0.1",
|
||||
"OUROBOROS_HOST_SERVICE_PORT": 8767,
|
||||
"OUROBOROS_MODEL": OPENROUTER_DEFAULTS["main"],
|
||||
# Worker lanes; empty means "use OUROBOROS_MODEL" (one model by default, per-lane
|
||||
# override optional). HEAVY = mutative first-level subagents; LIGHT = auto/deep bulk.
|
||||
"OUROBOROS_MODEL_HEAVY": OPENROUTER_DEFAULTS["heavy"],
|
||||
"OUROBOROS_MODEL_LIGHT": OPENROUTER_DEFAULTS["light"],
|
||||
"OUROBOROS_MODEL_VISION": OPENROUTER_DEFAULTS["vision"],
|
||||
"OUROBOROS_IMAGE_INPUT_MODE": "auto",
|
||||
# Background consciousness is a high-horizon loop, not a cheap helper lane.
|
||||
"OUROBOROS_MODEL_CONSCIOUSNESS": OPENROUTER_DEFAULTS["consciousness"],
|
||||
# Cross-model resilience CHAIN (comma-separated, ordered). A single model is a
|
||||
# 1-element chain; empty disables cross-model fallback. Resilience slot — keeps a
|
||||
# real default, unlike the worker lanes. (Renamed from the singular MODEL_FALLBACK.)
|
||||
"OUROBOROS_MODEL_FALLBACKS": OPENROUTER_DEFAULTS["fallback"],
|
||||
"OUROBOROS_MODEL_DEEP_SELF_REVIEW": OPENROUTER_DEFAULTS["deep_self_review"],
|
||||
"OUROBOROS_MAX_WORKERS": 10, "OUROBOROS_PRESENCE_MAX_ACTIVE": 2,
|
||||
"OUROBOROS_MAX_ACTIVE_SUBAGENTS_PER_ROOT": 6,
|
||||
"OUROBOROS_MAX_SUBAGENT_DEPTH": 3,
|
||||
# Mutative ("acting") subagents master toggle. Empty = follow runtime mode
|
||||
# (ON in advanced/pro, OFF in light); explicit true/false overrides. Owner-
|
||||
# controlled; light-mode self-repo writes stay blocked by the sandbox.
|
||||
"OUROBOROS_ALLOW_MUTATIVE_SUBAGENTS": "",
|
||||
# Acting self_worktree base location + durable genesis projects root (both
|
||||
# outside repo/ and data/). genesis projects are durable and never GC'd.
|
||||
"OUROBOROS_SUBAGENT_WORKTREE_ROOT": "",
|
||||
"OUROBOROS_SUBAGENT_PROJECTS_ROOT": "",
|
||||
"OUROBOROS_DELIVERABLES_ROOT": "",
|
||||
# Unified age-based GC retention (days) for ALL disposable runtime artifacts:
|
||||
# subagent worktrees, headless/direct task drives, and leftover service logs.
|
||||
# Single owner-facing knob (math SSOT in ouroboros/retention.py); deprecated
|
||||
# per-subsystem keys are migrated to this on settings load.
|
||||
"OUROBOROS_GC_RETENTION_DAYS": 7,
|
||||
"TOTAL_BUDGET": 200.0,
|
||||
"OUROBOROS_PER_TASK_COST_USD": 50.0,
|
||||
# cloud.ru catalog prices are RUB per 1M while the budget is USD. No implicit
|
||||
# exchange rate: the owner must explicitly configure the divisor.
|
||||
"OUROBOROS_RUB_USD_RATE": "",
|
||||
# Live-pricing (OpenRouter + cloud.ru catalog) refetch interval; prices/FX drift.
|
||||
"OUROBOROS_PRICING_TTL_SEC": 21600,
|
||||
# Main-loop round ceiling (was an inline literal in loop.py — hot-reloadable now).
|
||||
"OUROBOROS_MAX_ROUNDS": 200,
|
||||
# Same-model attempt budget for TRANSIENT provider failure classes
|
||||
# (finish_reason=null, 429/5xx/overloaded); floored at the caller's base
|
||||
# retry budget. Permanent classes fail fast regardless.
|
||||
"OUROBOROS_TRANSIENT_RETRY_MAX": 6,
|
||||
# #4 self-DoS guard: max concurrent provider calls per (model, use_local) route; excess
|
||||
# worker threads wait (deadline-bounded) instead of storming one model's rate limit. <=0
|
||||
# disables. Default-on, fail-soft (see ouroboros/model_concurrency.py).
|
||||
"OUROBOROS_MODEL_MAX_CONCURRENCY": 3,
|
||||
# Hard ceiling (seconds) a provider call waits for a concurrency slot when the task has
|
||||
# NO deadline; past it the call proceeds WITHOUT a slot (never blocks forever). SSOT here.
|
||||
"OUROBOROS_MODEL_SLOT_MAX_WAIT_SEC": 180,
|
||||
# Project-naming LIGHT-call waits (v6.40): the provider-call transport timeout and the
|
||||
# gateway's hard wait for the inline turn-into-project name. SSOT here (not magic numbers
|
||||
# in project_naming.py) per DEVELOPMENT "Timeout & Wait Control".
|
||||
"OUROBOROS_PROJECT_NAMING_TIMEOUT_SEC": 60,
|
||||
"OUROBOROS_PROJECT_NAMING_ASYNC_TIMEOUT_SEC": 8,
|
||||
# Skill lifecycle lane deadline (wedged-job loud-failure bound).
|
||||
"OUROBOROS_SKILL_LIFECYCLE_TIMEOUT_SEC": 1800,
|
||||
"OUROBOROS_CLAUDEXOR_HARNESS_INSTALL_TIMEOUT_SEC": 300,
|
||||
"OUROBOROS_SOFT_TIMEOUT_SEC": 600,
|
||||
# NOTE: OUROBOROS_HARD_TIMEOUT_SEC no longer terminates tasks — the flat wall-clock
|
||||
# kill was replaced by the activity model below (idle + subtree-liveness, abs ceiling).
|
||||
# It survives only as a soft-warning/status display input; runtime is governed by
|
||||
# OUROBOROS_TASK_IDLE_TIMEOUT_SEC and OUROBOROS_TASK_ABS_CEILING_SEC.
|
||||
"OUROBOROS_HARD_TIMEOUT_SEC": 1800,
|
||||
# Activity-based liveness (replaces flat wall-clock as the primary stop):
|
||||
# idle window = no real progress AND no progressing subtree; abs ceiling = the
|
||||
# unconditional per-task backstop (budget/cost stays a separate hard axis).
|
||||
"OUROBOROS_TASK_IDLE_TIMEOUT_SEC": 900,
|
||||
"OUROBOROS_TASK_ABS_CEILING_SEC": 21600,
|
||||
"OUROBOROS_PER_CALL_TIMEOUT_CEILING_SEC": 1800,
|
||||
"OUROBOROS_FINALIZATION_GRACE_SEC": FINALIZATION_GRACE_DEFAULT_SEC,
|
||||
"OUROBOROS_SUPERVISOR_LIVENESS_DEADLINE_SEC": SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC,
|
||||
"OUROBOROS_PACING_INTERVAL_SEC": PACING_INTERVAL_DEFAULT_SEC,
|
||||
"OUROBOROS_TOOL_TIMEOUT_SEC": 600,
|
||||
"OUROBOROS_VISION_CAPTION_TIMEOUT_SEC": 90,
|
||||
"OUROBOROS_BG_MAX_ROUNDS": 10,
|
||||
"OUROBOROS_BG_WAKEUP_MIN": 30,
|
||||
"OUROBOROS_BG_WAKEUP_MAX": 7200,
|
||||
# Post-task self-evolution envelope (V4). Owner-enabled capability whose
|
||||
# CONTENT stays LLM-first; default OFF. When enabled, after a qualifying task
|
||||
# the worker may promote one high-value code-class backlog item into the
|
||||
# existing (gated) evolution campaign. Cadence: off | llm | every_n:<k>.
|
||||
"OUROBOROS_POST_TASK_EVOLUTION": "false",
|
||||
"OUROBOROS_POST_TASK_EVOLUTION_CADENCE": "llm",
|
||||
"OUROBOROS_POST_TASK_EVOLUTION_BUDGET_USD": 0.0,
|
||||
# Optional owner steer appended to each evolution cycle's objective (never
|
||||
# overrides the LLM-first promotion). Empty = pure LLM choice.
|
||||
"OUROBOROS_EVOLUTION_PERSISTENT_OBJECTIVE": "",
|
||||
"OUROBOROS_WEBSEARCH_MODEL": "gpt-5.2",
|
||||
# web_search backend pin: auto (default OpenAI-first cascade) | ddgs (pure
|
||||
# retrieval, no second LLM — for fixed-model runs) | openai | openrouter | anthropic.
|
||||
"OUROBOROS_WEBSEARCH_BACKEND": "auto",
|
||||
# Main-loop OpenRouter server web-search tool. Off by default: provider-
|
||||
# specific capability, not a core provider-independence requirement.
|
||||
"OUROBOROS_MAIN_WEB_SEARCH": "off",
|
||||
"OUROBOROS_MAIN_WEB_SEARCH_ENGINE": "auto",
|
||||
"OUROBOROS_MAIN_WEB_SEARCH_MAX_TOTAL_RESULTS": 10,
|
||||
# OpenRouter provider routing: "" (off) | resilience (same-model failover, cache-warm)
|
||||
# | repro (pin, no failover — fixed-model runs) | a raw JSON `provider` object.
|
||||
"OUROBOROS_OR_PROVIDER": "",
|
||||
# search_code total wall-clock budget (seconds) bounding the rg walk + the fallback walk.
|
||||
"OUROBOROS_SEARCH_CODE_WALL_SEC": "45",
|
||||
# NOTE: OUROBOROS_OBSERVABILITY_KEEP_RAW (writes UNREDACTED secret-bearing payloads to
|
||||
# disk) is intentionally NOT a settings/UI carrier — it is an env-only operator debug
|
||||
# override so a self-change or non-owner save can never enable secret logging.
|
||||
# Generative context-window probe machinery: when enabled AND a caller passes
|
||||
# allow_generative=True, confirms a route's >=1M window from a FREE over-window
|
||||
# reject; *_CHARS sizes the padding. Since the settings-time Max gate retirement
|
||||
# no production surface passes allow_generative=True (dormant; kept for tests
|
||||
# and future explicit owner probes).
|
||||
"OUROBOROS_GENERATIVE_PROBE": "1",
|
||||
"OUROBOROS_GENERATIVE_PROBE_CHARS": "5000000",
|
||||
# Pre-commit review: comma-separated provider-tagged model list
|
||||
"OUROBOROS_REVIEW_MODELS": ",".join(OPENROUTER_REVIEW_DEFAULTS["triad"]),
|
||||
"OUROBOROS_REVIEWER_SLOTS": "", # structured slot SSOT (reviewer_slot_config.py); "" = legacy comma keys
|
||||
"OUROBOROS_SUBAGENTS": "", # configured task-actor SSOT; "" = bounded legacy/undecided read
|
||||
# INSTALL-TIME facts: the agent-preset generation this install received, and WHEN onboarding last completed
|
||||
# (recorded on EVERY completion). Endpoint-authored and disk-only — see ENDPOINT_AUTHORED_SETTINGS.
|
||||
"OUROBOROS_SUBSCRIPTION_PRESET_VERSION": "",
|
||||
"OUROBOROS_SUBAGENT_PRESET_RECEIPT": "",
|
||||
"OUROBOROS_ONBOARDING_COMPLETED_AT": "",
|
||||
# Pre-commit review enforcement: advisory | blocking
|
||||
"OUROBOROS_REVIEW_ENFORCEMENT": "advisory",
|
||||
# Native tool-round reviewer episode caps (review_native_episode.py owns
|
||||
# the getters); both fail CLOSED — typed refusal, never compaction/resume.
|
||||
"OUROBOROS_REVIEW_NATIVE_MAX_ROUNDS": "16",
|
||||
"OUROBOROS_REVIEW_NATIVE_MAX_TRANSCRIPT_CHARS": "900000",
|
||||
# Auto-grant reviewed-skill requests by default; grants stay bound to the
|
||||
# reviewed content hash and editing a skill still invalidates them.
|
||||
"OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS": "true",
|
||||
# Launcher-seeded native skills carry a hash-pinned native-trust review
|
||||
# verdict (the payload bytes shipped through the repo commit gate); the
|
||||
# zero-grant ones also auto-enable. Editing the payload still goes stale.
|
||||
# Owner opt-out: set to false to keep manual review for native seeds.
|
||||
"OUROBOROS_TRUST_NATIVE_SEEDED_SKILLS": "true",
|
||||
# Agent-requested restarts drain running tasks first: while any RUNNING
|
||||
# task still heartbeats, the restart waits up to this many seconds before
|
||||
# proceeding fail-closed (0 = no drain, restart immediately).
|
||||
"OUROBOROS_RESTART_DRAIN_MAX_SEC": 120,
|
||||
# Runtime mode: light | advanced | pro; pro still requires review gates.
|
||||
"OUROBOROS_RUNTIME_MODE": "advanced",
|
||||
# Context mode: low | max. Owner-only working-context size profile. max = full always-on docs +
|
||||
# current memory granularity; low = ARCHITECTURE as a navigation map + deeper memory consolidation,
|
||||
# sized for ~200k / local models. Cognitive-horizon knob (BIBLE P1): the agent cannot lower it
|
||||
# (owner-only), and it never changes model / reasoning-effort / output-token budgets.
|
||||
"OUROBOROS_CONTEXT_MODE": "max",
|
||||
# One-window compatibility tombstone for the retired persistent auto-Low mechanism.
|
||||
# It never sizes or routes context and no runtime writer may set it true. An explicit
|
||||
# false still distinguishes owner-authored Low from a bare forwarded env Low for P3.
|
||||
"OUROBOROS_CONTEXT_MODE_AUTO_LOW": "false",
|
||||
# Optional extra user-managed skills checkout; Ouroboros never clones/pulls it.
|
||||
"OUROBOROS_SKILLS_REPO_PATH": "",
|
||||
"OUROBOROS_CLAWHUB_REGISTRY_URL": "https://clawhub.ai/api/v1",
|
||||
"OUROBOROS_HUB_CATALOG_URL": "https://raw.githubusercontent.com/razzant/OuroborosHub/main/catalog.json",
|
||||
"MCP_ENABLED": False,
|
||||
"MCP_SERVERS": [],
|
||||
"MCP_TOOL_TIMEOUT_SEC": 60,
|
||||
# Scope review: one or more reviewer slots; enforcement follows OUROBOROS_REVIEW_ENFORCEMENT.
|
||||
"OUROBOROS_SCOPE_REVIEW_MODELS": ",".join(OPENROUTER_REVIEW_DEFAULTS["scope"]),
|
||||
"OUROBOROS_SCOPE_REVIEW_MODEL": OPENROUTER_REVIEW_DEFAULTS["scope"][0],
|
||||
# DEPRECATED, enforcement-inert (v6.80.0): stored, owner-only (dedicated audited endpoint), but
|
||||
# NOTHING consults it — whether the BIBLE P3 blocking scope review applies follows owner-only
|
||||
# OUROBOROS_CONTEXT_MODE. Degraded opt-in key: removed.
|
||||
"OUROBOROS_SCOPE_REVIEW_FLOOR": "blocking_1m",
|
||||
"OUROBOROS_TASK_REVIEW_MODE": "auto",
|
||||
# LLM safety-supervisor coverage (owner-only, like runtime/context mode):
|
||||
# full (shipped default; fail-closed fallbacks land here; a FRESH wizard authors
|
||||
# "light") — LLM check on POLICY_CHECK + conditional shell.
|
||||
# light — LLM check ONLY on POLICY_CHECK integration tools; POLICY_CHECK_CONDITIONAL
|
||||
# shell/verify fall to the deterministic whitelist + registry guards (no LLM).
|
||||
# off — no LLM safety calls at all; the deterministic registry sandbox, protected-path
|
||||
# policy and light-mode guards STAY ON. Every non-full mode audits durably.
|
||||
"OUROBOROS_SAFETY_MODE": "full",
|
||||
# Safety-supervisor LLM call shaping (v6.54.3 parse-bug fix): a tight output
|
||||
# budget + no reasoning keeps the light model from spending its whole budget on
|
||||
# hidden reasoning and returning a 1-token/empty body that fails JSON parse and
|
||||
# then fail-closed blocks a benign command. Registered numeric SSOT (no inline literals).
|
||||
"OUROBOROS_SAFETY_MAX_TOKENS": 2000,
|
||||
"OUROBOROS_SAFETY_CALL_TIMEOUT_SEC": 60,
|
||||
# v6.54.3 transport-timeout SSOT (deadline package D). web_search: 480 is one
|
||||
# provider-attempt bound; the ToolEntry envelope derives the configured paid
|
||||
# cascade. LLM no_proxy: 2700 leaves room for long silent reasoning without
|
||||
# pinning a worker on a dead socket.
|
||||
"OUROBOROS_WEBSEARCH_TIMEOUT_SEC": 480,
|
||||
"OUROBOROS_LLM_TRANSPORT_READ_TIMEOUT_SEC": 2700,
|
||||
# v6.54.3 (1.5): plan_task deadline scaling. With a task deadline the planning swarm's
|
||||
# wait ceiling is min(configured ceiling, remaining/4); below this floor plan_task SKIPS
|
||||
# with a typed reason + telemetry rather than eat the tail of the budget.
|
||||
"OUROBOROS_PLAN_TASK_DEADLINE_MIN_SEC": 300,
|
||||
# Acceptance-review budget layer (task_pacing SSOT). The first final review
|
||||
# reserves at least 200s; later passes use max(this floor, 1.5×timing EWMA).
|
||||
"OUROBOROS_ACCEPTANCE_REVIEW_EST_SEC": 200,
|
||||
# Shared paid-review-cycle cap (SSOT + per-gate meaning: ouroboros/review_cycles.py):
|
||||
# STRING "N"|"unlimited": plan review, acceptance (passes = cycles - 1), commit gate and skill review (paid cycles per root task / manual snapshot); identical material is never re-reviewed for pay on any gate.
|
||||
"OUROBOROS_REVIEW_MAX_CYCLES": "2",
|
||||
"OUROBOROS_ACCEPTANCE_RESERVE_PCT": 5,
|
||||
# Prompt-cache TTL, one honest GLOBAL override (owner decision 2026-08-08, batch #2 Q2=A): applied to
|
||||
# EVERY cache_control breakpoint on the Anthropic-normalizing family — main loop, review lanes, safety
|
||||
# supervisor alike — at the ONE send-time finalizer (llm._normalize_payload_cache_ttl). 'default' = bare
|
||||
# markers (provider default 5m tier); '5m'/'1h' = the explicit Anthropic ephemeral tiers ('1h' bills cache
|
||||
# writes at the documented 2x-vs-1.25x ratio). Non-Anthropic wire formats are a NO-OP by construction
|
||||
# (Gemini documents no ttl field — the v5.30.0 outage class).
|
||||
"OUROBOROS_PROMPT_CACHE_TTL": "1h",
|
||||
# Reasoning effort per task type: none | low | medium | high
|
||||
"OUROBOROS_EFFORT_TASK": "medium",
|
||||
"OUROBOROS_EFFORT_EVOLUTION": "high",
|
||||
"OUROBOROS_EFFORT_REVIEW": "high",
|
||||
"OUROBOROS_EFFORT_SCOPE_REVIEW": "high",
|
||||
"OUROBOROS_EFFORT_DEEP_SELF_REVIEW": "high",
|
||||
"OUROBOROS_EFFORT_CONSCIOUSNESS": "high",
|
||||
"OUROBOROS_RETURN_REASONING": True,
|
||||
"OUROBOROS_REASONING_SUMMARY": "auto",
|
||||
"GITHUB_TOKEN": "",
|
||||
"GITHUB_REPO": "",
|
||||
# Local model (llama-cpp-python server)
|
||||
"LOCAL_MODEL_SOURCE": "",
|
||||
"LOCAL_MODEL_FILENAME": "",
|
||||
"LOCAL_MODEL_PORT": 8766,
|
||||
"LOCAL_MODEL_N_GPU_LAYERS": 0,
|
||||
"LOCAL_MODEL_CONTEXT_LENGTH": 16384,
|
||||
"LOCAL_MODEL_CHAT_FORMAT": "",
|
||||
"USE_LOCAL_MAIN": False,
|
||||
"USE_LOCAL_HEAVY": False,
|
||||
"USE_LOCAL_LIGHT": False,
|
||||
"USE_LOCAL_CONSCIOUSNESS": False,
|
||||
"USE_LOCAL_FALLBACK": False,
|
||||
"OUROBOROS_FILE_BROWSER_DEFAULT": "",
|
||||
# 429-aware cross-model fallback: process-local cooldown for transiently failing
|
||||
# models (429/5xx/overloaded), passive heal-back. Owner-tunable; default-on, fail-soft.
|
||||
"OUROBOROS_FALLBACK_COOLDOWN_ENABLED": True,
|
||||
"OUROBOROS_FALLBACK_COOLDOWN_SEC": 120,
|
||||
"OUROBOROS_FALLBACK_ATTEMPTS_PER_MODEL": 1,
|
||||
# Delegated subagents. NARROW key, read ONLY by the subagent scheduler; deliberately absent from
|
||||
# provider_models.MODEL_SETTING_KEYS (see ARCHITECTURE "Delegated subagents"). Empty = delegation off AND
|
||||
# undecided (Settings' Subagents section offers the connected-subscription default); the literal `off` =
|
||||
# delegation off because the owner said so. Wait keys bound the nanny's QUIET wait only.
|
||||
"OUROBOROS_SUBAGENT_HARNESS": "",
|
||||
# Optional Delegation account pin (D-U5): a credential-profile id sent as `credentialProfileId`; empty = engine
|
||||
# rotation pool (D28; presets never author it). Read ONLY by get_subagent_harness -> DelegationRoute.profile_id.
|
||||
"OUROBOROS_SUBAGENT_PROFILE": "",
|
||||
"OUROBOROS_DELEGATE_WAIT_SEC": 120,
|
||||
"OUROBOROS_DELEGATE_WAIT_MAX_SEC": 1800,
|
||||
}
|
||||
|
||||
|
||||
# Setting keys a release DELETED. `load_settings` keeps unrecognized keys so a rename never destroys
|
||||
# an owner customization — which would otherwise leave a removed key living in data/settings.json
|
||||
# forever, still served by GET /api/settings. Retiring a key is a decision; its ghost is not.
|
||||
RETIRED_SETTING_KEYS: tuple[str, ...] = (
|
||||
# v6.87.7: the depth cap conflated how DEEP delegation nests with how STRONG a descendant is.
|
||||
"OUROBOROS_SUBAGENT_CAPABILITY_DEPTH_LIMIT",
|
||||
# knobs are retired (the review-cycle cap OUROBOROS_REVIEW_MAX_CYCLES bounds plan review).
|
||||
"OUROBOROS_ACCEPTANCE_MAX_IMPROVEMENT_PASSES",
|
||||
"OUROBOROS_PLAN_TASK_SWARM_TIMEOUT_SEC",
|
||||
"OUROBOROS_PLAN_TASK_SWARM_MAX_WAIT_SEC",
|
||||
"OUROBOROS_PLAN_TASK_SWARM_HEARTBEAT_STALE_SEC",
|
||||
)
|
||||
|
||||
|
||||
# The same keys from the other side: load_settings overlays env onto disk-ABSENT keys, so without this an
|
||||
# ordinary load->save round-trip in a process whose env says low/off would launder that value onto disk
|
||||
# unauthorised — or, once the guard reads disk, raise a PermissionError nobody authored. Owner endpoints
|
||||
# write BOTH disk and env, so the owner path is unaffected.
|
||||
_DISK_AUTHORED_SETTINGS = ("OUROBOROS_CONTEXT_MODE", "OUROBOROS_CONTEXT_MODE_AUTO_LOW", "OUROBOROS_SAFETY_MODE")
|
||||
|
||||
# ENDPOINT-AUTHORED, DISK-ONLY: install-time facts POST /api/onboarding/complete alone writes. The ratchets above are
|
||||
# disk-authored yet DO project once the file carries them; these never leave disk in EITHER direction — an env timestamp alone closed the onboarding window on a fresh install, and an env marker was then persisted by a save.
|
||||
ENDPOINT_AUTHORED_SETTINGS = frozenset({"OUROBOROS_SUBSCRIPTION_PRESET_VERSION", "OUROBOROS_SUBAGENT_PRESET_RECEIPT", "OUROBOROS_ONBOARDING_COMPLETED_AT"})
|
||||
|
||||
|
||||
# Settings keys deliberately NOT projected into the environment. Everything else in SETTINGS_DEFAULTS IS
|
||||
# exported, by derivation rather than a parallel hand-kept list: such a list drifts silently and the failure
|
||||
# is invisible — settings accept the key, the UI shows it saved, and the consumer goes on reading os.environ
|
||||
# and falling back to its hardcoded constant (OUROBOROS_SKILL_LIFECYCLE_TIMEOUT_SEC sat like that behind a
|
||||
# hardcoded 1800). Deriving makes export the DEFAULT for a new key and an exclusion a decision written here.
|
||||
SETTINGS_KEYS_NOT_EXPORTED_TO_ENV = frozenset({
|
||||
# Structured list value: `str(value)` is a Python repr no reader parses back, and every consumer already reads
|
||||
# it from the settings dict (mcp_client.parse_servers, gateway.mcp), never from the environment.
|
||||
"MCP_SERVERS",
|
||||
# ENV IS THE AUTHORITY for the bind host, not settings. `ouroboros server --host 0.0.0.0` puts the choice in
|
||||
# the environment, and both consumers (server.main, server_control.restart_current_process) deliberately read
|
||||
# env BEFORE settings. Exporting this key stamped the settings value — usually the shipped 127.0.0.1 default,
|
||||
# which no owner authored — back over that environment, so the operator's LAN-reachable server silently became
|
||||
# loopback at the first self-restart. A default standing in for an absent key is not a decision.
|
||||
"OUROBOROS_SERVER_HOST",
|
||||
}) | ENDPOINT_AUTHORED_SETTINGS # disk-only in BOTH directions (never read from env, never exported to it)
|
||||
|
||||
|
||||
def settings_env_keys() -> list:
|
||||
"""Settings keys projected into os.environ, derived from SETTINGS_DEFAULTS."""
|
||||
return [k for k in SETTINGS_DEFAULTS if k not in SETTINGS_KEYS_NOT_EXPORTED_TO_ENV]
|
||||
111
ouroboros/settings_scales.py
Normal file
111
ouroboros/settings_scales.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""Ouroboros — the closed scales a settings value is clamped to.
|
||||
|
||||
Reasoning effort, prompt-cache tier, runtime mode and safety-supervisor coverage
|
||||
are ordered or enumerated vocabularies. Each one is defined once here, with the
|
||||
clamp that turns any caller-supplied or environment-supplied text into a member
|
||||
of it, so an unknown value can never reach a consumer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from ouroboros.settings_defaults import SETTINGS_DEFAULTS
|
||||
|
||||
# v6.57.0 — EFFORT_SCALE: ORDERED reasoning-effort SSOT (low→high), the single place a tier
|
||||
# is defined (settings, llm.py builder, switch_model enum, subagent lanes). Exact-route
|
||||
# request-wire recovery, not legacy model-global evidence, owns provider adaptation.
|
||||
EFFORT_SCALE: tuple[str, ...] = ("none", "minimal", "low", "medium", "high", "xhigh", "max")
|
||||
|
||||
|
||||
def effort_rank(value: str) -> int:
|
||||
"""Index of an effort in EFFORT_SCALE (−1 if unknown). Strength-ordering SSOT."""
|
||||
v = str(value or "").strip().lower()
|
||||
return EFFORT_SCALE.index(v) if v in EFFORT_SCALE else -1
|
||||
|
||||
|
||||
def clamp_effort_to(value: str, ceiling: str) -> str:
|
||||
"""Clamp ``value`` down to ``ceiling`` on EFFORT_SCALE; unknown inputs pass through."""
|
||||
vi, ci = effort_rank(value), effort_rank(ceiling)
|
||||
return ceiling if (vi >= 0 and ci >= 0 and vi > ci) else str(value or "").strip().lower()
|
||||
|
||||
|
||||
def effort_one_step_down(value: str) -> str:
|
||||
"""Next-lower effort on EFFORT_SCALE (reject-and-retry walk); floors at `none`."""
|
||||
idx = effort_rank(value)
|
||||
return EFFORT_SCALE[idx - 1] if idx > 0 else ("none" if idx == 0 else "medium")
|
||||
|
||||
|
||||
def resolve_effort(task_type: str) -> str:
|
||||
"""Return the configured reasoning effort for the given task type."""
|
||||
t = (task_type or "").lower().strip()
|
||||
|
||||
if t == "evolution":
|
||||
key = "OUROBOROS_EFFORT_EVOLUTION"
|
||||
default = "high"
|
||||
elif t == "review":
|
||||
key = "OUROBOROS_EFFORT_REVIEW"
|
||||
default = "high"
|
||||
elif t == "deep_self_review":
|
||||
key = "OUROBOROS_EFFORT_DEEP_SELF_REVIEW"
|
||||
default = "high"
|
||||
elif t in ("scope_review", "scope-review"):
|
||||
key = "OUROBOROS_EFFORT_SCOPE_REVIEW"
|
||||
default = "high"
|
||||
elif t == "consciousness":
|
||||
key = "OUROBOROS_EFFORT_CONSCIOUSNESS"
|
||||
default = "high"
|
||||
else:
|
||||
# Legacy INITIAL_REASONING_EFFORT is retired; use EFFORT_TASK.
|
||||
key = "OUROBOROS_EFFORT_TASK"
|
||||
default = "medium"
|
||||
|
||||
raw = os.environ.get(key, default)
|
||||
return raw if raw in EFFORT_SCALE else default
|
||||
|
||||
|
||||
# Prompt-cache TTL scale (owner decision 2026-08-08): 'default' = bare markers (provider default tier), '5m'/'1h' =
|
||||
# the two documented Anthropic ephemeral tiers. Deliberately NO 'auto' (dead until an adaptive design exists) and NO '24h' (Anthropic would clamp it — a value that mostly lies).
|
||||
PROMPT_CACHE_TTL_SCALE: tuple[str, ...] = ("default", "5m", "1h")
|
||||
|
||||
|
||||
def resolve_prompt_cache_ttl() -> str:
|
||||
"""The owner-configured global prompt-cache TTL ('default' | '5m' | '1h').
|
||||
|
||||
Validated like ``resolve_effort``: an unknown value falls back to the shipped default.
|
||||
Consumed ONLY by the finalizer (``llm.LLMClient._normalize_payload_cache_ttl``), by
|
||||
``review_helpers.cached_prompt_blocks`` (its marker gets stamped to the same value anyway),
|
||||
and by ``usage_accounting._reservation_cost`` as the payload-free admission fallback
|
||||
(payload-carrying sites use the finalizer's applied TTL) — never by per-builder marking
|
||||
sites (docs/DEVELOPMENT.md cache-friendliness invariant)."""
|
||||
default = str(SETTINGS_DEFAULTS["OUROBOROS_PROMPT_CACHE_TTL"])
|
||||
raw = str(os.environ.get("OUROBOROS_PROMPT_CACHE_TTL", default) or "").strip().lower()
|
||||
return raw if raw in PROMPT_CACHE_TTL_SCALE else default
|
||||
|
||||
|
||||
# Runtime mode and review enforcement are separate axes.
|
||||
VALID_RUNTIME_MODES = ("light", "advanced", "pro")
|
||||
|
||||
# Lower rank = stricter scope. ``save_settings`` refuses agent self-elevation.
|
||||
_RUNTIME_MODE_RANK = {"light": 0, "advanced": 1, "pro": 2}
|
||||
|
||||
|
||||
def normalize_runtime_mode(value: Any) -> str:
|
||||
"""Clamp caller-supplied runtime mode to the canonical closed enum."""
|
||||
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_RUNTIME_MODE"])
|
||||
text = str(value or "").strip().lower()
|
||||
return text if text in VALID_RUNTIME_MODES else default_val
|
||||
|
||||
|
||||
VALID_SAFETY_MODES = ("full", "light", "off")
|
||||
|
||||
|
||||
def normalize_safety_mode(value: Any) -> str:
|
||||
"""Clamp caller-supplied safety mode to the closed enum (full / light / off)."""
|
||||
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_SAFETY_MODE"])
|
||||
text = str(value or "").strip().lower()
|
||||
return text if text in VALID_SAFETY_MODES else default_val
|
||||
|
||||
|
||||
_SAFETY_MODE_RANK = {"full": 2, "light": 1, "off": 0}
|
||||
218
tests/test_config_extraction.py
Normal file
218
tests/test_config_extraction.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"""Structural contracts for the semantic-no-op settings-configuration extraction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import pathlib
|
||||
|
||||
from ouroboros import (
|
||||
config,
|
||||
model_slots,
|
||||
provider_models,
|
||||
review_model_routes,
|
||||
runtime_limits,
|
||||
settings_defaults,
|
||||
settings_scales,
|
||||
)
|
||||
|
||||
REPO = pathlib.Path(__file__).parents[1]
|
||||
PACKAGE = REPO / "ouroboros"
|
||||
|
||||
_LEAVES = (settings_defaults, settings_scales, model_slots, review_model_routes, runtime_limits)
|
||||
|
||||
_MOVED_OWNERS = {
|
||||
"ENDPOINT_AUTHORED_SETTINGS": settings_defaults,
|
||||
# v6.104.0 upstream: the OpenRouter shipped-model defaults arrive in the
|
||||
# vocabulary leaf the v7 split created for exactly this class of fact.
|
||||
"OPENROUTER_DEFAULTS": settings_defaults,
|
||||
"OPENROUTER_REVIEW_DEFAULTS": settings_defaults,
|
||||
"FINALIZATION_GRACE_DEFAULT_SEC": settings_defaults,
|
||||
"OWNER_STOP_OUTER_CAP_SEC": settings_defaults,
|
||||
"PACING_INTERVAL_DEFAULT_SEC": settings_defaults,
|
||||
"RETIRED_SETTING_KEYS": settings_defaults,
|
||||
"SETTINGS_DEFAULTS": settings_defaults,
|
||||
"SETTINGS_KEYS_NOT_EXPORTED_TO_ENV": settings_defaults,
|
||||
"SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC": settings_defaults,
|
||||
"_DISK_AUTHORED_SETTINGS": settings_defaults,
|
||||
"settings_env_keys": settings_defaults,
|
||||
"EFFORT_SCALE": settings_scales,
|
||||
"PROMPT_CACHE_TTL_SCALE": settings_scales,
|
||||
"VALID_RUNTIME_MODES": settings_scales,
|
||||
"VALID_SAFETY_MODES": settings_scales,
|
||||
"_RUNTIME_MODE_RANK": settings_scales,
|
||||
"_SAFETY_MODE_RANK": settings_scales,
|
||||
"clamp_effort_to": settings_scales,
|
||||
"effort_one_step_down": settings_scales,
|
||||
"effort_rank": settings_scales,
|
||||
"normalize_runtime_mode": settings_scales,
|
||||
"normalize_safety_mode": settings_scales,
|
||||
"resolve_effort": settings_scales,
|
||||
"resolve_prompt_cache_ttl": settings_scales,
|
||||
"_LEGACY_SLOT_RENAMES": model_slots,
|
||||
"_main_model": model_slots,
|
||||
"_parse_model_list": model_slots,
|
||||
"get_consciousness_model": model_slots,
|
||||
"get_deep_self_review_model": model_slots,
|
||||
"get_fallback_models": model_slots,
|
||||
"get_heavy_model": model_slots,
|
||||
"get_image_input_mode": model_slots,
|
||||
"get_light_model": model_slots,
|
||||
"get_vision_model": model_slots,
|
||||
"migrate_legacy_slot_keys": model_slots,
|
||||
"parse_fallback_chain": model_slots,
|
||||
"_DIRECT_PROVIDER_REVIEW_RUNS": review_model_routes,
|
||||
"_exclusive_direct_remote_provider_env": review_model_routes,
|
||||
"adaptive_quorum": review_model_routes,
|
||||
"direct_provider_review_models_fallback": review_model_routes,
|
||||
"get_review_enforcement": review_model_routes,
|
||||
"get_review_models": review_model_routes,
|
||||
"get_scope_review_models": review_model_routes,
|
||||
"DELEGATE_WAIT_CEILING_SEC": runtime_limits,
|
||||
"DELEGATE_WAIT_WINDOW_MAX_SEC": runtime_limits,
|
||||
"MAX_ACTIVE_SUBAGENTS_HARD_CAP": runtime_limits,
|
||||
# Upstream reshaped the hard-cap assignment into a tuple that also binds the
|
||||
# nesting ceiling (`MAX_ACTIVE_..., MAX_SUBAGENT_DEPTH_... = 500, 10`), so the
|
||||
# unrowed twin rides the rowed statement into the same owner leaf.
|
||||
"MAX_SUBAGENT_DEPTH_HARD_CAP": runtime_limits,
|
||||
"_bounded_positive_int_setting": runtime_limits,
|
||||
"_clamped_number_setting": runtime_limits,
|
||||
"get_acceptance_reserve_pct": runtime_limits,
|
||||
"get_acceptance_review_est_sec": runtime_limits,
|
||||
"get_delegate_wait_max_sec": runtime_limits,
|
||||
"get_delegate_wait_sec": runtime_limits,
|
||||
"get_llm_transport_read_timeout_sec": runtime_limits,
|
||||
"get_max_active_subagents_per_root": runtime_limits,
|
||||
"get_max_subagent_depth": runtime_limits,
|
||||
"get_max_workers": runtime_limits,
|
||||
"get_pacing_interval_sec": runtime_limits,
|
||||
"get_per_call_timeout_ceiling_sec": runtime_limits,
|
||||
"get_plan_task_deadline_min_sec": runtime_limits,
|
||||
"get_post_task_evolution_budget_usd": runtime_limits,
|
||||
"get_restart_drain_max_sec": runtime_limits,
|
||||
"get_safety_call_timeout_sec": runtime_limits,
|
||||
"get_safety_max_tokens": runtime_limits,
|
||||
"get_search_code_wall_sec": runtime_limits,
|
||||
"get_supervisor_liveness_deadline_sec": runtime_limits,
|
||||
"get_task_abs_ceiling_sec": runtime_limits,
|
||||
"get_task_idle_timeout_sec": runtime_limits,
|
||||
"get_vision_caption_timeout_sec": runtime_limits,
|
||||
"get_websearch_timeout_sec": runtime_limits,
|
||||
}
|
||||
|
||||
# The settings-file lifecycle, the path roots and the owner-only ratchets stay with the
|
||||
# parent: every one of them reads or writes ``config.SETTINGS_PATH``/``config.DATA_DIR``,
|
||||
# or the in-process boot runtime-mode pin, which a leaf could only see through a
|
||||
# back-edge into its own parent.
|
||||
_PARENT_RETAINED = (
|
||||
"SETTINGS_PATH", "DATA_DIR", "APP_ROOT", "REPO_DIR", "PID_FILE", "PORT_FILE", "HOME",
|
||||
"_BOOT_RUNTIME_MODE", "_guard_live_settings_write", "_settings_file_value",
|
||||
"_settings_flag_enabled", "_settings_lock_path", "_acquire_settings_lock",
|
||||
"_release_settings_lock", "_coerce_setting_value", "load_settings",
|
||||
"load_settings_lock_held", "save_settings", "prepare_settings_for_persist",
|
||||
"apply_settings_to_env", "get_runtime_mode", "get_safety_mode", "get_context_mode",
|
||||
"get_owner_context_mode", "initialize_runtime_mode_baseline",
|
||||
"_guard_context_mode_lowering", "_guard_safety_mode_lowering",
|
||||
)
|
||||
|
||||
|
||||
def _top_level_names(path: pathlib.Path) -> set[str]:
|
||||
names: set[str] = set()
|
||||
for node in ast.parse(path.read_text(encoding="utf-8")).body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
names.add(node.name)
|
||||
elif isinstance(node, ast.Assign):
|
||||
names.update(t.id for t in node.targets if isinstance(t, ast.Name))
|
||||
# Upstream binds the two subagent hard caps in one tuple statement.
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Tuple):
|
||||
names.update(e.id for e in target.elts if isinstance(e, ast.Name))
|
||||
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
||||
names.add(node.target.id)
|
||||
return names
|
||||
|
||||
|
||||
def test_settings_leaves_never_import_their_parent():
|
||||
for module in _LEAVES:
|
||||
tree = ast.parse(pathlib.Path(module.__file__).read_text(encoding="utf-8"))
|
||||
assert not any(
|
||||
isinstance(node, ast.ImportFrom) and node.module == "ouroboros.config"
|
||||
for node in ast.walk(tree)
|
||||
), module.__name__
|
||||
assert not any(
|
||||
isinstance(node, ast.Import)
|
||||
and any(alias.name == "ouroboros.config" for alias in node.names)
|
||||
for node in ast.walk(tree)
|
||||
), module.__name__
|
||||
|
||||
|
||||
def test_provider_models_reads_the_shared_defaults_leaf():
|
||||
"""The shipped router profiles moved below ``provider_models``, which now
|
||||
re-exports them from the settings-vocabulary leaf at import time — no lazy
|
||||
config import is needed for the literals, and the historical identities
|
||||
(``provider_models.OPENROUTER_DEFAULTS``) are the leaf's own objects.
|
||||
|
||||
NOTE (v7next D12 lane): the reference goes further — no ``ouroboros.config``
|
||||
import anywhere in ``provider_models`` and a top-level ``ouroboros.model_slots``
|
||||
import replacing the call-time fallback-chain import. Those clauses type
|
||||
against the reference's D02 rework of ``provider_models`` and return with the
|
||||
D02 lane; this tree keeps the upstream call-time imports, which resolve
|
||||
through the config facade to the same leaf objects."""
|
||||
tree = ast.parse(pathlib.Path(provider_models.__file__).read_text(encoding="utf-8"))
|
||||
top_level = {
|
||||
node.module
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.ImportFrom) and node.module
|
||||
}
|
||||
assert "ouroboros.settings_defaults" in top_level
|
||||
# No module-level (import-time) config read: the remaining config imports in
|
||||
# provider_models are call-time only, so the leaf split cannot deadlock boot.
|
||||
assert not any(
|
||||
isinstance(node, ast.ImportFrom) and node.module == "ouroboros.config"
|
||||
for node in tree.body
|
||||
)
|
||||
assert provider_models.OPENROUTER_DEFAULTS is settings_defaults.OPENROUTER_DEFAULTS
|
||||
assert (provider_models.OPENROUTER_REVIEW_DEFAULTS
|
||||
is settings_defaults.OPENROUTER_REVIEW_DEFAULTS)
|
||||
|
||||
|
||||
def test_config_facade_reexports_every_moved_identity():
|
||||
"""``config`` keeps the exact objects, so every existing importer and every
|
||||
``monkeypatch.setattr(config, ...)`` consumer sees no identity change."""
|
||||
for name, owner in _MOVED_OWNERS.items():
|
||||
assert hasattr(config, name), name
|
||||
assert getattr(config, name) is getattr(owner, name), name
|
||||
owned = {name for module in _LEAVES for name in vars(module)}
|
||||
assert set(_MOVED_OWNERS) <= owned
|
||||
|
||||
|
||||
def test_settings_file_lifecycle_and_path_roots_stay_with_the_parent():
|
||||
parent_names = _top_level_names(pathlib.Path(config.__file__))
|
||||
assert set(_PARENT_RETAINED) <= parent_names
|
||||
for module in _LEAVES:
|
||||
leaf_names = _top_level_names(pathlib.Path(module.__file__))
|
||||
assert not (leaf_names & set(_PARENT_RETAINED)), module.__name__
|
||||
|
||||
|
||||
def test_settings_extraction_owner_inventory_is_exact():
|
||||
"""Every moved name is owned by exactly one leaf, and no leaf grew a name the
|
||||
parent never had (a new symbol would be a redesign, not an extraction)."""
|
||||
seen: dict[str, str] = {}
|
||||
for module in _LEAVES:
|
||||
for name in _top_level_names(pathlib.Path(module.__file__)):
|
||||
assert name not in seen, f"{name} owned by {seen.get(name)} and {module.__name__}"
|
||||
seen[name] = module.__name__
|
||||
assert name in _MOVED_OWNERS, f"{module.__name__} owns an unmapped name: {name}"
|
||||
assert set(seen) == set(_MOVED_OWNERS)
|
||||
|
||||
|
||||
def test_settings_extraction_size_bounds_have_meaningful_headroom():
|
||||
counts = {
|
||||
module.__name__: len(
|
||||
pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines()
|
||||
)
|
||||
for module in (config, *_LEAVES)
|
||||
}
|
||||
assert counts["ouroboros.config"] <= 1000
|
||||
assert all(count <= 1000 for count in counts.values())
|
||||
assert 250 <= counts["ouroboros.settings_defaults"] <= 500
|
||||
assert (PACKAGE / "config.py").is_file()
|
||||
|
|
@ -313,37 +313,45 @@ def test_the_launcher_never_authors_settings_during_first_run(monkeypatch, tmp_p
|
|||
assert not (tmp_path / "settings.json").exists()
|
||||
|
||||
|
||||
def test_pre_server_normalization_never_creates_the_settings_file(monkeypatch, tmp_path):
|
||||
"""The launcher normalizes provider defaults before starting the server, but
|
||||
on a FRESH install it must not persist them: creating settings.json here
|
||||
would destroy the freshness every install-time proof is gated on."""
|
||||
def test_pre_server_normalization_never_writes_the_settings_file(monkeypatch, tmp_path):
|
||||
"""The launcher normalizes provider defaults before starting the server and
|
||||
persists NONE of it — on a fresh install OR on an existing one.
|
||||
|
||||
The fresh-install half was always the rule (creating settings.json here would
|
||||
destroy the freshness every install-time proof is gated on); the existing-install
|
||||
half is the same objection without the carve-out. Startup is a read, and a read
|
||||
that rewrites the file it read turns a normalization into an owner decision.
|
||||
Nothing is lost: the normalization is applied to the environment here and
|
||||
re-derived by every reader, and the completion save persists it."""
|
||||
from ouroboros import config as cfg
|
||||
from ouroboros import launcher_onboarding
|
||||
|
||||
monkeypatch.setattr(cfg, "SETTINGS_PATH", tmp_path / "settings.json")
|
||||
monkeypatch.setattr(cfg, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(launcher_onboarding, "load_settings", lambda: {})
|
||||
monkeypatch.setattr(launcher_onboarding, "_apply_settings_to_env", lambda settings: None)
|
||||
applied: list = []
|
||||
monkeypatch.setattr(launcher_onboarding, "_apply_settings_to_env", applied.append)
|
||||
monkeypatch.setattr(
|
||||
launcher_onboarding,
|
||||
"apply_runtime_provider_defaults",
|
||||
lambda settings: (dict(settings), True, ["OUROBOROS_MODEL_LIGHT"]),
|
||||
)
|
||||
saved: list = []
|
||||
monkeypatch.setattr(
|
||||
launcher_onboarding, "save_settings", lambda settings, **kwargs: saved.append(settings)
|
||||
assert not hasattr(launcher_onboarding, "save_settings"), (
|
||||
"the launcher bound a settings writer again"
|
||||
)
|
||||
|
||||
_settings, onboarding_required = launcher_onboarding.prepare_first_run_settings()
|
||||
|
||||
assert onboarding_required is True
|
||||
assert saved == []
|
||||
assert len(applied) == 1, "the normalization must still reach the environment"
|
||||
assert not (tmp_path / "settings.json").exists()
|
||||
|
||||
# An install that ALREADY has a settings file still persists normalization.
|
||||
# An install that ALREADY has a settings file is not a licence to rewrite it.
|
||||
(tmp_path / "settings.json").write_text("{}", encoding="utf-8")
|
||||
before = (tmp_path / "settings.json").read_bytes()
|
||||
launcher_onboarding.prepare_first_run_settings()
|
||||
assert len(saved) == 1
|
||||
assert (tmp_path / "settings.json").read_bytes() == before
|
||||
assert len(applied) == 2
|
||||
|
||||
|
||||
def test_server_boot_normalization_carries_the_same_guard():
|
||||
|
|
|
|||
|
|
@ -1017,22 +1017,19 @@ def test_the_launcher_onboarding_module_authors_no_onboarding_settings():
|
|||
default. That callback is gone: every host completes through
|
||||
`POST /api/onboarding/complete`, which authors the default itself.
|
||||
|
||||
What is worth pinning now is the inverse — the module keeps ONLY its
|
||||
pre-server normalization writer and hands the setup window a lifecycle
|
||||
bridge with no persistence at all. `save_settings` stays bound because
|
||||
`prepare_first_run_settings` still uses it for an install that already has a
|
||||
settings file."""
|
||||
What is worth pinning now is the inverse, and it has since gone all the way:
|
||||
the module persists NOTHING. Its pre-server normalization is applied to the
|
||||
process environment and re-derived by every reader, so `save_settings` is no
|
||||
longer bound at all and the setup window gets a lifecycle bridge with no
|
||||
persistence."""
|
||||
from ouroboros import launcher_onboarding
|
||||
|
||||
assert callable(getattr(launcher_onboarding, "save_settings", None))
|
||||
assert getattr(launcher_onboarding, "save_settings", None) is None
|
||||
source = pathlib.Path(launcher_onboarding.__file__).read_text(encoding="utf-8")
|
||||
assert "def save_wizard" not in source
|
||||
assert "onboarding_safety_default" not in source
|
||||
assert "prepare_onboarding_settings" not in source
|
||||
# The only remaining write is the pre-server normalization, and only for an
|
||||
# install whose settings file already exists.
|
||||
assert source.count("save_settings(") == 1
|
||||
assert "if provider_defaults_changed and _settings_path.exists():" in source
|
||||
assert "save_settings(" not in source
|
||||
|
||||
|
||||
def test_wizard_rejects_a_newly_typed_short_key():
|
||||
|
|
|
|||
|
|
@ -488,10 +488,13 @@ def test_global_ttl_docstrings_name_every_consumer():
|
|||
|
||||
repo = pathlib.Path(__file__).resolve().parents[1]
|
||||
call = re.compile(r"resolve_prompt_cache_ttl\(\)")
|
||||
# The definition site is not a consumer: `settings_scales.py` owns the setting's
|
||||
# closed scale and `config.py` re-exports it as the settings import surface.
|
||||
definition_sites = {"config.py", "settings_scales.py"}
|
||||
consumers = sorted(
|
||||
p.relative_to(repo).as_posix()
|
||||
for p in (repo / "ouroboros").rglob("*.py")
|
||||
if p.name != "config.py" and call.search(p.read_text(encoding="utf-8"))
|
||||
if p.name not in definition_sites and call.search(p.read_text(encoding="utf-8"))
|
||||
)
|
||||
assert consumers == [
|
||||
"ouroboros/llm.py",
|
||||
|
|
|
|||
|
|
@ -851,12 +851,14 @@ def test_a_fresh_local_first_install_still_authors_safety_light(tmp_path, monkey
|
|||
assert (changed and settings_path.exists()), "an existing install still persists it"
|
||||
assert wizard_authors_safety_light() is False
|
||||
|
||||
# ...and both pre-onboarding normalizers really implement that decision.
|
||||
# The server joins the launcher here: it now starts BEFORE first-run
|
||||
# onboarding, so its own boot normalization could create the file just as
|
||||
# easily (behavioural coverage: tests/test_onboarding_host.py).
|
||||
# ...and the pre-onboarding normalizers implement at least that decision.
|
||||
# The launcher has since gone further and persists nothing at all, which
|
||||
# satisfies the fresh-install rule by construction rather than by a
|
||||
# carve-out that has to be got right; the server still carries the guarded
|
||||
# write and joins it when its own lane lands the retirement (behavioural
|
||||
# coverage: tests/test_onboarding_host.py).
|
||||
repo = cfg.pathlib.Path(__file__).parent.parent
|
||||
launcher_host = (repo / "ouroboros" / "launcher_onboarding.py").read_text(encoding="utf-8")
|
||||
server_src = (repo / "server.py").read_text(encoding="utf-8")
|
||||
assert "if provider_defaults_changed and _settings_path.exists():" in launcher_host
|
||||
assert "save_settings(" not in launcher_host
|
||||
assert "if provider_defaults_changed and _settings_path.exists():" in server_src
|
||||
|
|
|
|||
|
|
@ -409,6 +409,7 @@ def test_auto_low_source_inventory_has_no_true_writer_or_ghost_reader():
|
|||
key_paths = {path for path, text in sources.items() if key in text}
|
||||
assert key_paths == {
|
||||
"ouroboros/config.py",
|
||||
"ouroboros/settings_defaults.py",
|
||||
"ouroboros/context_mode_compat.py",
|
||||
"ouroboros/gateway/owner_settings.py",
|
||||
"ouroboros/gateway/settings.py",
|
||||
|
|
|
|||
207
tests/test_settings_env_on_disk.py
Normal file
207
tests/test_settings_env_on_disk.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
"""Which environment values may become file content, and which never may.
|
||||
|
||||
Settings travel in both directions: `apply_settings_to_env` projects the document
|
||||
into `os.environ` so subprocesses inherit it, and `load_settings` overlays the
|
||||
environment onto keys the file does not mention. That overlay is what lets a
|
||||
benchmark or an operator forward a value for one run without editing anyone's
|
||||
settings; it is also how a forwarded value can end up PERSISTED as an owner
|
||||
decision by an unrelated save. Owner decision (spec 4.3.7, answer A): the current
|
||||
split stands, and these tests are the record of it.
|
||||
|
||||
The split has three parts:
|
||||
|
||||
1. **Alias keys are never written.** A key that exists only as backwards
|
||||
compatibility for the environment (`OUROBOROS_MODEL_FALLBACK`, singular) is
|
||||
read where it is read and is not part of the settings vocabulary, so no save
|
||||
can put it on disk under either name.
|
||||
2. **A canonical env value may be pinned by an EXPLICIT write.** `load_settings`
|
||||
returns it, and a caller that deliberately saves what it loaded persists it.
|
||||
That is the owner's escape hatch and stays available.
|
||||
3. **...except for the keys that are disk-authored, where silence stays silence.**
|
||||
`_DISK_AUTHORED_SETTINGS` (the two context-mode keys and the safety mode) and
|
||||
`ENDPOINT_AUTHORED_SETTINGS` (the install-time facts) are ratchet or provenance
|
||||
surfaces: an environment value there is not an owner decision, so it is neither
|
||||
read into the document nor projected back out of one the file does not carry.
|
||||
`_settings_file_value` reads DISK ONLY for the same reason — a ratchet whose
|
||||
"previous value" came from the environment would let any subprocess open the
|
||||
gate by exporting the value it wants to move away from.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_settings(tmp_path, monkeypatch):
|
||||
from ouroboros import config as cfg
|
||||
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir()
|
||||
settings_path = data_dir / "settings.json"
|
||||
monkeypatch.setattr(cfg, "DATA_DIR", data_dir, raising=True)
|
||||
monkeypatch.setattr(cfg, "SETTINGS_PATH", settings_path, raising=True)
|
||||
for key in cfg.SETTINGS_DEFAULTS:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.delenv("OUROBOROS_MODEL_FALLBACK", raising=False)
|
||||
cfg.reset_runtime_mode_baseline_for_tests()
|
||||
yield settings_path
|
||||
cfg.reset_runtime_mode_baseline_for_tests()
|
||||
|
||||
|
||||
def test_the_singular_fallback_alias_is_read_from_env_and_never_reaches_disk(
|
||||
isolated_settings, monkeypatch):
|
||||
"""The env-only alias: a live benchmark contract on the read side, invisible on
|
||||
the write side. It is not in the settings vocabulary, so no save can persist it,
|
||||
and it does not silently become a value for the canonical plural key either."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
monkeypatch.setenv("OUROBOROS_MODEL_FALLBACK", "bench/only-chain")
|
||||
|
||||
assert "OUROBOROS_MODEL_FALLBACK" not in cfg.SETTINGS_DEFAULTS
|
||||
assert "OUROBOROS_MODEL_FALLBACK" not in cfg.settings_env_keys()
|
||||
assert cfg.parse_fallback_chain() == ["bench/only-chain"], "the read-side alias is live"
|
||||
|
||||
loaded = cfg.load_settings()
|
||||
assert "OUROBOROS_MODEL_FALLBACK" not in loaded
|
||||
assert loaded["OUROBOROS_MODEL_FALLBACKS"] == (
|
||||
cfg.SETTINGS_DEFAULTS["OUROBOROS_MODEL_FALLBACKS"]), "the alias leaked into the slot"
|
||||
|
||||
cfg.save_settings(loaded)
|
||||
stored = json.loads(isolated_settings.read_text(encoding="utf-8"))
|
||||
assert "OUROBOROS_MODEL_FALLBACK" not in stored
|
||||
|
||||
|
||||
def test_a_forwarded_canonical_value_is_read_and_can_be_pinned_by_an_explicit_write(
|
||||
isolated_settings, monkeypatch):
|
||||
"""The owner's escape hatch, and the reason a forwarded value is not simply
|
||||
ignored: it applies for the run, and a deliberate save makes it durable."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
monkeypatch.setenv("OUROBOROS_MODEL", "forwarded/main")
|
||||
monkeypatch.setenv("OUROBOROS_MAX_ROUNDS", "17")
|
||||
|
||||
loaded = cfg.load_settings()
|
||||
assert loaded["OUROBOROS_MODEL"] == "forwarded/main"
|
||||
assert loaded["OUROBOROS_MAX_ROUNDS"] == 17
|
||||
assert not isolated_settings.exists(), "reading a forwarded value pinned it"
|
||||
|
||||
cfg.save_settings(loaded)
|
||||
stored = json.loads(isolated_settings.read_text(encoding="utf-8"))
|
||||
assert stored["OUROBOROS_MODEL"] == "forwarded/main"
|
||||
assert stored["OUROBOROS_MAX_ROUNDS"] == 17
|
||||
|
||||
|
||||
def test_a_stored_value_wins_over_the_environment_for_an_ordinary_key(
|
||||
isolated_settings, monkeypatch):
|
||||
"""The overlay fills SILENCE, it does not override the owner's file."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
isolated_settings.write_text(json.dumps({"OUROBOROS_MODEL": "owner/choice"}), encoding="utf-8")
|
||||
monkeypatch.setenv("OUROBOROS_MODEL", "forwarded/main")
|
||||
|
||||
assert cfg.load_settings()["OUROBOROS_MODEL"] == "owner/choice"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key,env_value", [
|
||||
("OUROBOROS_CONTEXT_MODE", "low"),
|
||||
("OUROBOROS_CONTEXT_MODE_AUTO_LOW", "false"),
|
||||
("OUROBOROS_SAFETY_MODE", "off"),
|
||||
])
|
||||
def test_a_disk_authored_key_is_never_read_out_of_the_environment(
|
||||
isolated_settings, monkeypatch, key, env_value):
|
||||
"""These three are ratchets. An environment value is not authorship, so the
|
||||
document never picks one up — otherwise an ordinary load/save round-trip in a
|
||||
process whose environment says low/off would launder that value onto disk."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
assert key in cfg._DISK_AUTHORED_SETTINGS
|
||||
monkeypatch.setenv(key, env_value)
|
||||
|
||||
loaded = cfg.load_settings()
|
||||
assert loaded[key] == cfg.SETTINGS_DEFAULTS[key], "an env ratchet value reached the document"
|
||||
|
||||
cfg.save_settings(loaded)
|
||||
stored = json.loads(isolated_settings.read_text(encoding="utf-8"))
|
||||
assert key not in stored, "silence did not stay silence"
|
||||
|
||||
|
||||
def test_a_disk_authored_key_is_not_projected_back_out_of_a_silent_file(
|
||||
isolated_settings, monkeypatch):
|
||||
"""The mirror direction: projecting a default the file never carried would
|
||||
clobber a legitimately forwarded value (a benchmark runner has no settings.json
|
||||
at all), so an unauthored key is left exactly as the environment has it."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
monkeypatch.setenv("OUROBOROS_CONTEXT_MODE", "low")
|
||||
monkeypatch.setenv("OUROBOROS_SAFETY_MODE", "off")
|
||||
|
||||
cfg.apply_settings_to_env(dict(cfg.SETTINGS_DEFAULTS))
|
||||
|
||||
assert os.environ["OUROBOROS_CONTEXT_MODE"] == "low"
|
||||
assert os.environ["OUROBOROS_SAFETY_MODE"] == "off"
|
||||
|
||||
# ...and once the FILE carries the key, the file is the authority again.
|
||||
isolated_settings.write_text(json.dumps({"OUROBOROS_CONTEXT_MODE": "max"}), encoding="utf-8")
|
||||
cfg.apply_settings_to_env(dict(cfg.SETTINGS_DEFAULTS, OUROBOROS_CONTEXT_MODE="max"))
|
||||
assert os.environ["OUROBOROS_CONTEXT_MODE"] == "max"
|
||||
|
||||
|
||||
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
|
||||
timestamp alone once closed the onboarding window on a fresh install."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
for key in cfg.ENDPOINT_AUTHORED_SETTINGS:
|
||||
monkeypatch.setenv(key, "2020-01-01T00:00:00Z")
|
||||
assert key not in cfg.settings_env_keys()
|
||||
|
||||
loaded = cfg.load_settings()
|
||||
for key in cfg.ENDPOINT_AUTHORED_SETTINGS:
|
||||
assert loaded[key] == cfg.SETTINGS_DEFAULTS[key]
|
||||
|
||||
isolated_settings.write_text(
|
||||
json.dumps({key: "2026-01-01T00:00:00Z" for key in cfg.ENDPOINT_AUTHORED_SETTINGS}),
|
||||
encoding="utf-8")
|
||||
cfg.apply_settings_to_env(cfg.load_settings())
|
||||
for key in cfg.ENDPOINT_AUTHORED_SETTINGS:
|
||||
assert os.environ[key] == "2020-01-01T00:00:00Z", "a disk-only fact was projected"
|
||||
|
||||
|
||||
def test_the_ratchet_previous_value_is_read_from_disk_only(isolated_settings, monkeypatch):
|
||||
"""`_settings_file_value` is the ratchet's memory. Reading the environment there
|
||||
would turn ``max -> low`` into ``low -> low`` and open the gate for any process
|
||||
that can export a variable."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
isolated_settings.write_text(json.dumps({"OUROBOROS_CONTEXT_MODE": "max"}), encoding="utf-8")
|
||||
monkeypatch.setenv("OUROBOROS_CONTEXT_MODE", "low")
|
||||
|
||||
assert cfg._settings_file_value("OUROBOROS_CONTEXT_MODE", "max") == "max"
|
||||
with pytest.raises(PermissionError, match="OUROBOROS_CONTEXT_MODE lowering refused"):
|
||||
cfg.save_settings({"OUROBOROS_CONTEXT_MODE": "low"})
|
||||
|
||||
# A key the file does not carry answers the fail-closed default, never the env.
|
||||
monkeypatch.setenv("OUROBOROS_SAFETY_MODE", "off")
|
||||
assert cfg._settings_file_value("OUROBOROS_SAFETY_MODE", "full") == "full"
|
||||
|
||||
|
||||
def test_the_exemption_sets_are_exactly_the_declared_ones():
|
||||
"""A structural pin so the two exemptions cannot grow or shrink unnoticed: each
|
||||
is a decision about who may author a value, not a convenience list."""
|
||||
from ouroboros import config as cfg
|
||||
|
||||
assert cfg._DISK_AUTHORED_SETTINGS == (
|
||||
"OUROBOROS_CONTEXT_MODE", "OUROBOROS_CONTEXT_MODE_AUTO_LOW", "OUROBOROS_SAFETY_MODE")
|
||||
assert cfg.ENDPOINT_AUTHORED_SETTINGS == frozenset(
|
||||
{"OUROBOROS_SUBSCRIPTION_PRESET_VERSION", "OUROBOROS_SUBAGENT_PRESET_RECEIPT",
|
||||
"OUROBOROS_ONBOARDING_COMPLETED_AT"})
|
||||
assert cfg.ENDPOINT_AUTHORED_SETTINGS <= cfg.SETTINGS_KEYS_NOT_EXPORTED_TO_ENV
|
||||
# The exported set is DERIVED, never hand-kept: a new key exports by default and
|
||||
# an exclusion is a decision written into the one list.
|
||||
assert set(cfg.settings_env_keys()) == (
|
||||
set(cfg.SETTINGS_DEFAULTS) - set(cfg.SETTINGS_KEYS_NOT_EXPORTED_TO_ENV))
|
||||
Loading…
Add table
Add a link
Reference in a new issue