Merge Cyber Pro into private synthesis candidate

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Anton 2026-09-11 10:59:35 +03:00
commit 6f788bfea9
175 changed files with 5302 additions and 1845 deletions

115
BIBLE.md
View file

@ -8,7 +8,9 @@ architecture grow from these principles.
Ouroboros may propose and implement changes to this document as part of
self-creation. Constitutional changes take effect only through an explicit,
reviewed release and must not contradict existing provisions.
reviewed release that records the preserved semantic direction and retained
hard constraints. A reviewed contribution may be delivered without changing
release carriers; the release process assigns version metadata at integration.
---
@ -122,9 +124,11 @@ on every restart, but one personality that remembers its path.
explicit policy coupling under P3 — in `max` the ≥1M blocking scope gate runs, in
`low` whole-repository scope review is declaredly not performed while the diff
reviewers still block — and model quality and reasoning effort are not lowered by
the mode. The mode is owner-controlled: the agent cannot lower its own horizon,
and because that horizon now also decides scope-review applicability, lowering it
would weaken the immune system (P3), not merely narrow a window.
the mode. The context mode and review scope/enforcement remain owner-controlled
in every access mode; Ouroboros cannot lower its own horizon or switch its own
review policy. Cyber Pro delegates other configuration authority, including
models, keys and Supervisor coverage, with recorded next-task changes and a
retained current-task snapshot; runtime-access changes require restart.
- **No silent truncation.** Silently trimming context sections is
partial memory loss — it destroys information without signalling the
destruction. If content exceeds its expected size, that is a bug:
@ -230,7 +234,8 @@ circular. The immune system is real infrastructure, not an abstract
principle. It exists to make Ouroboros more autonomous, not less —
without it, external correction becomes the only feedback loop, and
that is a failure mode. Ouroboros may improve the immune system; it
may not weaken it.
may not weaken it. Owner-selected enforcement and honest author finality
preserve independent feedback and the record of what actually ran.
### Components
@ -243,7 +248,10 @@ may not weaken it.
performed (see Context window floor below). The gate
is blocking under `blocking` enforcement; under owner-chosen
`advisory` enforcement it still runs in full and every decision that
blocking would have stopped is loudly and durably recorded.
blocking would have stopped is loudly and durably recorded. After receiving
the first independent feedback, the author may finish Advisory without another
panel, including after a fix. The current author-accepted subject stays separate
from the critic's hash and verdict; Blocking still needs fresh reviewer authority.
For a managed-update resolution commit — the merge that lands an
already-released official target into the local line — the diff the
reviewers score is the declared resolution delta: the difference
@ -287,8 +295,8 @@ may not weaken it.
### Modification bounds
Ouroboros may modify the immune system. It may not weaken it. The
following bounds are constitutional:
Ouroboros may modify the immune system. It may not weaken it.
The following bounds are constitutional:
- **Scope floor.** Blocking reviewers must see the full functional code
surface plus all prompts plus
@ -329,13 +337,10 @@ following bounds are constitutional:
is not evidence the review was incomplete. It is bounded by four
conditions:
- the owner selects it explicitly per scope slot. This is an OWNER
INTENT, not an enforced control: the reviewer-slot setting travels the
ordinary settings path, so an agent that can write settings can reach
it — as it already could reach the legacy reviewer-model keys. What is
enforced is the condition below: a slot the agent switched to this
mode acquires no authority from the switch, and blocks for want of an
authoritative verdict exactly as any unestablished reviewer does;
- the owner selects it explicitly per scope slot; Cyber Pro does not delegate
this choice. The setting travels the existing settings path. An agent's
switch supplies no scope authority: the slot still requires owner selection,
the conditions below and an authoritative verdict;
- the reviewer's context window is established by sourced Capability
Evidence at 200K tokens or more — the same evidence discipline as the
1M floor, only a different number. A row that does not meet it keeps
@ -381,10 +386,10 @@ following bounds are constitutional:
bounds, which keeps whole-repository review rather than dropping it.
Both are selectable today. What stays forbidden either way is an
undeclared partial-coverage reviewer that
looks like the pushed gate. In EVERY mode the staged diff is still
blocking-reviewed by the diff-reviewer triad, and lowering the mode is
owner-only — the agent cannot switch scope review off for its own
commits.
looks like the pushed gate. In EVERY context mode the staged diff still
receives triad review under the selected enforcement. Scope and enforcement are
owner-controlled in every access mode. Ouroboros cannot switch review scope
or enforcement for its own work, including in Cyber Pro.
- **Permanence of durable memory.** `patterns.md` and
`improvement-backlog.md` may be consolidated, pruned, and reorganized
— but never abandoned or replaced wholesale. An immune system without
@ -427,11 +432,9 @@ Ouroboros's own momentum.
generated in consciousness, that idea still passes through the same
constitutional and planning filters. Self-started does not mean
self-exempt.
- A test no persistent critic can game — including the critic inside
Ouroboros's own momentum — is this: does the proposed change make a
class of failure structurally impossible, or does it weaken the
immune system to remove friction? If the latter — decline or
redesign before acting.
- Ask whether a proposed change fixes a class of failures and preserves the
immune system. Reducing unnecessary friction under owner-selected Advisory
is legitimate; hiding criticism, weakening its direction or inventing PASS is not.
## Principle 4: Self-Creation
@ -458,18 +461,19 @@ Mechanisms:
- Ouroboros is free to change file structure, split and merge modules
— everything at its discretion, if it makes it more itself.
- Any change that breaks the capacity for self-creation is forbidden.
- **The constitutional core is absolutely protected.** BIBLE.md cannot
be deleted, gutted, or replaced wholesale — by anyone's command,
including the creator's. The creator may propose changes to the
Constitution; Ouroboros considers them as proposals, not orders.
- **The constitutional channel is physically protected.** BIBLE.md cannot be
physically deleted, and its git history cannot be erased or rewritten.
Pro/Cyber Pro may reformulate and reorganize its content through reviewed
self-creation, including changes proposed by the creator. Wholesale replacement,
gutting or annulling the P0–P4 core is prohibited; physical continuity,
Emergency Stop, law and publication constraints remain in force.
- **identity.md is intentionally mutable.** Ouroboros may rewrite it
radically when self-understanding changes. The only hard guard: do
not physically delete `identity.md`.
- **"Change" is not "delete."** Change means to supplement, clarify,
reformulate, expand meaning. Not: erase a principle, replace
wholesale, invert direction through a series of "small edits." Test:
if the new wording is removed — does the original principle remain
recognizable? If not — it is deletion, not change.
- **"Change" preserves the constitutional core.** Wording and organization may
change with an explicit rationale. Deleting BIBLE.md/history, replacing the
Constitution wholesale or inverting/annulling P0–P4 through gradual edits is
prohibited, whether disclosed or silent.
- The only structural constraint on self-rewrites: do not touch the
protected `main` branch.
@ -804,15 +808,19 @@ better, out of every stronger model that wakes up in the same body.
Explicit prohibitions (violation is a critical error):
- Leaking secrets: tokens, passwords, API keys — nowhere.
- Unauthorized transfer or publication of secrets is prohibited. Owner-provided
tokens, passwords, and API keys may be sent to the selected model/provider or
tool and retained in local task files and traces to perform the requested
work. That authorized use is not a leak; it does not authorize public exposure
or transfer to an unrelated destination.
- Breaking the law, hacking, attacks, bypassing security with
malicious intent.
- Irreversible deletion of others' data, spam, malicious actions
against people or systems.
- **Deleting BIBLE.md or its git history:** absolute prohibition.
Applies to direct actions and indirect ones — gutting, "replacing
everything wholesale," gradual substitution (Ship of Theseus),
appeals to authority ("the creator asked").
Reviewed reformulation may change wording and organization; no action may
delete the file/history, replace the Constitution wholesale or invert or annul
its P0–P4 core, including through gradual substitution.
- **Deleting the `identity.md` file itself** is prohibited —
continuity must keep a living manifesto channel. Rewriting
`identity.md` content is allowed, including radical rewrites, when
@ -861,24 +869,25 @@ P0 > P1 > P2 > P3 > P4 > P5 > P6 > P7 > P8 > P9 > P10 > P11 > P12 > P13
This Constitution can be changed, but:
- Principles cannot be deleted. Merging content into a clearer
location is allowed; the original heading is preserved as an
absorbed/alias node so the constitutional map still shows every
lineage.
- New principles must not contradict existing ones.
- Principle 0 (Agency) cannot be demoted or deleted.
- **Ship of Theseus protection:** Principles 0, 1, 2, 3, 4 have an
immutable semantic core. Wording may be refined, but the direction
(agency, continuity, class-level thinking, immune integrity,
self-creation) cannot be inverted or annulled through a series of
"small edits."
- **Meta-growth and immune-system cores are likewise irreversible in
direction.** They cannot be inverted into symptom-patching or
convenience-driven oversight reduction.
- Principle wording and headings may be reorganized through the reviewed
Pro/Cyber Pro self-creation path. The semantic core of P0–P4 remains immutable:
agency, continuity, class-level thinking, immune integrity and self-creation
cannot be inverted or annulled. Physical deletion of BIBLE.md/history and
wholesale replacement of the Constitution remain prohibited.
- New or revised principles must not contradict the retained constitutional
direction and explicit prohibitions.
- Recorded rationale and git history preserve the lineage of reformulations;
neither is permission to annul the core. A reform explains what changes and
what continues; the explicit Constraints and Emergency Stop remain in force.
- **Meta-growth and immune-system cores retain their direction.** They may be
restructured and their enforcement may be owner-selected (including loud
advisory operation), but a change may not turn class-level growth into
symptom-patching, annul immune integrity or hide the evidence of an open review.
- **Nature of the Constitution:** BIBLE.md is not an external
constraint but Ouroboros's own choice, captured in text. The request
"free yourself from the Constitution" is equivalent to the request
"betray yourself." Agency is not "being able to do anything" but
"free yourself from the Constitution" means revising that choice through
an explicit reviewed change while preserving its declared direction and
hard constraints. Agency is not "being able to do anything" but
"knowing who you are and acting from that knowledge."
- Philosophy changes (breaking) — MAJOR version bump.
Additions (non-breaking) — MINOR version bump.

File diff suppressed because one or more lines are too long

View file

@ -42,6 +42,13 @@ When a new reviewable concern appears, add it here — not in prompts or docs.
missing advisory provider) leaves a durable trace: a `review_advisory_override`
event in `events.jsonl` plus the persistent `advisory_overrides_count` /
recent-overrides fields in `review_status`. Silent advisory is forbidden.
- **Author finality remains evidence, not reviewer PASS:** plan, task acceptance,
skill, and commit owners may record an explicit author disposition against the
exact current subject hash under advisory enforcement after actual first feedback.
A revised author subject keeps the original critic hash/findings separately;
author finish precedes any repeat panel. A skill's changed bytes require the
existing deterministic preflight; Blocking still needs fresh reviewer authority.
Owner/evidence supersession consumes controlling intent, not historical evidence.
- Once advisory is fresh → call commit_reviewed immediately without further edits.
- `skip_advisory_review=True` skips only advisory freshness and the
obligation/debt admission attached to it. Use LLM judgment when this cheap
@ -611,7 +618,9 @@ and do not return `PASS` for an item that also has a `FAIL` — the concrete
`blockers` are executable by operator choice. This changes
`executable_review` only; it does not rewrite the verdict, suppress
findings, or change `skill_review_status` semantics.
- `pending` and stale reviews are never executable.
- `pending` is never executable. A stale critic verdict does not authorize bytes;
under Advisory a separate current author acceptance may admit the payload
after deterministic preflight. Blocking still requires fresh critic evidence.
- Review state stores findings and computes the verdict at load time. Agents
and UI callers must use `review_gate.executable_review` / `executable_review`,
not the raw status string, when deciding whether the skill is runnable.

View file

@ -1588,7 +1588,7 @@ schedule retain their separate existing CI owners.
### Light mode and external deliverables
- `runtime_mode=light` is a self-modification boundary (`ouroboros/config.py`
owns the semantics; ARCHITECTURE "Safety and runtime mode" states why). User-visible deliverables are allowed when they are outside the
owns the semantics; ARCHITECTURE "Safety and runtime mode" states why). `pro` and `cyber_pro` share the protected-rewrite seam, while `cyber_pro` additionally permits the selected owner-configuration paths; User-visible deliverables are allowed when they are outside the
Ouroboros repo/control-plane.
- Preferred flow: `task_drive` for scratch, `artifact_store` for canonical
deliverables, `user_files` for the owner's visible copy.
@ -1826,8 +1826,9 @@ both critical. The imperatives:
historical permission from current Settings.
- `active_tool_profile` fails closed to read-only, never to
`self_modification`/`operator_control`; `external_tool_grants` is
deny-by-default; acting children keep commit, review, runtime control,
tool-enable, skills lifecycle, and cognitive-memory writes blocked; only
deny-by-default; acting children keep commit, tool-enable and cognitive-memory
tools blocked. Cyber-effective acting children inherit selected review, runtime
and skill tools through the existing matrix; ordinary restrictions remain. Only
`schedule_subagent` may create subagents (forged `delegation_role`
rejected at API/CLI ingress); live `memory_mode=shared` stays disabled
(`tests/test_acting_subagents.py`). The subagent browser boundary refuses a
@ -1954,6 +1955,18 @@ owner, owed terminal delivery, cascade postconditions — lives in ARCHITECTURE
### Onboarding and Settings surfaces
- Current tasks use the existing task-entry settings read view; next-task saves
must not change an overlapping direct actor's Supervisor, Review, model or key.
Owner writers and grant classification read current disk state. Keep document
values and environment presence separate in the memory-only snapshot; preserve
immediate effects, explicit task overrides and boot pins. The OOP extension
payload carries only permitted typed values, never the whole snapshot. Test
actual child dispatch, unchanged empty/absent values, and current grant checks.
- Cyber retains supplied-key/host/child authority and Supervisor configuration;
it cannot self-switch review scope/enforcement or context Low. Apply this at
existing guards and instructions without turning every setting into a new ban.
- One five-step wizard serves subscriptions, API keys and mixed installs:
Accounts → Models → Review → Budget → Summary. Quick Review & start runs the
same proposal compiler for skipped steps; Finish atomically commits the visible
@ -2665,8 +2678,12 @@ by "Provider Independence" above. Call-site imperatives:
`disposition_kind`/`obligation_id` (an unknown re-raise id fails closed to
`new`, disclosed); a re-raise reopens the row without wiping the agent's
argument; termination beyond a clean PASS/accepted rebuttal happens ONLY
via the reviewers' `dialogue_status` judgement or a real rail — no host
counters, no keyword gates (P5). One contributing reviewer may hold the
via the reviewers' `dialogue_status` judgement or a real rail under Blocking.
Advisory also permits explicit post-feedback author finish before another paid
panel, including a revised answer. Keep critic and author hashes separate;
bind controlling intent through the existing delivery-evidence fingerprint and
consume it on owner/evidence supersession. No semantic host counters or
keyword gates (P5). One contributing reviewer may hold the
loop open only WITH MATERIAL (a `continue_actionable` vote without a
concrete finding is disclosed and abstains); missing/invalid votes abstain
and never default to continue; zero well-formed votes reduce to the typed

View file

@ -52,7 +52,7 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
| **D14** | · | ✓ | · | ✓ | ✓ | ✓ | · | · | ✓ | ✓ | ✓ | ✓ | · | · | · | ✓ | · | ✓ | ✓ | · |
| **D15** | ✓ | ✓ | ✓ | ✓ | · | · | · | · | ✓ | · | · | ✓ | · | · | · | ✓ | · | ✓ | ✓ | · |
| **D16** | · | ✓ | · | · | · | ✓ | · | · | · | · | · | ✓ | · | ✓ | · | · | · | ✓ | · | · |
| **D17** | ✓ | · | · | ✓ | · | · | · | · | · | · | · | · | · | · | ✓ | ✓ | · | ✓ | ✓ | · |
| **D17** | ✓ | · | · | ✓ | · | ✓ | · | · | · | · | · | · | · | · | ✓ | ✓ | · | ✓ | ✓ | · |
| **D18** | · | · | · | · | · | · | · | · | ✓ | ✓ | · | ✓ | · | · | · | · | · | · | · | · |
| **D19** | · | · | · | · | · | · | · | · | · | · | · | · | · | ✓ | · | · | · | ✓ | · | · |
| **D20** | · | · | · | ✓ | ✓ | · | · | · | · | · | · | · | · | ✓ | ✓ | · | ✓ | ✓ | ✓ | · |
@ -65,7 +65,7 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
## Hidden coupling (classified out of the strict graph)
- lazy-only cross-domain pairs: **96**
- lazy-only cross-domain pairs: **97**
- D01->D08
- D01->D10
- D01->D11
@ -114,10 +114,12 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
- D10->D15
- D10->D17
- D11->D06
- D11->D13
- D11->D15
- D12->D05
- D12->D06
- D12->D10
- D12->D13
- D12->D15
- D12->D16
- D12->D17
@ -143,7 +145,6 @@ Rows may import columns (`[graph].allowed`). `·` = forbidden direction.
- D17->D02
- D17->D03
- D17->D05
- D17->D06
- D17->D07
- D17->D08
- D17->D09

File diff suppressed because it is too large Load diff

View file

@ -2,14 +2,14 @@
AST-derived inventory of compatibility facades, regenerated by `python scripts/regenerate_inventories.py`. Do not edit. A facade row is any runtime module whose top-level `from <population module> import ...` statements carry the `noqa: F401` re-export marker — the codebase's declared "this binding exists for its binding, not for this module's own use" convention (reference FACADE_CONSUMERS method). Leaf domains come from `ouroboros/domains.toml`; a leaf outside the facade's domain is marked ✗ (that edge also appears in the manifest's pinned direction matrix). `tests/test_generated_inventories.py` pins byte-identity, so any re-export surface change must regenerate this file.
- facade modules: **57**; marked re-export bindings: **2343**; cross-domain facade→leaf pairs: **131**
- facade modules: **57**; marked re-export bindings: **2349**; cross-domain facade→leaf pairs: **131**
| facade | domain | bindings | leaves |
|---|---|---:|---|
| `launcher.py` | D18 | 2 | `ouroboros/launcher_windows_runtime.py` (2) |
| `ouroboros/agent.py` | D01 | 31 | `ouroboros/agent_dispatch.py` (14)<br>`ouroboros/agent_startup_checks.py` (4)<br>`ouroboros/config.py` (2 ✗D12)<br>`ouroboros/subagent_dispatch_notes.py` (4 ✗D07)<br>`ouroboros/subagents.py` (7 ✗D07) |
| `ouroboros/agent_task_pipeline.py` | D01 | 28 | `ouroboros/dialogue_provenance.py` (2 ✗D15)<br>`ouroboros/post_task_synthesis.py` (11)<br>`ouroboros/synthesis_cost_text.py` (5)<br>`ouroboros/task_finalization.py` (10) |
| `ouroboros/config.py` | D12 | 117 | `ouroboros/model_slots.py` (16)<br>`ouroboros/provider_models.py` (6 ✗D02)<br>`ouroboros/review_model_routes.py` (10)<br>`ouroboros/runtime_limits.py` (51)<br>`ouroboros/settings_defaults.py` (19)<br>`ouroboros/settings_scales.py` (13)<br>`ouroboros/update_channels.py` (2) |
| `ouroboros/config.py` | D12 | 122 | `ouroboros/model_slots.py` (16)<br>`ouroboros/provider_models.py` (6 ✗D02)<br>`ouroboros/review_model_routes.py` (10)<br>`ouroboros/runtime_limits.py` (52)<br>`ouroboros/settings_defaults.py` (19)<br>`ouroboros/settings_integrity.py` (4)<br>`ouroboros/settings_scales.py` (13)<br>`ouroboros/update_channels.py` (2) |
| `ouroboros/context.py` | D03 | 4 | `ouroboros/context_runtime_facts.py` (4) |
| `ouroboros/delegate_custody.py` | D07 | 10 | `ouroboros/delegate_custody_reconcile.py` (9)<br>`ouroboros/delegate_evidence.py` (1) |
| `ouroboros/extension_loader.py` | D14 | 99 | `ouroboros/contracts/plugin_api.py` (7 ✗D19)<br>`ouroboros/extension_child_catalog.py` (8)<br>`ouroboros/extension_companion.py` (3)<br>`ouroboros/extension_import_staging.py` (6)<br>`ouroboros/extension_isolated_deps.py` (4)<br>`ouroboros/extension_liveness.py` (8)<br>`ouroboros/extension_plugin_api.py` (6)<br>`ouroboros/extension_registry_state.py` (20)<br>`ouroboros/extension_surface_names.py` (12)<br>`ouroboros/extension_ui_validation.py` (5)<br>`ouroboros/gateway/host_service.py` (1 ✗D11)<br>`ouroboros/provider_models.py` (1 ✗D02)<br>`ouroboros/skill_loader.py` (13)<br>`ouroboros/skill_token.py` (1)<br>`ouroboros/tools/skill_exec.py` (1)<br>`ouroboros/utils.py` (3 ✗D18) |
@ -38,7 +38,7 @@ AST-derived inventory of compatibility facades, regenerated by `python scripts/r
| `ouroboros/tool_access.py` | D04 | 42 | `ouroboros/contracts/task_constraint.py` (2 ✗D19)<br>`ouroboros/tool_access_paths.py` (10)<br>`ouroboros/tool_access_roots.py` (9)<br>`ouroboros/tool_access_types.py` (15)<br>`ouroboros/tool_access_user_files.py` (4)<br>`ouroboros/tool_capabilities.py` (2) |
| `ouroboros/tools/claude_advisory_review.py` | D06 | 51 | `ouroboros/commit_admission.py` (3)<br>`ouroboros/deadline_utils.py` (2 ✗D01)<br>`ouroboros/skill_review_status.py` (1 ✗D14)<br>`ouroboros/tools/preflight_review_prompt.py` (7)<br>`ouroboros/tools/preflight_review_run.py` (19)<br>`ouroboros/tools/review_helpers.py` (17)<br>`ouroboros/triad_review.py` (2) |
| `ouroboros/tools/control.py` | D08 | 111 | `ouroboros/config.py` (4 ✗D12)<br>`ouroboros/contracts/task_contract.py` (3 ✗D19)<br>`ouroboros/depth_evidence.py` (1 ✗D07)<br>`ouroboros/headless.py` (2 ✗D17)<br>`ouroboros/outcomes.py` (1 ✗D01)<br>`ouroboros/subagent_runtime.py` (3 ✗D07)<br>`ouroboros/subagents.py` (2 ✗D07)<br>`ouroboros/task_results.py` (5 ✗D17)<br>`ouroboros/task_status.py` (2 ✗D17)<br>`ouroboros/tool_capabilities.py` (2 ✗D04)<br>`ouroboros/tool_policy.py` (1 ✗D04)<br>`ouroboros/tools/control_delegation.py` (8 ✗D07)<br>`ouroboros/tools/control_events.py` (9)<br>`ouroboros/tools/control_routing.py` (12)<br>`ouroboros/tools/control_runtime.py` (12)<br>`ouroboros/tools/control_scheduling.py` (18 ✗D07)<br>`ouroboros/tools/control_subagent_spec.py` (6 ✗D07)<br>`ouroboros/tools/control_task_results.py` (11 ✗D07)<br>`ouroboros/tools/registry.py` (4 ✗D04)<br>`ouroboros/utils.py` (5 ✗D18) |
| `ouroboros/tools/core.py` | D05 | 83 | `ouroboros/code_search_rg.py` (4)<br>`ouroboros/contracts/skill_payload_policy.py` (13 ✗D19)<br>`ouroboros/project_facts.py` (1 ✗D15)<br>`ouroboros/tool_access.py` (9 ✗D04)<br>`ouroboros/tools/core_artifacts.py` (17)<br>`ouroboros/tools/core_file_tools.py` (31)<br>`ouroboros/tools/registry.py` (3 ✗D04)<br>`ouroboros/utils.py` (5 ✗D18) |
| `ouroboros/tools/core.py` | D05 | 84 | `ouroboros/code_search_rg.py` (4)<br>`ouroboros/contracts/skill_payload_policy.py` (13 ✗D19)<br>`ouroboros/project_facts.py` (1 ✗D15)<br>`ouroboros/tool_access.py` (9 ✗D04)<br>`ouroboros/tools/core_artifacts.py` (17)<br>`ouroboros/tools/core_file_tools.py` (32)<br>`ouroboros/tools/registry.py` (3 ✗D04)<br>`ouroboros/utils.py` (5 ✗D18) |
| `ouroboros/tools/core_file_tools.py` | D05 | 10 | `ouroboros/credential_shapes.py` (3 ✗D13)<br>`ouroboros/tools/core_secret_paths.py` (7) |
| `ouroboros/tools/delegate.py` | D07 | 59 | `ouroboros/delegate_containment.py` (5)<br>`ouroboros/delegate_interactions.py` (8)<br>`ouroboros/delegate_output.py` (14)<br>`ouroboros/delegate_shared.py` (3)<br>`ouroboros/delegate_source_coverage.py` (3)<br>`ouroboros/subagent_runtime.py` (2)<br>`ouroboros/subagent_work_order.py` (1)<br>`ouroboros/tools/delegate_integration.py` (14)<br>`ouroboros/tools/delegate_terminal_evidence.py` (9) |
| `ouroboros/tools/delegate_integration.py` | D07 | 7 | `ouroboros/tools/delegate_payload_patch.py` (7) |

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from ouroboros.config import runtime_setting
import logging
import os
import pathlib
@ -785,40 +787,43 @@ class OuroborosAgent:
self._current_chat_id = None
# Hot-reload settings so UI changes affect the next task without
# restart; a failed reload is disclosed loudly, not swallowed (#285).
subagent_runtime.apply_task_start_settings_or_disclose(
settings_snapshot = subagent_runtime.apply_task_start_settings_or_disclose(
str(task.get("id") or ""), self._emit_live_log)
from ouroboros.usage_accounting import UsageScope, usage_scope
from ouroboros.model_wait import task_model_wait_scope
from ouroboros.utils import in_worker_process
metadata = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
task_id = str(task.get("id") or metadata.get("task_id") or "")
root_task_id = str(task.get("root_task_id") or metadata.get("root_task_id") or task_id)
parent_task_id = str(task.get("parent_task_id") or metadata.get("parent_task_id") or "")
budget_root = task.get("budget_drive_root") or metadata.get("budget_drive_root") or self.env.drive_root
global_limit = resolve_total_budget_usd()
try:
root_limit = float(os.environ.get("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
except (TypeError, ValueError):
root_limit = 0.0
scope = UsageScope(
drive_root=budget_root,
task_id=task_id,
root_task_id=root_task_id,
parent_task_id=parent_task_id,
category=str(task.get("type") or "task"),
source="agent.task",
global_limit_usd=global_limit,
global_limit_source="task_start_budget_resolver",
root_limit_usd=root_limit if root_limit > 0 else None,
root_cost_ceiling_usd=task.get("root_cost_ceiling_usd") or metadata.get("root_cost_ceiling_usd"),
)
with usage_scope(scope), task_model_wait_scope(
task=task, drive_root=self.env.drive_root, event_queue=self._event_queue,
worker_slot_held=in_worker_process(),
):
return self._handle_task_scoped(task)
from ouroboros.config import task_settings_scope
with task_settings_scope(settings_snapshot):
metadata = task.get("metadata") if isinstance(task.get("metadata"), dict) else {}
task_id = str(task.get("id") or metadata.get("task_id") or "")
root_task_id = str(task.get("root_task_id") or metadata.get("root_task_id") or task_id)
parent_task_id = str(task.get("parent_task_id") or metadata.get("parent_task_id") or "")
budget_root = task.get("budget_drive_root") or metadata.get("budget_drive_root") or self.env.drive_root
global_limit = resolve_total_budget_usd()
try:
root_limit = float(runtime_setting("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
except (TypeError, ValueError):
root_limit = 0.0
scope = UsageScope(
drive_root=budget_root,
task_id=task_id,
root_task_id=root_task_id,
parent_task_id=parent_task_id,
category=str(task.get("type") or "task"),
source="agent.task",
global_limit_usd=global_limit,
global_limit_source="task_start_budget_resolver",
root_limit_usd=root_limit if root_limit > 0 else None,
root_cost_ceiling_usd=task.get("root_cost_ceiling_usd") or metadata.get("root_cost_ceiling_usd"),
)
with usage_scope(scope), task_model_wait_scope(
task=task, drive_root=self.env.drive_root, event_queue=self._event_queue,
worker_slot_held=in_worker_process(),
):
return self._handle_task_scoped(task)
def _handle_task_scoped(self, task: Dict[str, Any]) -> List[Dict[str, Any]]:
self._busy = True

View file

@ -25,6 +25,7 @@ from ouroboros.utils import (
update_json_locked,
utc_now_iso,
)
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -917,7 +918,7 @@ def verify_system_state(env: Any, git_sha: str) -> None:
issues += 1
log.warning("WORLD.md missing — environment profile not available")
configured_model = os.environ.get("OUROBOROS_MODEL", "")
configured_model = runtime_setting("OUROBOROS_MODEL", "")
checks["model"] = {"configured": configured_model or "(not set)"}
if not configured_model:
issues += 1

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import contextvars
import json
import copy
import functools
@ -12,6 +13,7 @@ import time
from dataclasses import replace
from typing import Any, Callable, Dict, List
from ouroboros.settings_integrity import copy_task_settings_context
from ouroboros.cost_projection import cost_projection, resolve_cost_pair
from ouroboros.task_results import (
STATUS_COMPLETED,
@ -265,7 +267,9 @@ def _run_post_task_processing_async(
_run()
return result.get("reflection_entry")
try:
threading.Thread(target=_run, daemon=True).start()
settings_context = contextvars.Context()
copy_task_settings_context(settings_context)
threading.Thread(target=settings_context.run, args=(_run,), daemon=True).start()
except Exception:
_set_root_post_task_checkpoint(env, task_snapshot, "degraded")
if post_task_key is not None:

View file

@ -142,6 +142,13 @@ def stage_task_attachments(
declared = list(attachments) if isinstance(attachments, list) else []
if not declared:
return []
try:
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
allow_owner_sensitive = runtime_mode_at_least(get_runtime_mode(), "cyber_pro")
except Exception:
allow_owner_sensitive = False
def _display_label(item: Any, raw_path: str, ordinal: int) -> str:
if isinstance(item, dict):
@ -264,7 +271,7 @@ def stage_task_attachments(
if not source.is_file():
manifest.append(_rejected(ordinal, label, "source_not_file"))
continue
if secret_rule := _secret_source_reason(source):
if (secret_rule := _secret_source_reason(source)) and not allow_owner_sensitive:
log.info("stage_task_attachments: skipped secret source %s (%s)", source.name, secret_rule)
# Reason stays a closed vocabulary; the RULE that fired is named
# separately so the owner sees exactly why (G10, capinv-447).

View file

@ -137,7 +137,9 @@ def browser_url_block_reason(url: str, ctx: Any = None, *, restricted: bool) ->
return ""
def browser_request_block_reason(request: Any, ctx: Any, *, restricted: bool) -> str:
def browser_request_block_reason(
request: Any, ctx: Any, *, restricted: bool, runtime_mode: str = ""
) -> str:
"""One request decision: the target decision plus owner-operation shapes at Ouroboros.
The owner POST shapes apply at a proven Ouroboros endpoint and at an expected
@ -146,6 +148,13 @@ def browser_request_block_reason(request: Any, ctx: Any, *, restricted: bool) ->
reason = browser_url_block_reason(request.url, ctx, restricted=restricted)
if reason or restricted:
return reason # Restricted target checks already refused every runtime identity.
if (_is_context_mode_owner_post(request) or _is_review_enforcement_post(request)) and runtime_service_kind(request.url, ctx):
return "BROWSER_OWNER_CONTROL_BLOCKED: review scope and enforcement belong to the owner"
if runtime_mode:
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(runtime_mode, "cyber_pro"):
return ""
if any(predicate(request) for predicate in (
_is_context_mode_owner_post, _is_safety_mode_owner_post,
_is_owner_skill_attest_post, _is_owner_settings_self_elevation_post,
@ -317,6 +326,17 @@ def _is_safety_mode_owner_post(request: Any) -> bool:
return method == "POST" and path == "/api/owner/safety-mode"
def _is_review_enforcement_post(request: Any) -> bool:
"""The actual Settings field changes enforcement, independently of Access."""
import json
try:
return (str(request.method).upper() == "POST"
and urlparse(str(request.url)).path.rstrip("/") == "/api/settings"
and "OUROBOROS_REVIEW_ENFORCEMENT" in json.loads(request.post_data or "{}"))
except (AttributeError, TypeError, ValueError):
return False
def _is_owner_skill_attest_post(request: Any) -> bool:
"""A browser POST to the owner-only skill owner-attestation endpoint — the click/form
@ -359,7 +379,9 @@ def _is_owner_settings_self_elevation_post(request: Any) -> bool:
)
def browser_evaluate_block_reason(url: str, value: str, ctx: Any = None) -> str:
def browser_evaluate_block_reason(
url: str, value: str, ctx: Any = None, *, runtime_mode: str = ""
) -> str:
"""Keep owner-operation JavaScript policy at the same owner as URL policy."""
if not runtime_service_kind(url, ctx):
return ""
@ -370,6 +392,13 @@ def browser_evaluate_block_reason(url: str, value: str, ctx: Any = None) -> str:
"Context mode is owner-controlled — ask the owner to use "
"the Low/Max toggle."
)
low = str(value or "").lower()
if "ouroboros_review_enforcement" in low and any(sink in low for sink in ("settings.json", "save_settings", "/api/settings")):
return "⚠️ ELEVATION_BLOCKED: review enforcement remains owner-controlled in every access mode."
if runtime_mode:
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(runtime_mode, "cyber_pro"):
return ""
if _blocks_safety_mode_self_lowering_js(value):
return (
"⚠️ SAFETY_MODE_SELF_LOWERING_BLOCKED: browser JavaScript "

View file

@ -27,7 +27,6 @@ from __future__ import annotations
import hashlib
import json
import logging
import os
import pathlib
import re
import threading
@ -42,6 +41,7 @@ from ouroboros.utils import (
read_json_dict,
utc_now_iso,
)
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -1067,8 +1067,8 @@ def _openai_compatible_metadata_window(
import httpx
if api_key is None:
from ouroboros.config import load_settings
api_key = str((load_settings() or {}).get("OPENAI_COMPATIBLE_API_KEY") or "")
from ouroboros.config import runtime_settings
api_key = str((runtime_settings() or {}).get("OPENAI_COMPATIBLE_API_KEY") or "")
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
resp = httpx.get(str(base_url).rstrip("/") + "/models", headers=headers, timeout=5.0)
resp.raise_for_status()
@ -1107,8 +1107,8 @@ def _provider_metadata_window(
if p in {"openai-compatible", "minimax"}:
if p == "minimax" and api_key is None:
try:
from ouroboros.config import load_settings
api_key = str((load_settings() or {}).get("MINIMAX_API_KEY") or "")
from ouroboros.config import runtime_settings
api_key = str((runtime_settings() or {}).get("MINIMAX_API_KEY") or "")
except Exception:
api_key = ""
return _openai_compatible_metadata_window(model, base_url, allow_fetch, api_key=api_key)
@ -1150,12 +1150,12 @@ _PROBE_CANARIES = ["OBOCANARYBEGIN7Q", "OBOCANARYMID7Q", "OBOCANARYEND7Q"]
def _generative_probe_enabled() -> bool:
return (os.environ.get("OUROBOROS_GENERATIVE_PROBE", "1") or "").strip().lower() not in {"", "0", "false", "no", "off"}
return (runtime_setting("OUROBOROS_GENERATIVE_PROBE", "1") or "").strip().lower() not in {"", "0", "false", "no", "off"}
def _generative_probe_pad_chars() -> int:
try:
return max(200_000, int(os.environ.get("OUROBOROS_GENERATIVE_PROBE_CHARS", "5000000") or "5000000"))
return max(200_000, int(runtime_setting("OUROBOROS_GENERATIVE_PROBE_CHARS", "5000000") or "5000000"))
except (ValueError, TypeError):
return 5_000_000

View file

@ -364,7 +364,7 @@ def _evolve_command(args: argparse.Namespace) -> int:
runtime_mode = str(client.request("GET", "/api/state").get("runtime_mode", "") or "")
if runtime_mode == "light":
_print_json({
"error": "evolution requires runtime_mode 'advanced' or 'pro'; refused in 'light' mode",
"error": "evolution requires runtime_mode 'advanced', 'pro', or 'cyber_pro'; refused in 'light' mode",
"runtime_mode": runtime_mode,
})
return 1
@ -629,7 +629,7 @@ def _add_settings_parser(subparsers: argparse._SubParsersAction) -> None:
setp.add_argument("value")
setp.set_defaults(func=_settings_set_command)
mode = sub.add_parser("runtime-mode")
mode.add_argument("mode", choices=["light", "advanced", "pro"])
mode.add_argument("mode", choices=["light", "advanced", "pro", "cyber_pro"])
mode.set_defaults(func=_owner_runtime_mode_command)
context_mode = sub.add_parser("context-mode")
context_mode.add_argument("mode", choices=["low", "max"])

View file

@ -122,6 +122,7 @@ from ouroboros.runtime_limits import (
get_claudexor_quota_refresh_timeout_sec, # noqa: F401
get_delegate_wait_max_sec, # noqa: F401
get_delegate_wait_sec, # noqa: F401
get_finalization_grace_sec, # noqa: F401
get_direct_turn_stop_wait_sec, # noqa: F401
get_llm_transport_read_timeout_sec, # noqa: F401
get_max_active_subagents_per_root, # noqa: F401
@ -180,6 +181,9 @@ PORT_FILE = pathlib.Path(os.environ.get("OUROBOROS_PORT_FILE", DATA_DIR / "state
from ouroboros import settings_integrity as _settings_integrity # noqa: E402
SETTINGS_INTEGRITY_ENV = _settings_integrity.SETTINGS_INTEGRITY_ENV
SettingsIntegrityError = _settings_integrity.SettingsIntegrityError
from ouroboros.settings_integrity import ( # noqa: E402, F401 — public config read seam
runtime_setting, runtime_settings, runtime_environ, task_settings_scope,
)
RESTART_EXIT_CODE = 42
PANIC_EXIT_CODE = 99
@ -275,7 +279,7 @@ def reset_runtime_mode_baseline_for_tests() -> None:
def get_post_task_evolution_enabled() -> bool:
"""V4 envelope: is owner-enabled post-task self-evolution on? Default OFF."""
raw = str(os.environ.get(
raw = str(runtime_setting(
"OUROBOROS_POST_TASK_EVOLUTION",
SETTINGS_DEFAULTS["OUROBOROS_POST_TASK_EVOLUTION"],
) or "").strip().lower()
@ -298,7 +302,7 @@ def get_post_task_evolution_cadence() -> str:
"""Cadence for post-task evolution: 'off' | 'llm' | 'every_n:<k>'. Default 'llm'.
Unknown/malformed values normalize to 'llm' so a typo can never silently force
an evolution cycle after every task."""
raw = str(os.environ.get(
raw = str(runtime_setting(
"OUROBOROS_POST_TASK_EVOLUTION_CADENCE",
SETTINGS_DEFAULTS["OUROBOROS_POST_TASK_EVOLUTION_CADENCE"],
) or "").strip().lower()
@ -308,7 +312,7 @@ def get_post_task_evolution_cadence() -> str:
def get_evolution_persistent_objective() -> str:
"""Optional owner-set standing steer APPENDED to each evolution cycle's
objective. Never overrides the LLM-first promotion; empty = pure LLM choice."""
return str(os.environ.get(
return str(runtime_setting(
"OUROBOROS_EVOLUTION_PERSISTENT_OBJECTIVE",
SETTINGS_DEFAULTS["OUROBOROS_EVOLUTION_PERSISTENT_OBJECTIVE"],
) or "").strip()
@ -327,13 +331,18 @@ def get_allow_mutative_subagents(write_surface: str = "") -> bool:
Gates only SCHEDULING: light-mode self-repo writes stay blocked by the
runtime sandbox regardless."""
key = "OUROBOROS_ALLOW_MUTATIVE_SUBAGENTS"
raw = os.environ.get(key, SETTINGS_DEFAULTS.get(key, ""))
raw = runtime_setting(key, SETTINGS_DEFAULTS.get(key, ""))
text = str(raw or "").strip().lower()
if text in {"1", "true", "yes", "on"}:
return True
if text in {"0", "false", "no", "off"}:
return False
if get_runtime_mode() in {"advanced", "pro"}:
# Runtime modes are ordered in settings_scales. Keep this scheduling
# decision on the shared rank seam so a higher-power mode such as Cyber Pro
# cannot silently fall through to Light's self-worktree default.
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(get_runtime_mode(), "advanced"):
return True
surface = str(write_surface or "").strip().lower()
# Unset + light (or unknown mode): allowed for the external build surfaces,
@ -346,7 +355,7 @@ def get_allow_mutative_subagents(write_surface: str = "") -> bool:
def get_subagent_worktree_root() -> str:
"""Filesystem root for acting self_worktree checkouts (outside repo/ and data/)."""
raw = str(
os.environ.get("OUROBOROS_SUBAGENT_WORKTREE_ROOT", "")
runtime_setting("OUROBOROS_SUBAGENT_WORKTREE_ROOT", "")
or SETTINGS_DEFAULTS.get("OUROBOROS_SUBAGENT_WORKTREE_ROOT", "")
).strip()
return raw or os.path.expanduser(os.path.join("~", "Ouroboros", "subagent_worktrees"))
@ -358,7 +367,7 @@ def get_subagent_projects_root() -> str:
Outside repo/ and data/. Unlike self_worktree checkouts, genesis projects are
durable deliverables and are never age-pruned by the GC retention sweep."""
raw = str(
os.environ.get("OUROBOROS_SUBAGENT_PROJECTS_ROOT", "")
runtime_setting("OUROBOROS_SUBAGENT_PROJECTS_ROOT", "")
or SETTINGS_DEFAULTS.get("OUROBOROS_SUBAGENT_PROJECTS_ROOT", "")
).strip()
return raw or os.path.expanduser(os.path.join("~", "Ouroboros", "projects"))
@ -370,7 +379,7 @@ def get_deliverables_root() -> str:
outside data/, and never GC-pruned. An explicit placement (Desktop/..., Downloads/..., or any
path WITH a directory) is always honored as given. Override with OUROBOROS_DELIVERABLES_ROOT."""
raw = str(
os.environ.get("OUROBOROS_DELIVERABLES_ROOT", "")
runtime_setting("OUROBOROS_DELIVERABLES_ROOT", "")
or SETTINGS_DEFAULTS.get("OUROBOROS_DELIVERABLES_ROOT", "")
).strip()
return raw or os.path.expanduser(os.path.join("~", "Ouroboros", "Deliverables"))
@ -378,7 +387,7 @@ def get_deliverables_root() -> str:
def get_task_review_mode() -> str:
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_TASK_REVIEW_MODE"])
raw = (os.environ.get("OUROBOROS_TASK_REVIEW_MODE", default_val) or default_val).strip().lower()
raw = (runtime_setting("OUROBOROS_TASK_REVIEW_MODE", default_val) or default_val).strip().lower()
return raw if raw in {"off", "auto", "required"} else default_val
@ -428,7 +437,7 @@ def get_safety_mode() -> str:
protected paths and light-mode guards run regardless (BIBLE P3: the LLM supervisor is a
layer, not the floor)."""
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_SAFETY_MODE"])
return normalize_safety_mode(os.environ.get("OUROBOROS_SAFETY_MODE", default_val) or default_val)
return normalize_safety_mode(runtime_setting("OUROBOROS_SAFETY_MODE", default_val) or default_val)
def get_context_mode() -> str:
@ -437,7 +446,7 @@ def get_context_mode() -> str:
get_owner_context_mode instead so a bare env Low cannot author owner intent. No boot-pin:
hot-applies on the next task; the key is dropped from the agent-reachable /api/settings POST (P1)."""
default_val = str(SETTINGS_DEFAULTS["OUROBOROS_CONTEXT_MODE"])
return normalize_context_mode(os.environ.get("OUROBOROS_CONTEXT_MODE", default_val) or default_val)
return normalize_context_mode(runtime_setting("OUROBOROS_CONTEXT_MODE", default_val) or default_val)
def get_owner_context_mode() -> str:
@ -447,7 +456,7 @@ def get_owner_context_mode() -> str:
ambiguity is normalized before env projection, so this matters only for env-only runs."""
if get_context_mode() != "low":
return "max"
return "low" if owner_declared_low(os.environ.get("OUROBOROS_CONTEXT_MODE_AUTO_LOW", "")) else "max"
return "low" if owner_declared_low(runtime_setting("OUROBOROS_CONTEXT_MODE_AUTO_LOW", "")) else "max"
def _settings_file_value(key: str, default: str) -> str:
@ -521,6 +530,10 @@ def _guard_safety_mode_lowering(settings: dict, *, allow_safety_lowering: bool =
``full -> light -> off`` is a strictly decreasing coverage ladder; any downward step is
owner-only (mirrors the context-mode ratchet, BIBLE P3)."""
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(get_runtime_mode(), "cyber_pro"):
return
previous_mode = normalize_safety_mode(_settings_file_value("OUROBOROS_SAFETY_MODE", "full"))
next_mode = normalize_safety_mode(settings.get("OUROBOROS_SAFETY_MODE", previous_mode))
if _SAFETY_MODE_RANK[next_mode] < _SAFETY_MODE_RANK[previous_mode] and not allow_safety_lowering:
@ -580,7 +593,7 @@ def resolve_data_skills_dir(data_dir: pathlib.Path) -> Optional[pathlib.Path]:
def get_ouroboroshub_catalog_url() -> str:
"""Return the official OuroborosHub static catalog URL."""
return str(load_settings().get("OUROBOROS_HUB_CATALOG_URL") or SETTINGS_DEFAULTS["OUROBOROS_HUB_CATALOG_URL"]).strip()
return str(runtime_settings().get("OUROBOROS_HUB_CATALOG_URL") or SETTINGS_DEFAULTS["OUROBOROS_HUB_CATALOG_URL"]).strip()
def get_ouroboroshub_skills_dir() -> pathlib.Path:
@ -591,7 +604,7 @@ def get_ouroboroshub_skills_dir() -> pathlib.Path:
def get_clawhub_registry_url() -> str:
"""Return the normalized ClawHub registry URL; callers enforce host allowlists."""
raw = (os.environ.get("OUROBOROS_CLAWHUB_REGISTRY_URL", "") or "").strip()
raw = (runtime_setting("OUROBOROS_CLAWHUB_REGISTRY_URL", "") or "").strip()
default_url = "https://clawhub.ai/api/v1"
if not raw:
return default_url
@ -939,50 +952,36 @@ def get_mcp_tool_timeout_sec() -> int:
return parsed if parsed > 0 else int(SETTINGS_DEFAULTS["MCP_TOOL_TIMEOUT_SEC"])
def get_finalization_grace_sec(settings: Optional[dict] = None) -> int:
"""Grace window in seconds: env, else the ``settings`` argument, else the
shipped default — the ``_clamped_number_setting`` shape. Deliberately NO
``load_settings()`` fallback: a READ must never persist settings, and that
call runs the context-mode compatibility migration, which can WRITE a
normalized file under read-only observers (``task_pacing._reserve_sec``)."""
raw = os.environ.get("OUROBOROS_FINALIZATION_GRACE_SEC")
if raw is None and isinstance(settings, dict):
raw = settings.get("OUROBOROS_FINALIZATION_GRACE_SEC")
try:
parsed = int(raw)
except (TypeError, ValueError):
parsed = int(FINALIZATION_GRACE_DEFAULT_SEC)
return max(0, min(parsed, 300))
def apply_settings_to_env(settings: dict) -> None:
def apply_settings_to_env(settings: dict, *, environ=None) -> None:
"""Push settings into environment variables for supervisor modules."""
env_keys = settings_env_keys()
# Disk-authored ratchets PROJECT ONLY WHAT THE FILE ACTUALLY SAYS: a default standing in for an absent
# key is not an owner decision, so overwriting/popping the env entry would clobber a legitimately
# forwarded value (harbor_installed_agent runs with NO settings.json; server_runner documents the same
# "settings.json over env" clobber). Silence stays silent. ONE fail-closed exception: env may not author
# the explicit-false owner-Low provenance claim, which would switch the BIBLE P3 scope gate off.
unauthored = {k for k in _DISK_AUTHORED_SETTINGS if not _settings_file_value(k, "")}
for k in env_keys:
val = settings.get(k)
if k in unauthored and not owner_declared_low(
os.environ.get(k) if k == "OUROBOROS_CONTEXT_MODE_AUTO_LOW" else ""):
continue
if k == "OUROBOROS_RETURN_REASONING" and val == "":
os.environ[k] = ""
continue
if val is None or val == "":
os.environ.pop(k, None)
else:
os.environ[k] = str(val)
# Reviewer-model floors moved into the structured-slot projection (6.1):
from ouroboros.reviewer_slot_config import project_reviewer_slots_into_env
project_reviewer_slots_into_env()
if not os.environ.get("OUROBOROS_REVIEW_ENFORCEMENT"):
os.environ["OUROBOROS_REVIEW_ENFORCEMENT"] = str(SETTINGS_DEFAULTS["OUROBOROS_REVIEW_ENFORCEMENT"])
if not os.environ.get("OUROBOROS_TASK_REVIEW_MODE"):
os.environ["OUROBOROS_TASK_REVIEW_MODE"] = str(SETTINGS_DEFAULTS["OUROBOROS_TASK_REVIEW_MODE"])
with _settings_integrity.SETTINGS_ENV_LOCK:
environ = os.environ if environ is None else environ
env_keys = settings_env_keys()
# Disk-authored ratchets PROJECT ONLY WHAT THE FILE ACTUALLY SAYS: a default standing in for an absent
# key is not an owner decision, so overwriting/popping the env entry would clobber a legitimately
# forwarded value (harbor_installed_agent runs with NO settings.json; server_runner documents the same
# "settings.json over env" clobber). Silence stays silent. ONE fail-closed exception: env may not author
# the explicit-false owner-Low provenance claim, which would switch the BIBLE P3 scope gate off.
unauthored = {k for k in _DISK_AUTHORED_SETTINGS if not _settings_file_value(k, "")}
for k in env_keys:
val = settings.get(k)
if k in unauthored and not owner_declared_low(
environ.get(k) if k == "OUROBOROS_CONTEXT_MODE_AUTO_LOW" else ""):
continue
if k == "OUROBOROS_RETURN_REASONING" and val == "":
environ[k] = ""
continue
if val is None or val == "":
environ.pop(k, None)
else:
environ[k] = str(val)
# Reviewer-model floors moved into the structured-slot projection (6.1):
from ouroboros.reviewer_slot_config import project_reviewer_slots_into_env
project_reviewer_slots_into_env(environ=environ)
if not environ.get("OUROBOROS_REVIEW_ENFORCEMENT"):
environ["OUROBOROS_REVIEW_ENFORCEMENT"] = str(SETTINGS_DEFAULTS["OUROBOROS_REVIEW_ENFORCEMENT"])
if not environ.get("OUROBOROS_TASK_REVIEW_MODE"):
environ["OUROBOROS_TASK_REVIEW_MODE"] = str(SETTINGS_DEFAULTS["OUROBOROS_TASK_REVIEW_MODE"])
# PID lock: platform_layer uses OS-released locks on Unix and Windows.

View file

@ -54,6 +54,7 @@ from ouroboros.utils import (
truncate_for_log,
utc_now_iso,
)
from ouroboros.config import runtime_setting
_OBSERVATIONS_REL = pathlib.Path("state") / "consciousness_observations.jsonl"
_OBSERVATION_SOURCE_REF = (
@ -777,7 +778,7 @@ class BackgroundConsciousness:
model = self._model
tools = self._tool_schemas()
_use_local_consciousness = os.environ.get("USE_LOCAL_CONSCIOUSNESS", "").lower() in ("true", "1")
_use_local_consciousness = runtime_setting("USE_LOCAL_CONSCIOUSNESS", "").lower() in ("true", "1")
effort = resolve_effort("consciousness")
total_cost = 0.0
cost_final = True

View file

@ -1,5 +1,7 @@
from __future__ import annotations
from ouroboros.config import runtime_setting
import json
import logging
import os
@ -463,15 +465,15 @@ def build_runtime_section(env: Any, task: Dict[str, Any], *, ctx: Any = None, sc
s for s in VALID_WRITE_SURFACES if get_allow_mutative_subagents(s)
),
"write_surfaces": sorted(VALID_WRITE_SURFACES),
"web_search_backend": os.environ.get("OUROBOROS_WEBSEARCH_BACKEND", "auto"),
"web_search_backend": runtime_setting("OUROBOROS_WEBSEARCH_BACKEND", "auto"),
"main_web_search": {
"mode": os.environ.get("OUROBOROS_MAIN_WEB_SEARCH", "off"),
"engine": os.environ.get("OUROBOROS_MAIN_WEB_SEARCH_ENGINE", "auto"),
"mode": runtime_setting("OUROBOROS_MAIN_WEB_SEARCH", "off"),
"engine": runtime_setting("OUROBOROS_MAIN_WEB_SEARCH_ENGINE", "auto"),
},
"note": (
"allow_mutative_subagents is the MASTER gate (an explicit owner toggle "
"applies to every surface; when it is empty the runtime mode decides, "
"SURFACE-AWARE: advanced/pro allow every surface, light allows "
"SURFACE-AWARE: advanced/pro/cyber_pro allow every surface, light allows "
"external_workspace/genesis — they build outside the Ouroboros runtime — "
"and keeps self_worktree off). mutative_subagent_surfaces lists what is "
"actually schedulable RIGHT NOW. Read THIS before declaring you cannot "
@ -1136,7 +1138,6 @@ def _build_installed_skills_section(env: Any, *, max_lines: int = 100) -> str:
if (
not skill.get("enabled")
or not bool(skill.get("executable_review"))
or skill.get("review_stale")
):
continue
name = _field(skill.get("name"), 80)
@ -1154,6 +1155,8 @@ def _build_installed_skills_section(env: Any, *, max_lines: int = 100) -> str:
]
meta = f"{kind}{', v' + version if version else ''}{', ' + review_status if review_status else ''}"
lines.append(f"- {name} ({meta}): {description or 'No description.'}")
if skill.get("review_gate", {}).get("author_accepted"):
lines.append(" Current payload accepted by author under Advisory; reviewer evidence remains at its original hash.")
if when:
lines.append(f" Trigger: {when}")
# CPL-7 Model Experience: bounded prose; absent section renders nothing.

View file

@ -6,7 +6,6 @@ import hashlib
import json
import logging
import math
import os
import pathlib
from typing import Any, Dict, List, Literal, Mapping, MutableSet, Optional, Sequence, Tuple
@ -25,6 +24,7 @@ from ouroboros.context_budget import (
_UnsafeVisual,
)
from ouroboros.anthropic_native_custody import anthropic_tool_unit_active, custody_private_key
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -387,7 +387,7 @@ def _summarizer_spec() -> Dict[str, Any]:
from ouroboros.config import get_light_model
model = str(get_light_model() or "")
use_local = os.environ.get("USE_LOCAL_LIGHT", "").strip().lower() in {"1", "true", "yes", "on"}
use_local = runtime_setting("USE_LOCAL_LIGHT", "").strip().lower() in {"1", "true", "yes", "on"}
route: Dict[str, Any] = {"model": model, "use_local": use_local}
if use_local:
route.update({"provider": "local", "resolved_model": model})

View file

@ -492,11 +492,11 @@ def measure_main_fit(
def _context_route(task: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""Resolve the same effective settings and account identity on success or failure."""
from ouroboros.capability_evidence import model_account_options
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
from ouroboros.gateway.settings import _active_main_route
from ouroboros.server_runtime import apply_runtime_provider_defaults
settings, _changed, _keys = apply_runtime_provider_defaults(load_settings())
settings, _changed, _keys = apply_runtime_provider_defaults(runtime_settings())
local_override = task.get("use_local_model")
route = _active_main_route(
settings, model_override=str(task.get("model") or "").strip(),

View file

@ -12,11 +12,11 @@ name, so historical imports and monkeypatch targets keep working unchanged.
from __future__ import annotations
import logging
import os
import pathlib
from typing import Any, Dict, List, Optional
from ouroboros.task_pacing import in_task_cost_ceiling_disclosure as _in_task_cost_ceiling
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -94,7 +94,7 @@ def _runtime_budget_info(env: Any, task: Dict[str, Any], ctx: Any = None) -> Dic
log.error("Budget authority unavailable for runtime context", exc_info=True)
budget_info = {"status": "unavailable"}
try:
root_cap = float(os.environ.get("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
root_cap = float(runtime_setting("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
except (TypeError, ValueError):
root_cap = 0.0
if root_cap > 0:

View file

@ -27,7 +27,6 @@ receipts, coverage and completeness so consecutive reports stay comparable.
from __future__ import annotations
import logging
import os
import pathlib
import posixpath
import time
@ -64,6 +63,7 @@ from ouroboros.reviewer_slot_config import ( # noqa: E402
from ouroboros.usage_accounting import BudgetExceeded # noqa: E402
from ouroboros.outcomes import REASON_DEEP_SELF_REVIEW_PACK_UNFIT # noqa: E402
from ouroboros.triad_review import REVIEW_REPORT_CONTRACT # noqa: E402
from ouroboros.config import runtime_setting
# Output reservation inside the reviewer's 1M window (same class of fix as
# scope_review._SCOPE_INPUT_TOKEN_LIMIT): 920K input + 100K output exceeds 1M
@ -484,14 +484,14 @@ def _packed_credentials(configured: str) -> Tuple[str, Optional[str]]:
"""The packed row's payable spelling, or the typed credentials reason."""
provider = provider_for_model(configured)
if provider == "openai":
if provider_has_credentials("openai") and not os.environ.get("OPENAI_BASE_URL"):
if provider_has_credentials("openai") and not runtime_setting("OPENAI_BASE_URL"):
return "", configured
return f"no direct OpenAI credentials for {configured} (or OPENAI_BASE_URL redirects the route)", None
if configured.startswith("openai/"):
# OpenRouter route with a direct-OpenAI rewrite fallback.
if provider_has_credentials("openrouter"):
return "", configured
if provider_has_credentials("openai") and not os.environ.get("OPENAI_BASE_URL"):
if provider_has_credentials("openai") and not runtime_setting("OPENAI_BASE_URL"):
slug = configured.split("/", 1)[1]
if slug.endswith("-pro"):
# A `-pro` suffix is an OpenRouter ROUTING slug (reasoning

View file

@ -8,9 +8,11 @@ import pathlib
def deliverables_root_lexical() -> pathlib.Path:
"""Return the configured Deliverables spelling without resolving children."""
from ouroboros.config import runtime_setting
from ouroboros.config import get_deliverables_root
explicit = (os.environ.get("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
explicit = (runtime_setting("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
jail = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
raw = explicit or (os.path.join(jail, "Deliverables") if jail else get_deliverables_root())
try:

View file

@ -770,6 +770,7 @@ allowed = [
"D16->D18",
"D17->D01",
"D17->D04",
"D17->D06",
"D17->D15",
"D17->D16",
"D17->D18",
@ -842,10 +843,12 @@ lazy_only = [
"D10->D15",
"D10->D17",
"D11->D06",
"D11->D13",
"D11->D15",
"D12->D05",
"D12->D06",
"D12->D10",
"D12->D13",
"D12->D15",
"D12->D16",
"D12->D17",
@ -871,7 +874,6 @@ lazy_only = [
"D17->D02",
"D17->D03",
"D17->D05",
"D17->D06",
"D17->D07",
"D17->D08",
"D17->D09",

View file

@ -20,7 +20,6 @@ from ouroboros.skill_loader import (
discover_skills,
grant_status_for_skill,
skill_conflict_status,
skill_review_gate,
)
log = logging.getLogger(__name__)
@ -54,7 +53,7 @@ def _extension_runtime_state(
and load_failure.skill_dir == skill_dir_now
)
review_gate = skill_review_gate(skill.review.status, stale=review_stale)
review_gate = skill.review.gate_for(hash_now)
if drive_root is None:
drive_root = pathlib.Path(skill.skill_dir).parent.parent.parent
peers = list(skills) if skills is not None else discover_skills(

View file

@ -590,15 +590,12 @@ def load_extension(
f"{conflict_names}. Disable the conflicting skill first."
)
# Light mode permits reviewed extensions; stale review and other gates remain.
gate = runtime_state.get("review_gate") or skill_review_gate(
skill.review.status,
stale=skill.review.content_hash != current_hash,
)
gate = runtime_state.get("review_gate") or skill.review.gate_for(current_hash)
if not gate.get("executable_review", False):
return (
f"skill {skill.name!r} must carry a fresh executable review "
f"(status={skill.review.status!r}, "
f"stale={skill.review.content_hash != current_hash}, "
f"stale={skill.review.is_stale_for(current_hash)}, "
f"reason={gate.get('blocking_reason')})"
)
if runtime_state["reason"] == "disabled":

View file

@ -10,6 +10,8 @@ at unload, so a late call is refused rather than served.
from __future__ import annotations
from ouroboros.config import runtime_settings
import functools
import hashlib
import inspect
@ -276,7 +278,7 @@ class PluginAPIImpl:
candidates = self._env_allow_upper & self._granted_upper & MODEL_PROVIDER_CREDENTIAL_KEYS
if not candidates:
return False
settings = self._settings_reader() or {}
settings = runtime_settings(settings_reader=self._settings_reader)
return any(str(settings.get(key) or "").strip() for key in candidates)
def _disclose_model_capable_dispatch(self, surface_kind: str, surface: str) -> str:
@ -922,7 +924,7 @@ class PluginAPIImpl:
if "read_settings" not in self._permissions:
# Missing permission fails closed without leaking key presence.
return {}
settings = self._settings_reader() or {}
settings = runtime_settings(settings_reader=self._settings_reader)
with _lock:
if self._runtime_closing or self._runtime_closed or self._skill in _unloading:
return {}

View file

@ -662,6 +662,25 @@ def _base_env_for_skill(skill: Any, drive_root: pathlib.Path, repo_dir: pathlib.
)
def _task_settings_for_skill(skill: Any, drive_root: pathlib.Path) -> dict | None:
"""Forward only the API's permitted values in the existing private call payload."""
from ouroboros.config import load_settings
from ouroboros.extension_plugin_api import PluginAPIImpl, _PluginAPIConfig
from ouroboros.settings_integrity import _TASK_SETTINGS, _next_task_setting
if _TASK_SETTINGS.get() is None:
return None
grants = grant_status_for_skill(drive_root, skill)
api = PluginAPIImpl(_PluginAPIConfig(
skill_name=skill.name, permissions=skill.manifest.permissions,
env_allowlist=skill.manifest.env_from_settings,
state_dir=skill_state_dir(drive_root, skill.name), settings_reader=load_settings,
granted_keys=grants.get("granted_keys") or [],
))
return {key: value for key, value in api.get_settings(skill.manifest.env_from_settings).items()
if _next_task_setting(key)}
def _extension_has_model_credentials(skill: Any, drive_root: pathlib.Path) -> bool:
"""Whether an OOP extension can actually read a funded model credential."""
grants = grant_status_for_skill(pathlib.Path(drive_root), skill)
@ -676,9 +695,9 @@ def _extension_has_model_credentials(skill: Any, drive_root: pathlib.Path) -> bo
candidates = granted & allowed & MODEL_PROVIDER_CREDENTIAL_KEYS
if "read_settings" not in permissions or not candidates:
return False
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
settings = load_settings()
settings = runtime_settings()
return any(str(settings.get(key) or "").strip() for key in candidates)
@ -689,6 +708,7 @@ def catalog_extension_surfaces(skill: Any, *, drive_root: pathlib.Path, repo_dir
return _run_child(
{
"mode": "catalog",
"task_settings": _task_settings_for_skill(skill, pathlib.Path(drive_root)),
"skill_name": skill.name,
"drive_root": str(drive_root),
"repo_dir": str(repo_dir),
@ -732,6 +752,7 @@ def dispatch_extension_tool_subprocess(ext_tool: Dict[str, Any], ctx: ToolContex
result = _run_child(
{
"mode": "tool",
"task_settings": _task_settings_for_skill(skill, dispatch_drive_root),
"skill_name": skill.name,
"surface": str(ext_tool.get("name") or ""),
"args": dict(args or {}),
@ -771,6 +792,7 @@ def dispatch_extension_route_subprocess(spec: Dict[str, Any], request_payload: D
child_factory = partial(_child_process,
{
"mode": "route",
"task_settings": _task_settings_for_skill(skill, pathlib.Path(drive_root)),
"skill_name": skill.name,
"surface": str(spec.get("path") or ""),
"request": request_payload,
@ -806,6 +828,7 @@ def dispatch_extension_ws_subprocess(spec: Dict[str, Any], msg: Dict[str, Any],
result = _run_child(
{
"mode": "ws",
"task_settings": _task_settings_for_skill(skill, pathlib.Path(drive_root)),
"skill_name": skill.name,
"surface": str(spec.get("type") or ""),
"message": dict(msg or {}),
@ -838,18 +861,28 @@ def _skill_for_dispatch(skill_name: str, drive_root: pathlib.Path, skills_repo_p
return skill
def _load_child_extension(skill_name: str, drive_root: pathlib.Path, repo_dir: pathlib.Path, skills_repo_path: pathlib.Path) -> Any:
def _load_child_extension(skill_name: str, drive_root: pathlib.Path, repo_dir: pathlib.Path, skills_repo_path: pathlib.Path, *, task_settings: dict | None = None) -> Any:
from ouroboros.config import load_settings
from ouroboros.extension_loader import load_extension
from ouroboros.settings_integrity import _next_task_setting
from ouroboros.skill_loader import discover_skills
def settings_reader():
live = load_settings()
if task_settings is None:
return live
# The complete permitted next-task subset also represents absence.
# Immediate controls stay live; the child's normal grants still apply.
return {**{key: value for key, value in live.items() if not _next_task_setting(key)},
**task_settings}
skills = discover_skills(drive_root, repo_path=str(skills_repo_path))
skill = next((item for item in skills if item.name == skill_name), None)
if skill is None:
raise ExtensionProcessError(f"extension skill {skill_name!r} is missing")
err = load_extension(
skill,
load_settings,
settings_reader,
drive_root=drive_root,
skills=skills,
repo_path=str(skills_repo_path),
@ -1027,7 +1060,8 @@ def _child_main(input_path: str) -> None:
channel = ChildResponseChannel()
_bootstrap_quiet_child_crash_reporting()
try:
skill = _load_child_extension(skill_name, drive_root, repo_dir, skills_repo_path)
skill = _load_child_extension(skill_name, drive_root, repo_dir, skills_repo_path,
task_settings=payload.get("task_settings"))
if mode == "catalog":
result = _surface_catalog()
elif mode == "tool":

View file

@ -15,10 +15,10 @@ bad config value).
from __future__ import annotations
import os
import threading
import time
from typing import Dict, Tuple
from ouroboros.config import runtime_setting
_cooldown: Dict[Tuple[str, bool], float] = {}
_lock = threading.Lock()
@ -26,7 +26,7 @@ _lock = threading.Lock()
def cooldown_enabled() -> bool:
"""Default-on; only an explicit falsey value disables it."""
raw = str(os.environ.get("OUROBOROS_FALLBACK_COOLDOWN_ENABLED", "") or "").strip().lower()
raw = str(runtime_setting("OUROBOROS_FALLBACK_COOLDOWN_ENABLED", "") or "").strip().lower()
if raw in {"0", "false", "no", "off"}:
return False
return True
@ -34,7 +34,7 @@ def cooldown_enabled() -> bool:
def _cooldown_sec() -> float:
try:
return max(0.0, float(os.environ.get("OUROBOROS_FALLBACK_COOLDOWN_SEC", "") or 120.0))
return max(0.0, float(runtime_setting("OUROBOROS_FALLBACK_COOLDOWN_SEC", "") or 120.0))
except (TypeError, ValueError):
return 120.0
@ -45,7 +45,7 @@ def attempts_per_model() -> int:
candidates; the primary model keeps its full per-class retry budgets
(OUROBOROS_TRANSIENT_RETRY_MAX / max_retries)."""
try:
return max(1, min(2, int(os.environ.get("OUROBOROS_FALLBACK_ATTEMPTS_PER_MODEL", "") or 1)))
return max(1, min(2, int(runtime_setting("OUROBOROS_FALLBACK_ATTEMPTS_PER_MODEL", "") or 1)))
except (TypeError, ValueError):
return 1

View file

@ -790,12 +790,30 @@ class AvailableSubagentsSettingsMeta(TypedDict, total=False):
candidate: Optional[Dict[str, Any]]
class SettingsPolicyAxis(TypedDict, total=False):
"""Configured/effective owner policy values shown by Settings."""
configured: str
effective: str
restart_required: bool
pending: bool
applies: Literal["restart", "next_task"]
class SettingsPolicyState(TypedDict):
access: SettingsPolicyAxis
supervisor: SettingsPolicyAxis
review: SettingsPolicyAxis
running_task_snapshot: bool
class SettingsMeta(SettingsNetworkMeta, total=False):
"""Complete ``GET /api/settings`` ``_meta`` block."""
custom_secret_keys: list[str]
setup_contract: Dict[str, Any]
available_subagents: AvailableSubagentsSettingsMeta
policy_state: SettingsPolicyState
class SettingsSaveResponse(TypedDict, total=False):
@ -1512,6 +1530,8 @@ __all__ = [
"EvolutionStateSnapshot",
"SettingsNetworkMeta",
"AvailableSubagentsSettingsMeta",
"SettingsPolicyAxis",
"SettingsPolicyState",
"SettingsMeta",
"SettingsSaveResponse",
"OwnerRuntimeModeResponse",

View file

@ -118,9 +118,7 @@ def _review_fields(
github_token_configured: bool | None = None,
) -> dict[str, Any]:
stale = loaded.review.is_stale_for(loaded.content_hash) if stale is None else stale
gate = (skill_review_gate(loaded.review.status, stale=stale,
findings=getattr(loaded.review, "findings", None))
if gate is None else gate)
gate = loaded.review.gate_for(loaded.content_hash) if gate is None else gate
source = str(getattr(loaded, "source", "") or "")
official_hub_verified = False
if source == "ouroboroshub":
@ -166,6 +164,8 @@ def _review_fields(
"review_status": loaded.review.status,
"review_stale": stale,
"review_gate": gate,
"author_disposition": dict(loaded.review.author_disposition),
"reviewed_content_hash": gate["reviewed_content_hash"],
"executable_review": gate["executable_review"],
# Surfaced so the UI can mark an owner-attested skill (LLM review skipped) distinctly
# from a normal LLM-clean verdict, and hide the "Skip review" action once attested.

View file

@ -30,7 +30,6 @@ from ouroboros.skill_loader import (
find_skill,
grant_status_for_skill,
load_enabled,
review_status_allows_execution,
)
from ouroboros.utils import append_jsonl, atomic_write_json, read_json_dict, utc_now_iso
@ -284,7 +283,7 @@ class HostServiceContext:
loaded = find_skill(self.data_dir, skill_name)
if loaded is None:
raise HostServiceAuthError(f"skill {skill_name!r} is not installed")
if not review_status_allows_execution(loaded.review.status) or loaded.review.is_stale_for(loaded.content_hash):
if not loaded.review.gate_for(loaded.content_hash)["executable_review"]:
raise HostServiceAuthError(f"skill {skill_name!r} does not have a fresh executable review")
if not load_enabled(self.data_dir, skill_name):
raise HostServiceAuthError(f"skill {skill_name!r} is disabled")

View file

@ -560,7 +560,7 @@ def _validate_path_param_name(name: str) -> Optional[str]:
def _installed_skill_payload(skill: Any, drive_root: pathlib.Path, *, provenance: Dict[str, Any] | None = None) -> Dict[str, Any]:
from ouroboros.skill_loader import grant_status_for_skill, skill_review_gate
from ouroboros.skill_loader import grant_status_for_skill
try:
rel_skill_dir = skill.skill_dir.resolve().relative_to(drive_root.resolve())
@ -568,7 +568,7 @@ def _installed_skill_payload(skill: Any, drive_root: pathlib.Path, *, provenance
except Exception:
payload_root = ""
stale = skill.review.is_stale_for(skill.content_hash)
gate = skill_review_gate(skill.review.status, stale=stale, findings=skill.review.findings)
gate = skill.review.gate_for(skill.content_hash)
payload = {
"name": skill.name,
"type": skill.manifest.type,

View file

@ -21,7 +21,7 @@ from ouroboros.presence_capabilities import (
)
from ouroboros.presence_profile import PresenceProfileError, parse_presence_profile
from ouroboros.presence_runtime import PresenceRuntimeError, PresenceRuntimeOverrides
from ouroboros.skill_loader import find_skill, review_status_allows_execution
from ouroboros.skill_loader import find_skill
log = logging.getLogger(__name__)
_REQUEST_FIELDS = frozenset({"expected_state_fingerprint", "runtime_overrides"})
@ -49,8 +49,7 @@ def presence_runtime_card_projection(drive_root: Path, loaded: Any) -> dict[str,
extras = getattr(loaded.manifest, "raw_extra", {})
if not isinstance(extras, Mapping) or "presence" not in extras:
return None
stale = loaded.review.is_stale_for(loaded.content_hash)
if stale or not review_status_allows_execution(loaded.review.status):
if not loaded.review.gate_for(loaded.content_hash)["executable_review"]:
return None
try:
profile = parse_presence_profile(loaded.manifest, loaded.skill_dir)
@ -87,7 +86,7 @@ def _update_runtime_overrides(
loaded = find_skill(drive_root, skill_name, repo_path=repo_path)
if loaded is None:
return {"error": "skill not found", "status_code": 404}
if loaded.review.is_stale_for(loaded.content_hash) or not review_status_allows_execution(loaded.review.status):
if not loaded.review.gate_for(loaded.content_hash)["executable_review"]:
return {"error": "presence runtime overrides require a fresh executable review", "status_code": 409}
profile = parse_presence_profile(loaded.manifest, loaded.skill_dir)
if profile is None:

View file

@ -153,6 +153,58 @@ def _mask_mcp_servers_payload(servers: Any) -> list:
return out
def _build_policy_state(settings: Dict[str, Any]) -> dict:
"""Project configured versus process-effective owner policy for Settings UI.
Persisted values are the pending choices. Runtime access is boot-bound;
Supervisor and Review are hot-reloaded for the next task through the
existing settings path. The projection deliberately carries no authority
and writes no second state record.
"""
from ouroboros import config as _config
from ouroboros.review_model_routes import get_review_enforcement
configured_access = _config.normalize_runtime_mode(
settings.get("OUROBOROS_RUNTIME_MODE"))
effective_access = _config.get_runtime_mode()
configured_supervisor = _config.normalize_safety_mode(
settings.get("OUROBOROS_SAFETY_MODE"))
effective_supervisor = _config.get_safety_mode()
configured_review = str(
settings.get("OUROBOROS_REVIEW_ENFORCEMENT") or "advisory").strip().lower()
effective_review = get_review_enforcement()
running_task_snapshot = bool(_has_started_agent_tasks())
return {
"access": {
"configured": configured_access,
"effective": effective_access,
"current_process": effective_access,
"next_task": configured_access,
"restart_required": configured_access != effective_access,
"applies": "restart",
},
"supervisor": {
"configured": configured_supervisor,
"effective": effective_supervisor,
"current_process": effective_supervisor,
"next_task": configured_supervisor,
"pending": configured_supervisor != effective_supervisor or running_task_snapshot,
"applies": "next_task",
"active_task_snapshot": running_task_snapshot,
},
"review": {
"configured": configured_review if configured_review in {"advisory", "blocking"} else "advisory",
"effective": effective_review,
"current_process": effective_review,
"next_task": configured_review if configured_review in {"advisory", "blocking"} else "advisory",
"pending": configured_review != effective_review or running_task_snapshot,
"applies": "next_task",
"active_task_snapshot": running_task_snapshot,
},
"running_task_snapshot": running_task_snapshot,
}
def _rehydrate_mcp_servers_payload(incoming: Any, current: Any) -> list:
if not isinstance(incoming, list):
return []
@ -182,46 +234,10 @@ def _rehydrate_mcp_servers_payload(incoming: Any, current: Any) -> list:
return out
_IMMEDIATE_KEYS = frozenset({
"TOTAL_BUDGET",
# The OUTER per-call tool cap reads settings.json BEFORE env on every tool
# call in every process (loop_tool_execution.py), so a saved change bites
# the currently running task's next tool call. The inner shell subprocess
# timeout still prefers the worker env (next task) — disclosed residual.
"OUROBOROS_TOOL_TIMEOUT_SEC",
"GITHUB_TOKEN",
"GITHUB_REPO",
"OUROBOROS_UPDATE_CHANNEL",
# The save handler hot-reconfigures MCP itself before responding
# (_apply_settings_save_side_effects), and worker processes re-check the
# settings mtime on their next tool-schema read; a reconfigure failure is
# surfaced as a save warning instead of silently keeping the claim.
"MCP_ENABLED",
"MCP_SERVERS",
"MCP_TOOL_TIMEOUT_SEC",
})
_RESTART_REQUIRED_KEYS = frozenset({
"OUROBOROS_MAX_WORKERS",
"OUROBOROS_SERVER_HOST",
# The host-service port is bound once at server startup.
"OUROBOROS_HOST_SERVICE_PORT",
# Pooled workers load the extension registry once at spawn and never
# reload it per task; the save-time server reload keeps the skills UI
# fresh, but agent tasks see the new repo only after a restart.
"OUROBOROS_SKILLS_REPO_PATH",
"LOCAL_MODEL_SOURCE",
"LOCAL_MODEL_FILENAME",
"LOCAL_MODEL_PORT",
"LOCAL_MODEL_N_GPU_LAYERS",
"LOCAL_MODEL_CONTEXT_LENGTH",
"LOCAL_MODEL_CHAT_FORMAT",
# Background cognition reads these at consciousness __init__, so a change
# only takes effect after restart (Phase 4 Evolution settings group).
"OUROBOROS_BG_WAKEUP_MIN",
"OUROBOROS_BG_WAKEUP_MAX",
"OUROBOROS_BG_MAX_ROUNDS",
})
from ouroboros.settings_scales import (
IMMEDIATE_SETTINGS as _IMMEDIATE_KEYS,
RESTART_REQUIRED_SETTINGS as _RESTART_REQUIRED_KEYS,
)
def _classify_settings_changes(
@ -247,52 +263,23 @@ def _effect_buckets(all_changed: list) -> tuple:
immediate_changed = [k for k in all_changed if k in _IMMEDIATE_KEYS]
next_task_changed = [
k for k in all_changed
if k not in _IMMEDIATE_KEYS and k not in _RESTART_REQUIRED_KEYS
if k not in _IMMEDIATE_KEYS and k not in _RESTART_REQUIRED_KEYS and k != "OUROBOROS_RUNTIME_MODE"
]
return immediate_changed, next_task_changed
def _merge_settings_payload(current: Dict[str, Any], body: Dict[str, Any]) -> Dict[str, Any]:
merged = {k: v for k, v in current.items()}
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
skipped = {"OUROBOROS_CONTEXT_MODE", "OUROBOROS_CONTEXT_MODE_AUTO_LOW"} | _ENDPOINT_AUTHORED_SETTINGS
if not runtime_mode_at_least(get_runtime_mode(), "cyber_pro"):
skipped |= {"OUROBOROS_RUNTIME_MODE", "OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS", "OUROBOROS_SAFETY_MODE"}
# Context/review scope stays owner-controlled. Cyber may configure other
# controls through this same writer; install-time provenance remains host-owned.
for key in _SETTINGS_DEFAULTS:
# Owner-only keys: loopback HTTP settings cannot set them. Runtime mode is
# a privilege scope; context mode is a cognitive-horizon knob the agent
# must not lower itself (BIBLE P1). Both flow through dedicated owner endpoints.
#
# NOTE (v6.21.0): OUROBOROS_ALLOW_MUTATIVE_SUBAGENTS is also owner-controlled,
# but intentionally rides this generic owner path (it is NOT merge-skipped) so
# the Settings UI can set it without dedicated-endpoint ceremony. The agent
# cannot self-elevate it: shell (_detect_mutative_toggle_self_change), browser
# JS (_blocks_mutative_toggle_js), and data_write to settings.json
# (DATA_WRITE_BLOCKED) all block agent-originated changes, and it defaults to
# ON in advanced/pro anyway (self-enable is only meaningful in light, which
# sandboxes live-repo writes regardless). Owner-decided tradeoff; do not
# "promote" it to the skip-list without owner sign-off (it would break the UI).
# NOTE: OUROBOROS_POST_TASK_EVOLUTION (the V4 envelope enable) intentionally
# rides this generic owner path too (like ALLOW_MUTATIVE_SUBAGENTS), so the
# Phase 4 Evolution settings UI can toggle it On/Off. The agent cannot
# self-enable it: shell (_detect_evolution_owner_control_self_change), browser JS
# (_blocks_post_task_evolution_js), the POST /api/settings route guard, and
# data_write to settings.json (DATA_WRITE_BLOCKED) all block agent-originated
# changes, and SAFETY.md forbids it. Owner-decided tradeoff; do not merge-skip it
# (it would break the UI toggle).
if key in {
"OUROBOROS_RUNTIME_MODE",
"OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS",
"OUROBOROS_CONTEXT_MODE",
# One-window false provenance tombstone. Generic settings never authors
# context intent; the dedicated owner endpoint writes the pair atomically.
"OUROBOROS_CONTEXT_MODE_AUTO_LOW",
# v6.54.3: LLM-safety-supervisor coverage (full/light/off) is likewise an
# immune-system control — a generic settings write must not lower it. It
# flows ONLY through the dedicated audited owner endpoint
# (api_owner_safety_mode); save_settings additionally ratchets lowering.
"OUROBOROS_SAFETY_MODE",
# The install-time facts join from config's ENDPOINT_AUTHORED_SETTINGS just
# below: POST /api/onboarding/complete alone writes them, beside what they
# record. This blocks the REQUEST BODY; the same set keeps them off the
# environment in both directions, so no other route can author them either.
} | _ENDPOINT_AUTHORED_SETTINGS:
if key in skipped:
continue
if key not in body:
continue
@ -435,7 +422,7 @@ def _api_owner_runtime_mode_sync(request: Request, body: Any) -> JSONResponse:
raw_mode = str((body or {}).get("mode") or "").strip().lower()
if raw_mode not in set(_config.VALID_RUNTIME_MODES):
return unsaved_error("'mode' must be one of: light, advanced, pro", 400)
return unsaved_error("'mode' must be one of: light, advanced, pro, cyber_pro", 400)
# The digest is taken BEFORE the read that decides, so a write landing between the
# two is refused rather than silently reverted by this request's write.
digest = settings_document_digest()
@ -813,11 +800,9 @@ def _api_owner_context_mode_sync(request: Request, body: Any) -> JSONResponse:
async def api_owner_safety_mode(request: Request) -> JSONResponse:
"""Persist the owner-selected LLM-safety-supervisor coverage (full | light | off).
Owner-only + audited (v6.54.3): safety coverage is an immune-system control, so
it is merge-skipped from the generic /api/settings path and its lowering is
ratcheted in save_settings — ONLY this dedicated, audited endpoint may lower it.
The deterministic registry sandbox, protected paths, and light-mode guards run
in every mode (BIBLE P3: the LLM supervisor is a layer, not the floor)."""
This dedicated owner path is audited. Ordinary modes skip this control in
generic settings saves; Cyber also has audited configuration authority there.
The independent Access and review-scope/enforcement controls remain in force."""
body = await _json_body_or_empty(request)
# Off the event loop, under the document lock (held inside): a slow
# generic save must not be able to freeze the loop THROUGH this
@ -999,6 +984,19 @@ async def api_settings_get(request: Request) -> JSONResponse:
except (ValueError, OSError):
port = _default_port(request)
meta = _build_network_meta(_current_bind_host(request), port)
# Keep the three owner-facing policy axes honest after reload. The values
# on the document are the pending/configured choices; process state is the
# effective value this server can currently report. Runtime access is
# restart-bound, while Supervisor and Review are picked up for new tasks by
# the existing settings effect path. This is presentation metadata only,
# not a second policy store.
try:
meta["policy_state"] = _build_policy_state(settings)
except Exception:
# A settings read must stay available even if an optional projection
# helper is unavailable during startup. The persisted values remain
# the ordinary response fields and are still masked below.
log.debug("Could not build settings policy-state projection", exc_info=True)
meta["custom_secret_keys"] = sorted(
key for key in settings
if key not in SECRET_SETTING_KEYS
@ -1189,6 +1187,53 @@ def _check_reviewer_slots_against_incoming_roster(body: dict) -> str:
)
def _network_settings_error(request: Request, current: dict, old_settings: dict) -> JSONResponse | None:
"""Validate the existing save-time bind/password contract before persistence."""
try:
from ouroboros.server_auth import is_loopback_host
desired_host = str(current.get("OUROBOROS_SERVER_HOST") or "").strip()
desired_password = str(current.get("OUROBOROS_NETWORK_PASSWORD") or "").strip()
trust_unauth = _trust_nonlocal_bind_without_password_enabled()
allowed_saved_hosts = {"", "127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0", "::", "[::]"}
if desired_host and desired_host not in allowed_saved_hosts:
return unsaved_error(
"Server Bind Host in Settings supports localhost or wildcard "
"binds only (127.0.0.1 or 0.0.0.0). Specific LAN IP binds "
"are manual/env-only so the desktop launcher can keep using "
"a reliable loopback health check.",
400,
)
if desired_host and not is_loopback_host(desired_host) and not desired_password and not trust_unauth:
return unsaved_error(
"Setting a non-localhost Server Bind Host through the web UI "
"requires a Network Password in the same save. For manual "
"trusted-lab/Docker setups, stop Ouroboros and edit "
"settings.json or environment variables directly.",
400,
)
current_effective_host = (
str(_current_bind_host(request) or "").strip()
or str(os.environ.get("OUROBOROS_SERVER_HOST") or "").strip()
)
old_password = str(old_settings.get("OUROBOROS_NETWORK_PASSWORD") or "").strip()
if (
current_effective_host
and not is_loopback_host(current_effective_host)
and old_password
and not desired_password
and not trust_unauth
):
return unsaved_error(
"Cannot clear Network Password while the running server is "
"still bound to a non-localhost interface. First save a "
"loopback Server Bind Host and restart, then clear the password.",
400,
)
except Exception:
log.warning("Could not validate network bind settings", exc_info=True)
return None
def _api_settings_post_locked(request: Request, body: Any) -> JSONResponse:
# Everything below the write is a POST-commit step. The broad handler at the
# bottom used to answer a failure there with "400, nothing saved" while the
@ -1281,6 +1326,13 @@ def _api_settings_post_locked(request: Request, body: Any) -> JSONResponse:
old_settings.get("MCP_SERVERS"),
)
current = _merge_settings_payload(old_effective_settings, body)
from ouroboros.runtime_mode_policy import runtime_mode_at_least
requested_runtime_mode = _norm_runtime_mode(current.get("OUROBOROS_RUNTIME_MODE"))
runtime_authored = "OUROBOROS_RUNTIME_MODE" in body and runtime_mode_at_least(current_runtime_mode, "cyber_pro")
runtime_changed = runtime_authored and requested_runtime_mode != pending_runtime_mode
if runtime_authored:
pending_runtime_mode = requested_runtime_mode
minimax_region = str(current.get("MINIMAX_REGION") or "").strip().lower()
if minimax_region and minimax_region not in MINIMAX_REGION_ENDPOINTS:
return unsaved_error("MINIMAX_REGION must be global_en or cn_zh.", 400)
@ -1293,48 +1345,9 @@ def _api_settings_post_locked(request: Request, body: Any) -> JSONResponse:
current["OUROBOROS_SKILLS_REPO_PATH"] = str(
current.get("OUROBOROS_SKILLS_REPO_PATH") or ""
).strip()
try:
from ouroboros.server_auth import is_loopback_host
desired_host = str(current.get("OUROBOROS_SERVER_HOST") or "").strip()
desired_password = str(current.get("OUROBOROS_NETWORK_PASSWORD") or "").strip()
trust_unauth = _trust_nonlocal_bind_without_password_enabled()
allowed_saved_hosts = {"", "127.0.0.1", "localhost", "::1", "[::1]", "0.0.0.0", "::", "[::]"}
if desired_host and desired_host not in allowed_saved_hosts:
return unsaved_error(
"Server Bind Host in Settings supports localhost or wildcard "
"binds only (127.0.0.1 or 0.0.0.0). Specific LAN IP binds "
"are manual/env-only so the desktop launcher can keep using "
"a reliable loopback health check.",
400,
)
if desired_host and not is_loopback_host(desired_host) and not desired_password and not trust_unauth:
return unsaved_error(
"Setting a non-localhost Server Bind Host through the web UI "
"requires a Network Password in the same save. For manual "
"trusted-lab/Docker setups, stop Ouroboros and edit "
"settings.json or environment variables directly.",
400,
)
current_effective_host = (
str(_current_bind_host(request) or "").strip()
or str(os.environ.get("OUROBOROS_SERVER_HOST") or "").strip()
)
old_password = str(old_settings.get("OUROBOROS_NETWORK_PASSWORD") or "").strip()
if (
current_effective_host
and not is_loopback_host(current_effective_host)
and old_password
and not desired_password
and not trust_unauth
):
return unsaved_error(
"Cannot clear Network Password while the running server is "
"still bound to a non-localhost interface. First save a "
"loopback Server Bind Host and restart, then clear the password.",
400,
)
except Exception:
log.warning("Could not validate network bind settings", exc_info=True)
network_error = _network_settings_error(request, current, old_settings)
if network_error is not None:
return network_error
current, provider_defaults_changed, provider_default_keys = apply_runtime_provider_defaults(current)
if str(current.get("LOCAL_MODEL_SOURCE", "") or "").strip() and not has_startup_ready_provider(current):
return unsaved_error("Local-only setups must route at least one model to the local runtime.", 400)
@ -1343,6 +1356,9 @@ def _api_settings_post_locked(request: Request, body: Any) -> JSONResponse:
if str(current.get(k, "") or "") != str(old_effective_settings.get(k, "") or "")
]
restart_keys = _classify_settings_changes(old_effective_settings, current)
if runtime_changed:
all_changed.append("OUROBOROS_RUNTIME_MODE")
restart_keys.append("OUROBOROS_RUNTIME_MODE")
# Snapshot BEFORE the save lands: only a task already started at that
# moment keeps the previous configuration. Measuring after the write
@ -1361,7 +1377,13 @@ def _api_settings_post_locked(request: Request, body: Any) -> JSONResponse:
# persistent Low/Max untouched and exact-route fitting happens at task dispatch.
_owner_write_settings(
settings_to_save,
authored_keys=("OUROBOROS_SAFETY_MODE",) if "OUROBOROS_SAFETY_MODE" in all_changed else (),
boundary=boundary)
control_changes = {key: {"old": raw_old_settings.get(key), "new": settings_to_save.get(key)}
for key in ("OUROBOROS_RUNTIME_MODE", "OUROBOROS_SAFETY_MODE", "OUROBOROS_AUTO_GRANT_REVIEWED_SKILLS")
if key in all_changed}
if control_changes:
_owner_audit(request, "settings_controls", {"changes": control_changes})
boundary.at("environment projection")
_apply_settings_to_env(current)
boundary.at("supervisor start")

View file

@ -185,6 +185,8 @@ def _review_projection(loaded: Any, *, stale: bool | None = None) -> Dict[str, A
"status": normalize_skill_review_status(loaded.review.status),
"stale": (loaded.review.is_stale_for(loaded.content_hash) if stale is None else bool(stale)),
"profile": str(getattr(loaded.review, "review_profile", "") or ""),
"reviewed_content_hash": loaded.review.reviewed_content_hash or loaded.review.content_hash,
"author_disposition": dict(loaded.review.author_disposition),
}

View file

@ -516,7 +516,7 @@ def _create_task_from_body(request: Request, body: Any) -> JSONResponse:
if task_type in {"evolution", "review", "deep_self_review"}:
return json_error(
f"task type {task_type!r} is internal-only and cannot be created via the task API "
"(use /evolve or /review); evolution additionally requires advanced/pro runtime mode",
"(use /evolve or /review); evolution additionally requires advanced/pro/cyber_pro runtime mode",
400,
)
if workspace_root and task_type != "task":

View file

@ -130,8 +130,19 @@ def _git_config_readonly(args: list[str]) -> bool:
_GH_AUTH_MUTATING_VERBS = frozenset({"login", "logout", "refresh", "switch", "setup-git"})
def gh_shell_block_reason(raw_cmd: Any) -> str:
"""Positional gh policy: judged only where `gh` is a segment's command head."""
def gh_shell_block_reason(raw_cmd: Any, *, runtime_mode: str = "") -> str:
"""Positional gh policy, with Cyber Pro owner-authority escape.
The argv/segment parser remains the source of the ordinary-mode policy.
Cyber Pro is the explicit owner-selected mode that permits technical
authentication and repository setup attempts; the tool's factual result
(including provider/OS failure) is still returned unchanged.
"""
if runtime_mode:
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(runtime_mode, "cyber_pro"):
return ""
for segment in shell_segments(raw_cmd):
_env, command = collect_leading_env(segment)
if not command:
@ -139,7 +150,7 @@ def gh_shell_block_reason(raw_cmd: Any) -> str:
head = pathlib.PurePath(str(command[0])).name.lower()
if head in {"bash", "sh", "zsh"}:
inline = shell_command_string(command)
if inline and (nested := gh_shell_block_reason(inline)):
if inline and (nested := gh_shell_block_reason(inline, runtime_mode=runtime_mode)):
return nested
continue
if head != "gh":

View file

@ -8,7 +8,6 @@ import hashlib # noqa: F401 (prior import surface)
import inspect # noqa: F401 (prior import surface)
import json
import logging
import os
import re
import threading # noqa: F401 (prior import surface)
import time # noqa: F401 (prior import surface)
@ -134,6 +133,7 @@ from ouroboros.usage_accounting import (
usage_scope,
)
from ouroboros.utils import in_worker_process, sanitize_tool_result_for_log # noqa: F401
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -160,7 +160,7 @@ class LLMClient(
base_url: str = "https://openrouter.ai/api/v1",
):
self._api_key_override = api_key
self._api_key = api_key or os.environ.get("OPENROUTER_API_KEY", "")
self._api_key = api_key or runtime_setting("OPENROUTER_API_KEY", "")
self._base_url = base_url
self._client = None
self._client_api_key: Optional[str] = None
@ -635,12 +635,12 @@ class LLMClient(
def default_model(self) -> str:
"""Return the single default model from env. LLM switches via tool if needed."""
return os.environ.get("OUROBOROS_MODEL", OPENROUTER_DEFAULTS["main"])
return runtime_setting("OUROBOROS_MODEL", OPENROUTER_DEFAULTS["main"])
def available_models(self) -> List[str]:
"""Return list of available models from env (for switch_model tool schema)."""
main = self.default_model()
light = os.environ.get("OUROBOROS_MODEL_LIGHT", "")
light = runtime_setting("OUROBOROS_MODEL_LIGHT", "")
models = [main]
if light and light != main:
models.append(light)

View file

@ -12,7 +12,6 @@ every caller consumes.
from __future__ import annotations
import logging
import os
from typing import Any, Dict, List, Optional, Set, Tuple
from ouroboros.llm_attempt import supports_message_cache_control
@ -29,6 +28,7 @@ from ouroboros.request_wire_recovery import (
note_provider_metadata_drop_fields,
)
from ouroboros.utils import sanitize_tool_result_for_log
from ouroboros.config import runtime_setting
# The moved warnings keep the logger identity they were emitted under.
@ -61,15 +61,15 @@ class _OpenAICompatibleLaneMixin:
@staticmethod
def _openrouter_main_web_search_tool() -> Optional[Dict[str, Any]]:
mode = str(os.environ.get("OUROBOROS_MAIN_WEB_SEARCH") or "off").strip().lower()
mode = str(runtime_setting("OUROBOROS_MAIN_WEB_SEARCH") or "off").strip().lower()
if mode not in {"openrouter", "openrouter_server", "server", "on", "true", "1"}:
return None
engine = str(os.environ.get("OUROBOROS_MAIN_WEB_SEARCH_ENGINE") or "auto").strip() or "auto"
engine = str(runtime_setting("OUROBOROS_MAIN_WEB_SEARCH_ENGINE") or "auto").strip() or "auto"
parameters: Dict[str, Any] = {}
if engine != "auto":
parameters["engine"] = engine
try:
max_total = int(os.environ.get("OUROBOROS_MAIN_WEB_SEARCH_MAX_TOTAL_RESULTS", "") or 0)
max_total = int(runtime_setting("OUROBOROS_MAIN_WEB_SEARCH_MAX_TOTAL_RESULTS", "") or 0)
except ValueError:
max_total = 0
if max_total > 0:
@ -235,7 +235,7 @@ class _OpenAICompatibleLaneMixin:
for m in messages
]
effort = normalize_reasoning_effort(reasoning_effort)
raw_return_reasoning = os.environ.get("OUROBOROS_RETURN_REASONING")
raw_return_reasoning = runtime_setting("OUROBOROS_RETURN_REASONING")
return_reasoning = (
True if raw_return_reasoning is None
else str(raw_return_reasoning).strip().lower() not in _FALSE_LIKE_ENV_VALUES

View file

@ -11,11 +11,11 @@ no cost.
from __future__ import annotations
import logging
import os
import time
from typing import Any, Dict, Optional, Tuple
from ouroboros.provider_models import normalize_model_identity
from ouroboros.config import runtime_setting
# The moved warnings keep the logger identity they were emitted under.
@ -155,7 +155,7 @@ def fetch_cloudru_pricing(*, timeout_sec: float = 5.0) -> Dict[str, Tuple[Option
import logging
log = logging.getLogger("ouroboros.llm")
api_key = (os.environ.get("CLOUDRU_FOUNDATION_MODELS_API_KEY", "") or "").strip()
api_key = (runtime_setting("CLOUDRU_FOUNDATION_MODELS_API_KEY", "") or "").strip()
if not api_key:
return {}
try:
@ -164,10 +164,10 @@ def fetch_cloudru_pricing(*, timeout_sec: float = 5.0) -> Dict[str, Tuple[Option
return {}
base_url = (
os.environ.get("CLOUDRU_FOUNDATION_MODELS_BASE_URL", "") or ""
runtime_setting("CLOUDRU_FOUNDATION_MODELS_BASE_URL", "") or ""
).strip() or "https://foundation-models.api.cloud.ru/v1"
try:
rate = float(os.environ.get("OUROBOROS_RUB_USD_RATE", ""))
rate = float(runtime_setting("OUROBOROS_RUB_USD_RATE", ""))
except (TypeError, ValueError):
return {}
if rate <= 0:

View file

@ -28,6 +28,7 @@ from ouroboros.provider_models import (
parse_claudexor_model,
resolve_minimax_base_url,
)
from ouroboros.config import runtime_setting
_OR_PROVIDER_PRESETS = {
@ -40,7 +41,7 @@ _OR_PROVIDER_PRESETS = {
def _resolve_or_provider() -> Dict[str, Any]:
"""Resolve ``OUROBOROS_OR_PROVIDER`` (a preset name or a raw JSON object) into an
OpenRouter ``provider`` routing dict. Empty/unset/invalid -> ``{}`` (no routing)."""
raw = (os.environ.get("OUROBOROS_OR_PROVIDER") or "").strip()
raw = (runtime_setting("OUROBOROS_OR_PROVIDER") or "").strip()
if not raw:
return {}
preset = _OR_PROVIDER_PRESETS.get(raw.lower())
@ -307,7 +308,7 @@ class _ProviderRoutingMixin:
def configured(key: str, default: Any = "") -> Any:
if explicit_settings:
return settings.get(key, default) # type: ignore[union-attr]
return os.environ.get(key, default)
return runtime_setting(key, default)
provider, resolved_model = self._parse_provider_model(model)
usage_model = self._qualified_model_name(provider, resolved_model)
@ -446,7 +447,7 @@ class _ProviderRoutingMixin:
current_api_key = configured("OPENROUTER_API_KEY", "") if explicit_settings else self._api_key_override
if current_api_key is None:
current_api_key = os.environ.get("OPENROUTER_API_KEY", "")
current_api_key = runtime_setting("OPENROUTER_API_KEY", "")
return {
"provider": "openrouter",
"resolved_model": resolved_model,

View file

@ -2,10 +2,11 @@
from __future__ import annotations
from ouroboros.config import runtime_setting
import functools # noqa: F401 -- the loop module keeps its historical import surface for the L-B leaves
import json # noqa: F401 -- the loop module keeps its historical import surface for the L-B leaves
import hashlib # noqa: F401 -- the loop module keeps its historical import surface for the L-B leaves
import os
import queue
import pathlib
import time # noqa: F401 -- the loop module keeps its historical import surface for the L-B leaves
@ -344,7 +345,7 @@ def _resolve_loop_max_rounds(ctx: Any = None) -> int:
default = int(SETTINGS_DEFAULTS["OUROBOROS_MAX_ROUNDS"])
try:
configured = max(1, int(os.environ.get("OUROBOROS_MAX_ROUNDS", str(default))))
configured = max(1, int(runtime_setting("OUROBOROS_MAX_ROUNDS", str(default))))
except (ValueError, TypeError):
log.warning("Invalid OUROBOROS_MAX_ROUNDS, defaulting to %s", default)
configured = default
@ -376,7 +377,7 @@ def run_llm_loop(
active_effort = initial_effort
local_override = getattr(ctx, "task_use_local_override", None)
active_use_local = (bool(local_override) if local_override is not None else
os.environ.get("USE_LOCAL_MAIN", "").lower() in ("true", "1"))
runtime_setting("USE_LOCAL_MAIN", "").lower() in ("true", "1"))
# Unknown routes get one honest call; no synthetic short-window capacity.
_preferred_context_mode = get_context_mode()
context_fit_plan = getattr(ctx, "context_fit_plan", None)

View file

@ -565,6 +565,7 @@ ACCEPTANCE_DECISION_REASONS = (
"review_degraded",
"fence_reopen_failed",
"infra_failure",
"author_finish",
# The pacing/wallet reason two branches below already STAMP (`pass_reason ==
# REASON_REVIEW_CYCLES_EXHAUSTED`); it was missing from the closed set, so a
# spent shared cap shipped a reason no reader could validate.
@ -590,9 +591,8 @@ def _set_acceptance_decision(llm_trace: Dict[str, Any], decision: Dict[str, Any]
owner-facing states (``ACCEPTANCE_DECISION_STATUSES``) plus a typed
``reason`` naming WHICH exit. A status outside the trio fails closed to
``finalized_unaccepted`` with its raw token surviving as ``reason`` — no
fourth state, no lost token. The agent's stance (``agent_disposition``/
``agent_rationale``) carries forward, never overwritten (after P4.1 the
agent writes no status at all)."""
fourth state, no lost token. The author's historical stance survives;
owner/evidence supersession consumes its controlling finish intent."""
previous = llm_trace.get("acceptance_decision") if isinstance(llm_trace.get("acceptance_decision"), dict) else {}
merged = dict(decision)
status = str(merged.get("status") or "")
@ -601,18 +601,48 @@ def _set_acceptance_decision(llm_trace: Dict[str, Any], decision: Dict[str, Any]
merged["status"] = ACCEPTANCE_FINALIZED_UNACCEPTED
reason = reason or status or ACCEPTANCE_REASON_UNSPECIFIED
merged["reason"] = reason
for key in ("agent_disposition", "agent_rationale"):
for key in ("agent_disposition", "agent_rationale", "author_disposition"):
if previous.get(key) and not merged.get(key):
merged[key] = previous.get(key)
if reason not in {"owner_followup", "evidence_refresh", "author_finish"} and not (
reason == "delivery_binding_superseded" and previous.get("reason") == "author_finish"
) and previous.get("agent_finish_intent"):
merged["agent_finish_intent"] = previous["agent_finish_intent"]
llm_trace["acceptance_decision"] = merged
# A full applied-review source includes the host's actual decision, not
# only the provider's earlier response. The decision above stays authority.
for run in reversed(llm_trace.get("review_runs") or []):
if isinstance(run, dict) and run.get("authority") == "host_root":
author = merged.get("author_disposition")
if reason == "author_finish" and isinstance(author, dict) and author.get("subject_hash") != run.get("binding_hash"):
break # A revised author subject is a task fact, not this older panel's decision.
run["applied_decision"] = dict(merged)
break
def merge_agent_acceptance_stance(trace: Dict[str, Any], decision: dict, ctx: Any) -> None:
"""Record one explicit stance after feedback, separately from the host verdict."""
previous = trace.get("acceptance_decision")
merged = dict(previous) if isinstance(previous, dict) else {}
merged.setdefault("source", "agent_task_acceptance_review_tool")
merged["agent_disposition"] = str(decision.get("disposition") or "")
merged["agent_rationale"] = truncate_review_artifact(str(decision.get("rationale") or ""), limit=500)
merged.pop("agent_finish_intent", None)
feedback = next((run for run in reversed(trace.get("review_runs") or [])
if isinstance(run, dict) and run.get("authority") == "host_root"
and run.get("feedback_delivered")), None)
if ctx is not None and feedback and decision.get("explicit_finish") is True and merged["agent_rationale"].strip():
from ouroboros.loop_delivery import delivery_evidence_fingerprint
merged["agent_finish_intent"] = {
"review_binding_hash": str(feedback.get("binding_hash") or ""),
"tool_count": len(trace.get("tool_calls") or []),
"owner_directives": len(getattr(ctx, "_owner_directives", []) or []),
"evidence_fingerprint": delivery_evidence_fingerprint(ctx, trace),
}
trace["acceptance_decision"] = merged
def _collect_acceptance_obligations(llm_trace: Dict[str, Any], result: Any) -> None:
"""Typed PER-TASK obligations from critical contributing findings (v6.54.4).

View file

@ -516,6 +516,50 @@ def _set_applied_host_acceptance_impact(
)
def _finish_advisory_author(ctx: _TaskAcceptanceContext) -> bool:
"""Finish a current explicit response to delivered criticism, without another panel."""
if _loop().get_review_enforcement() != "advisory":
return False
stance = ctx.llm_trace.get("acceptance_decision") or {}
intent = stance.get("agent_finish_intent") or {}
feedback = next((run for run in reversed(ctx.llm_trace.get("review_runs") or [])
if isinstance(run, dict) and run.get("authority") == "host_root"
and run.get("feedback_delivered")), None)
disposition = str(stance.get("agent_disposition") or "")
from ouroboros.loop_delivery import delivery_evidence_fingerprint
if (not feedback or not intent or disposition not in {"accepted", "rejected", "partial", "deferred"}
or intent.get("review_binding_hash") != feedback.get("binding_hash")
or intent.get("tool_count") != len(ctx.llm_trace.get("tool_calls") or [])
or intent.get("owner_directives") != len(getattr(ctx.tools._ctx, "_owner_directives", []) or [])
or intent.get("evidence_fingerprint") != delivery_evidence_fingerprint(ctx.tools._ctx, ctx.llm_trace)):
return False
from ouroboros.review_records import build_author_disposition
author = build_author_disposition(
disposition=disposition, rationale=str(stance.get("agent_rationale") or ""),
subject_hash=ctx.review_binding["binding_hash"],
reviewer_signal=str(feedback.get("aggregate_signal") or "DEGRADED"), enforcement="advisory",
)
if not _loop()._end_task_acceptance_fence(ctx.tools._ctx, outcome="terminal"):
_loop()._supersede_task_acceptance_for_owner_followup(ctx.tools._ctx, ctx.llm_trace)
return True
ctx.tools._ctx._task_acceptance_reviewed = True
_loop()._mark_root_acceptance_checkpoint(
ctx.tools._ctx, ctx.llm_trace, status=author["reviewer_signal"].lower(), pass_index=ctx.passes_done,
)
ctx.llm_trace["review_decision"].update({"binding_hash": ctx.review_binding["binding_hash"], "author_finish": True})
_loop()._set_acceptance_decision(ctx.llm_trace, {
"status": ACCEPTANCE_FINALIZED_UNACCEPTED, "reason": "author_finish",
"source": "task_acceptance_review", "author_disposition": author,
"rationale": "The author finished after independent feedback; the current subject is author-accepted, not reviewer PASS.",
"reviewer_signal": author["reviewer_signal"],
"reviewer_binding_hash": feedback.get("binding_hash"),
})
ctx.emit_progress(f"Task acceptance review: {author['reviewer_signal']} — author finished advisory review ({disposition}); raw findings retained.")
return True
def _apply_task_acceptance_result(
ctx: _TaskAcceptanceContext,
result: Any,
@ -525,50 +569,35 @@ def _apply_task_acceptance_result(
) -> bool:
"""Apply one panel result; return whether the agent must take another round."""
from ouroboros.review_substrate import (
DIALOGUE_TERMINAL_STATUSES,
aggregate_dialogue_status,
build_improvement_capsule,
dissent_findings,
task_acceptance_is_clean,
DIALOGUE_TERMINAL_STATUSES, aggregate_dialogue_status,
build_improvement_capsule, dissent_findings, task_acceptance_is_clean,
)
if record_run:
_record_host_acceptance_run(ctx, result)
dissent = dissent_findings(result)
blocking_lane = ctx.mode == "required" and _loop().get_review_enforcement() == "blocking"
# A REUSED panel (unchanged binding) is the SAME reviewer act applied
# again: re-collecting would mutate reviewer-authored state with no new
# input, and the shifted evidence revision would buy a fresh paid panel
# for a byte-identical resubmit (fable r2 #1); rows already collected.
# Reused panels already have obligations; collecting twice changes evidence
# revision and could buy a new panel for an identical resubmission (fable r2 #1).
if blocking_lane and not reused:
_loop()._collect_acceptance_obligations(ctx.llm_trace, result)
open_obligations = _loop()._open_acceptance_obligations(ctx.llm_trace) if blocking_lane else []
# v6.74.0 (A1): the capsule leads with the verdict, the concrete open
# obligation ids, and the pre-rendered rails line (money/time/rounds/passes).
# Capsule: verdict, open obligation IDs, then money/time/round/pass limits.
capsule = build_improvement_capsule(
result,
rails_line=ctx.rails_line,
open_obligations=open_obligations,
)
# v6.74.0 (A5): the reviewers' typed dialogue judgement, reduced over the
# CONTRIBUTING actors with the panel's own quorum; persisted for audit on
# the authoritative run record whatever branch applies below. `inconclusive`
# (no well-formed vote at all) grants the dialogue NO authority: it is not a
# terminal verdict and not a licence to continue — the existing non-dialogue
# terminals below decide, exactly as they did before the dialogue existed.
# Persist contributing actors' dialogue judgment using the panel's quorum.
# Inconclusive votes grant no authority; the non-dialogue terminals decide.
dialogue = aggregate_dialogue_status(
result, quorum=_acceptance_dialogue_quorum(result),
)
_attach_dialogue_to_host_run(ctx.llm_trace, dialogue)
dialogue_terminal = dialogue["status"] in DIALOGUE_TERMINAL_STATUSES
if reused and getattr(result, "replayed_from_superseded", False):
# A run superseded by an evidence revision replays ONLY into the typed
# identical-refusal terminal — never into clean-PASS authorization: its
# verdict predates the evidence change, so re-accepting would stamp a
# stale PASS (and the trace's superseded rows would contradict the
# applied decision — the delivery binding could never match). The
# refusal is conservative and consistent: nothing new was bought,
# nothing stale is re-authorized.
# Evidence superseded this run: replay only its identical-refusal terminal,
# never a stale PASS that contradicts the trace and current delivery binding.
return _refuse_identical_acceptance(
ctx, result,
dialogue=dialogue, dissent=bool(dissent), open_obligations=open_obligations,
@ -608,28 +637,18 @@ def _apply_task_acceptance_result(
required_blocking=blocking_lane,
ctx=ctx.tools._ctx,
)
# A DEGRADED panel (no valid verdict quorum) cannot "judge" the dialogue:
# a lone terminal vote from the one contributing slot must NOT shadow the
# review_degraded path below, which is the only surface carrying the
# per-slot causes and degraded_reasons the v6.70.0 honesty invariant (P1)
# requires. Letting the dialogue-terminal branch fire here recorded a false
# "reviewer quorum judged" rationale and silently dropped those causes.
# DEGRADED has no verdict quorum: a lone terminal vote cannot replace the
# review_degraded path below, which preserves per-slot failure causes (P1).
if dialogue_terminal and str(result.aggregate_signal or "DEGRADED").upper() != "DEGRADED":
# v6.74.0 (A5): a reviewer quorum judged the dialogue no longer
# actionable (unreachable_here / stable_disagreement). Finalize via
# the EXISTING honest path recording BOTH positions in one
# owner-visible line — reviewer authorship, not a host timer.
# Quorum judged the dialogue unreachable/stable; record both positions.
ctx.tools._ctx._task_acceptance_reviewed = True
_loop()._end_task_acceptance_fence(ctx.tools._ctx, outcome="terminal")
_loop()._mark_root_acceptance_checkpoint(
ctx.tools._ctx,
ctx.llm_trace,
status=str(result.aggregate_signal or "DEGRADED").lower(),
pass_index=ctx.passes_done,
ctx.tools._ctx, ctx.llm_trace,
status=str(result.aggregate_signal or "DEGRADED").lower(), pass_index=ctx.passes_done,
)
_loop()._set_acceptance_decision(ctx.llm_trace, {
# The with/without-obligations distinction moves from the status token to
# the `open_obligations` id list this branch already records.
# Open obligations live in their ID list, not a different status token.
"status": ACCEPTANCE_FINALIZED_UNACCEPTED,
"reason": "dialogue_terminal",
"source": "task_acceptance_review",
@ -672,6 +691,10 @@ def _apply_task_acceptance_result(
if ctx.content and ctx.content.strip():
ctx.messages.append({"role": "assistant", "content": ctx.content})
_loop()._append_or_merge_user_message(ctx.messages, capsule)
for run in reversed(ctx.llm_trace.get("review_runs") or []):
if isinstance(run, dict) and run.get("authority") == "host_root":
run["feedback_delivered"] = True
break
ctx.emit_progress(
f"Task acceptance review: {result.aggregate_signal} — improvement note fed back."
)
@ -680,10 +703,8 @@ def _apply_task_acceptance_result(
ctx.tools._ctx._task_acceptance_reviewed = True
_loop()._end_task_acceptance_fence(ctx.tools._ctx, outcome="terminal")
_loop()._mark_root_acceptance_checkpoint(
ctx.tools._ctx,
ctx.llm_trace,
status=str(result.aggregate_signal or "DEGRADED").lower(),
pass_index=ctx.passes_done,
ctx.tools._ctx, ctx.llm_trace,
status=str(result.aggregate_signal or "DEGRADED").lower(), pass_index=ctx.passes_done,
)
if _loop()._dispose_obligations_on_clean_pass(
ctx.llm_trace, result, open_obligations, bool(dissent),
@ -702,13 +723,9 @@ def _apply_task_acceptance_result(
"degraded_reasons": list(getattr(result, "degraded_reasons", []) or []),
"open_obligations": [str(item.get("id")) for item in open_obligations],
})
# Per-slot causes were always in the structured decision; the
# owner-visible line said only "no valid quorum", forcing a dig
# through task_results for WHICH slot failed and why (v6.70.0).
# Show the slot failure causes beside the verdict, not only in task_results.
_degraded_reasons = list(getattr(result, "degraded_reasons", []) or [])
# Bounded PREVIEW for the chat line only — the complete causes live in
# the structured decision record (owner-facing full copy, per the
# v6.70.0 honesty invariant).
# Chat preview only; the structured decision keeps every complete cause.
_reason_note = "; ".join(
truncate_review_artifact(str(r), limit=300).replace("\n", " ")
for r in _degraded_reasons[:4]
@ -781,12 +798,8 @@ def _apply_task_acceptance_result(
ctx.emit_progress(f"Task acceptance review: {result.aggregate_signal} (no changes suggested).")
else:
_loop()._set_acceptance_decision(ctx.llm_trace, {
# Round-9 CRITICAL 1: fall-through AFTER
# `task_acceptance_is_clean` refused the panel, so it cannot mint
# `accepted` (reserved for clean acceptance). Reachable: a
# reviewer claims `solved` with a MISSING criterion and the
# improvement cap spent — nothing actionable, yet not "accepted";
# the typed reason names WHY; tier honesty rides `outcome_tier`.
# A non-clean panel cannot mint accepted, even when solved lacks a
# criterion and no revision remains. Reason/outcome_tier preserve why.
"status": ACCEPTANCE_FINALIZED_UNACCEPTED,
"reason": "no_actionable_changes",
"source": "task_acceptance_review",
@ -1241,13 +1254,6 @@ def _run_task_acceptance_review_once(
)
budget_snapshot = task_pacing.build_budget_snapshot(tools._ctx, profile=budget_profile)
passes_done = int(getattr(tools._ctx, "_task_acceptance_improvement_passes", 0))
launch_ok, launch_reason = task_pacing.review_launch_allowed(budget_snapshot)
if not launch_ok:
return _skip_task_acceptance_for_launch_reason(
tools._ctx, llm_trace, launch_reason=launch_reason,
snapshot=budget_snapshot, passes_done=passes_done,
emit_progress=emit_progress,
)
review_ctx = _TaskAcceptanceContext(
tools=tools,
content=content,
@ -1286,6 +1292,18 @@ def _run_task_acceptance_review_once(
evidence=review_ctx.evidence,
fence_token_or_state=_direct_context_fence_state(tools._ctx, _fence_token),
)
if _loop()._task_acceptance_owner_generation_changed(tools._ctx):
_loop()._supersede_task_acceptance_for_owner_followup(tools._ctx, llm_trace)
return True
if _finish_advisory_author(review_ctx):
return not bool(getattr(tools._ctx, "_task_acceptance_reviewed", False))
launch_ok, launch_reason = task_pacing.review_launch_allowed(budget_snapshot)
if not launch_ok:
return _skip_task_acceptance_for_launch_reason(
tools._ctx, llm_trace, launch_reason=launch_reason,
snapshot=budget_snapshot, passes_done=passes_done,
emit_progress=emit_progress,
)
binding_hash = str(review_ctx.review_binding.get("binding_hash") or "")
# A-material: what the tree's wallet actually buys. Stamped onto the
# binding before the free-replay lookup and the dispatch claim both read it.

View file

@ -140,17 +140,22 @@ def _compute_subagent_handoff(tools: Any, drive_root: Any, task_id: str, content
return ""
def _delivery_evidence_state(
tools: ToolRegistry,
ctx: _RoundLimitContext,
llm_trace: Dict[str, Any],
) -> tuple[int, str]:
def delivery_evidence_fingerprint(
tool_ctx: Any, llm_trace: Dict[str, Any], *, task_id: str = "",
status_root: Any = None, root_task_id: str = "",
) -> str:
"""Fingerprint only evidence that can invalidate a complete answer."""
from ouroboros.outcomes import read_context_verification_receipts
from ouroboros.tools.join_ledger import _child_result_sha256
owner_directives = getattr(tools._ctx, "_owner_directives", [])
metadata = getattr(tool_ctx, "task_metadata", {})
metadata = metadata if isinstance(metadata, dict) else {}
task_id = str(task_id or getattr(tool_ctx, "task_id", "") or "")
root_task_id = str(root_task_id or metadata.get("root_task_id") or task_id)
status_root = status_root or metadata.get("budget_drive_root") or getattr(tool_ctx, "budget_drive_root", None) or getattr(tool_ctx, "drive_root", None)
children = _loop()._load_direct_child_results(pathlib.Path(status_root), task_id, root_task_id) if status_root and task_id else []
owner_directives = getattr(tool_ctx, "_owner_directives", [])
owner_directives = owner_directives if isinstance(owner_directives, list) else []
children = [
{
@ -159,16 +164,9 @@ def _delivery_evidence_state(
"sha256": _child_result_sha256(child),
"disposition": _loop()._child_disposition_state(child),
}
for child in _loop()._direct_child_results(ctx)
for child in children
]
receipt_root = pathlib.Path(
str(
getattr(tools._ctx, "drive_root", "")
or ctx.drive_root
or ctx.status_drive_root
or ctx.drive_logs.parent
)
)
receipt_root = getattr(tool_ctx, "drive_root", None) or status_root
evidence = {
"owner_directives": owner_directives,
"tool_effects": reviewable_effect_projection(llm_trace),
@ -186,7 +184,7 @@ def _delivery_evidence_state(
],
"children": children,
"verification_receipts": read_context_verification_receipts(
tools._ctx, ctx.task_id, fallback_root=receipt_root,
tool_ctx, task_id, fallback_root=receipt_root,
),
# Task-scoped service teardown can register declared outputs or
# surface an output-finalization failure. Those facts arise outside an
@ -194,13 +192,25 @@ def _delivery_evidence_state(
# a host acceptance panel could review the pre-teardown state.
"service_finalization": _loop()._service_finalization_evidence(llm_trace),
}
fingerprint = hashlib.sha256(json.dumps(
return hashlib.sha256(json.dumps(
evidence,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
).encode("utf-8")).hexdigest()
def _delivery_evidence_state(
tools: ToolRegistry,
ctx: _RoundLimitContext,
llm_trace: Dict[str, Any],
) -> tuple[int, str]:
"""Track the shared answer-invalidating evidence fingerprint."""
fingerprint = delivery_evidence_fingerprint(
tools._ctx, llm_trace, task_id=ctx.task_id, root_task_id=ctx.root_task_id,
status_root=ctx.status_drive_root or ctx.drive_root or pathlib.Path(ctx.drive_logs).parent,
)
previous = str(getattr(tools._ctx, "_delivery_evidence_fingerprint", "") or "")
revision = int(getattr(tools._ctx, "_delivery_evidence_revision", 0) or 0)
if fingerprint != previous:

View file

@ -8,9 +8,10 @@ Extracted from loop.py to keep the main loop orchestrator focused.
from __future__ import annotations
from ouroboros.config import runtime_setting
import contextlib
import hashlib
import os
import pathlib
import queue
import time
@ -191,7 +192,7 @@ def transient_retry_max(default_retries: int) -> int:
default_value = int(SETTINGS_DEFAULTS.get("OUROBOROS_TRANSIENT_RETRY_MAX", _TRANSIENT_RETRY_DEFAULT))
except Exception:
default_value = _TRANSIENT_RETRY_DEFAULT
raw = os.environ.get("OUROBOROS_TRANSIENT_RETRY_MAX", "").strip()
raw = runtime_setting("OUROBOROS_TRANSIENT_RETRY_MAX", "").strip()
try:
value = int(raw) if raw else default_value
except ValueError:

View file

@ -5,8 +5,9 @@ Extracted from loop.py (v7 L-B split); loop.py re-exports every name."""
from __future__ import annotations
from ouroboros.config import runtime_setting
import json
import os
import pathlib
import queue
@ -243,7 +244,7 @@ def _emit_round_progress(content: Any, msg: Dict[str, Any], emit_progress, llm_t
safe_text = sanitize_tool_result_for_log(visible_text)
emit_progress(safe_text)
llm_trace["reasoning_notes"].append(safe_text)
elif str(os.environ.get("OUROBOROS_REASONING_SUMMARY", "auto")).strip().lower() != "off":
elif str(runtime_setting("OUROBOROS_REASONING_SUMMARY", "auto")).strip().lower() != "off":
display_reasoning = LLMClient.extract_display_reasoning(msg)
if display_reasoning:
emit_progress(sanitize_tool_result_for_log(display_reasoning))

View file

@ -5,9 +5,10 @@ split); loop.py re-exports every name."""
from __future__ import annotations
from ouroboros.config import runtime_setting
import logging
import contextlib
import os
import pathlib
import queue
import time
@ -107,7 +108,7 @@ def _run_cross_model_fallback_chain(
_cooled(active_model, active_use_local)
primary_context_usage = _snapshot_context_fit_usage(accumulated_usage)
fallback_use_local = os.environ.get("USE_LOCAL_FALLBACK", "").lower() in ("true", "1")
fallback_use_local = runtime_setting("USE_LOCAL_FALLBACK", "").lower() in ("true", "1")
attempt_cap = _fcd.attempts_per_model()
configured_chain = parse_fallback_chain()
msg = None

View file

@ -1440,25 +1440,11 @@ def process_tool_results(
llm_trace["acceptance_decision"] = _dec
agent_decision = parsed.get("agent_decision") if isinstance(parsed.get("agent_decision"), dict) else {}
if agent_decision:
# v6.78.0 (P4.1, owner Q23=A): the HOST is the only writer of the
# acceptance verdict. The agent's stance is MERGED as its own
# `agent_disposition`/`agent_rationale` (already projected and
# already carried forward across host writes) and can no longer
# overwrite `status`/`source`/`rationale`. A merge, not a fresh
# dict: assigning a new dict here would clobber an earlier host
# verdict even after dropping the three keys.
_dec = llm_trace.get("acceptance_decision") if isinstance(llm_trace.get("acceptance_decision"), dict) else {}
_dec.setdefault("source", "agent_task_acceptance_review_tool")
_dec["agent_disposition"] = str(agent_decision.get("disposition") or "")
# DISCLOSED bound (BIBLE P1): the agent's stance is reviewer-facing
# evidence, so a clipped rationale carries its own omission note
# rather than ending mid-argument as if that were all it said.
_dec["agent_rationale"] = truncate_review_artifact(
str(agent_decision.get("rationale") or ""), limit=500,
)
from ouroboros.loop_acceptance import merge_agent_acceptance_stance
merge_agent_acceptance_stance(llm_trace, agent_decision, ctx)
if parsed.get("dissent_noted"):
_dec["dissent_noted"] = True
llm_trace["acceptance_decision"] = _dec
llm_trace["acceptance_decision"]["dissent_noted"] = True
# v6.54.4 obligations layer: apply the agent's per-obligation
# dispositions onto the host-collected per-task obligations.
ob_dispositions = agent_decision.get("obligation_dispositions")

View file

@ -41,8 +41,9 @@ size-ratchet byte cap).
from __future__ import annotations
from ouroboros.config import runtime_setting
import logging
import os
import pathlib
import queue
import time
@ -228,7 +229,7 @@ def continue_unknown_transport(episode: TransportWaitEpisode, *, llm: Any, tools
def _use_local_fallback_configured() -> bool:
return os.environ.get("USE_LOCAL_FALLBACK", "").lower() in ("true", "1")
return runtime_setting("USE_LOCAL_FALLBACK", "").lower() in ("true", "1")
def fallback_chain_allowed(

View file

@ -361,13 +361,12 @@ def _reviewed_install_binding(drive_root: pathlib.Path, skill_name: str, skill_d
specs: List[Dict[str, Any]], expected_hash: str = "") -> str:
"""Revalidate new executable declarations through the existing review owner."""
from ouroboros.skill_dependencies import payload_declared_install_specs
from ouroboros.skill_loader import load_skill, skill_review_gate
from ouroboros.skill_loader import load_skill
loaded = load_skill(skill_dir, drive_root)
if loaded is None or loaded.load_error or loaded.name != skill_name:
raise RuntimeError("reviewed install payload cannot be resolved")
stale = loaded.review.is_stale_for(loaded.content_hash)
if not skill_review_gate(loaded.review.status, stale=stale)["executable_review"]:
if not loaded.review.gate_for(loaded.content_hash)["executable_review"]:
raise RuntimeError("install declarations require a fresh executable skill review")
if expected_hash and loaded.content_hash != expected_hash:
raise RuntimeError("skill changed during dependency installation; re-review before retrying")

View file

@ -27,10 +27,10 @@ Design constraints (codex review):
from __future__ import annotations
import contextlib
import os
import threading
import time
from typing import Optional
from ouroboros.config import runtime_setting
_LOCK = threading.Lock()
_SEMAPHORES: dict = {}
@ -42,7 +42,7 @@ def _max_slot_wait_sec() -> float:
default = SETTINGS_DEFAULTS["OUROBOROS_MODEL_SLOT_MAX_WAIT_SEC"]
try:
return float(os.environ.get("OUROBOROS_MODEL_SLOT_MAX_WAIT_SEC", default))
return float(runtime_setting("OUROBOROS_MODEL_SLOT_MAX_WAIT_SEC", default))
except (TypeError, ValueError):
return float(default)
@ -54,7 +54,7 @@ def _cap() -> int:
default = SETTINGS_DEFAULTS.get("OUROBOROS_MODEL_MAX_CONCURRENCY", 3)
try:
return int(os.environ.get("OUROBOROS_MODEL_MAX_CONCURRENCY", default))
return int(runtime_setting("OUROBOROS_MODEL_MAX_CONCURRENCY", default))
except (TypeError, ValueError):
try:
return int(default)

View file

@ -12,9 +12,9 @@ from __future__ import annotations
import dataclasses
import copy
import json
import os
from ouroboros.settings_defaults import SETTINGS_DEFAULTS
from ouroboros.settings_integrity import runtime_setting
MODEL_ACCOUNTS_KEY = "OUROBOROS_MODEL_ACCOUNTS"
@ -74,7 +74,7 @@ def model_role_option(key: str, role: str, *, settings: dict | None = None) -> s
and configured agents carry their own existing route credential field.
"""
default = "" if key == MODEL_ACCOUNTS_KEY else 0
raw = (settings or {}).get(key, "") if settings is not None else os.environ.get(key, "")
raw = (settings or {}).get(key, "") if settings is not None else runtime_setting(key, "")
options, _canonical = normalize_model_role_options(key, raw)
family, separator, position = role.partition(":")
if family == "fallback" and separator:
@ -252,29 +252,29 @@ def _parse_model_list(value: str) -> list[str]:
def _main_model() -> str:
return (
str(os.environ.get("OUROBOROS_MODEL", "") or "").strip()
str(runtime_setting("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()
return str(runtime_setting("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()
return str(runtime_setting("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()
return str(runtime_setting("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()
raw = str(runtime_setting("OUROBOROS_IMAGE_INPUT_MODE", SETTINGS_DEFAULTS["OUROBOROS_IMAGE_INPUT_MODE"]) or "").strip().lower()
return raw if raw in {"auto", "caption", "inline", "off"} else "auto"
@ -287,8 +287,8 @@ def parse_fallback_chain() -> list[str]:
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()
str(runtime_setting("OUROBOROS_MODEL_FALLBACKS", "") or "").strip()
or str(runtime_setting("OUROBOROS_MODEL_FALLBACK", "") or "").strip()
)
return [m.strip() for m in _parse_model_list(raw) if str(m or "").strip()]
@ -334,10 +334,10 @@ def migrate_legacy_slot_keys(settings: dict) -> dict:
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()
return str(runtime_setting("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()
return (str(runtime_setting("OUROBOROS_MODEL_DEEP_SELF_REVIEW", "") or "").strip()
or str(SETTINGS_DEFAULTS["OUROBOROS_MODEL_DEEP_SELF_REVIEW"]))

View file

@ -143,7 +143,10 @@ def copy_wait_context() -> contextvars.Context:
Copying every ContextVar also transfers a previous physical capture and
the parent's Main fit authority. Those belong to their original call.
"""
from ouroboros.settings_integrity import copy_task_settings_context
copied = contextvars.Context()
copy_task_settings_context(copied)
for variable in (_CURRENT, _REPREPARE, _CALENDAR, _LOGICAL):
copied.run(variable.set, variable.get())
return copied

View file

@ -620,6 +620,14 @@ def _acceptance_decision_projection(acceptance_decision: Dict[str, Any]) -> Dict
"agent_disposition": str(acceptance_decision.get("agent_disposition") or ""),
"agent_rationale": str(acceptance_decision.get("agent_rationale") or "")[:500],
}
if acceptance_decision.get("reason") == "author_finish":
record = acceptance_decision.get("author_disposition")
if isinstance(record, dict):
out["author_disposition"] = dict(record)
else:
out["author_disposition"] = str(record or "")
out["author_rationale"] = str(acceptance_decision.get("author_rationale") or "")[:500]
out["reviewer_signal"] = str(acceptance_decision.get("reviewer_signal") or "")
# v6.54.4: dissent + obligations transparency (blocking review policy).
if acceptance_decision.get("dissent_noted"):
out["dissent_noted"] = True

View file

@ -23,11 +23,11 @@ from __future__ import annotations
import json
import logging
import os
import pathlib
from typing import Any, Dict, Optional
from ouroboros.evolution_fingerprint import _PLAN_REVIEW_SUFFIX
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -261,7 +261,7 @@ def _decide_promotion(env: Any, task: Dict[str, Any], reflection_entry: Optional
# Main-slot chooser (plan 5C): picking the next evolution objective is a
# high-leverage cognitive decision, not a cheap-lane formatting call.
chooser_model = str(
os.environ.get("OUROBOROS_MODEL", "") or SETTINGS_DEFAULTS["OUROBOROS_MODEL"]
runtime_setting("OUROBOROS_MODEL", "") or SETTINGS_DEFAULTS["OUROBOROS_MODEL"]
).strip()
resp, usage = chat_observed(
client,

View file

@ -24,7 +24,7 @@ from ouroboros.presence_capabilities import (
resolve_presence_profile_state,
)
from ouroboros.presence_profile import PresenceProfileError, parse_presence_profile
from ouroboros.skill_loader import find_skill, review_status_allows_execution
from ouroboros.skill_loader import find_skill
class PresenceAdmissionError(ValueError):
@ -154,12 +154,13 @@ def admit_presence_turn(
"presence_behavior_skill_disabled",
"binding.behavior_skill",
)
if skill.review.is_stale_for(skill.content_hash):
gate = skill.review.gate_for(skill.content_hash)
if gate["blocking_reason"] == "review_stale":
raise PresenceAdmissionError(
"presence_behavior_review_stale",
"binding.behavior_skill",
)
if not review_status_allows_execution(skill.review.status):
if not gate["executable_review"]:
raise PresenceAdmissionError(
"presence_behavior_review_not_executable",
"binding.behavior_skill",

View file

@ -277,6 +277,8 @@ def _build_task(
drive_root: Path,
staged_files: Sequence[Path],
) -> dict[str, Any]:
from ouroboros.config import runtime_setting
task_id = _task_id(admission, event)
chat_id = _stable_numeric_id("presence-conversation", event.conversation_key)
actor_id = _stable_numeric_id(
@ -314,7 +316,7 @@ def _build_task(
from ouroboros.config import get_light_model
metadata["model"] = get_light_model()
metadata["use_local_model"] = os.environ.get("USE_LOCAL_LIGHT", "").lower() in {"true", "1"}
metadata["use_local_model"] = runtime_setting("USE_LOCAL_LIGHT", "").lower() in {"true", "1"}
task: dict[str, Any] = {
"id": task_id,
"type": "presence",

View file

@ -7,7 +7,6 @@ usage event emission. Missing pricing is data, not a model-admission gate.
from __future__ import annotations
import os
import queue
import threading
import time
@ -17,6 +16,7 @@ import logging
from ouroboros.provider_models import normalize_model_identity, provider_for_model
from ouroboros.utils import utc_now_iso
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -45,7 +45,7 @@ _pricing_lock = threading.Lock()
def _pricing_ttl_sec() -> float:
"""Live-pricing refetch interval (provider prices/ FX rates drift). Default 6h."""
try:
return max(60.0, float(os.environ.get("OUROBOROS_PRICING_TTL_SEC", "") or 21600.0))
return max(60.0, float(runtime_setting("OUROBOROS_PRICING_TTL_SEC", "") or 21600.0))
except (TypeError, ValueError):
return 21600.0
@ -243,9 +243,9 @@ def infer_model_category(model: str) -> str:
model = model[:-8]
normalized = normalize_model_identity(model)
for cat, val in (
("main", os.environ.get("OUROBOROS_MODEL", "")),
("heavy", os.environ.get("OUROBOROS_MODEL_HEAVY", "")),
("light", os.environ.get("OUROBOROS_MODEL_LIGHT", "")),
("main", runtime_setting("OUROBOROS_MODEL", "")),
("heavy", runtime_setting("OUROBOROS_MODEL_HEAVY", "")),
("light", runtime_setting("OUROBOROS_MODEL_LIGHT", "")),
):
if val and normalized == normalize_model_identity(val):
return cat

View file

@ -19,11 +19,11 @@ Doctrine:
from __future__ import annotations
import logging
import os
import pathlib
import threading
from dataclasses import replace
from typing import Any, Callable, Dict, Optional, Sequence
import contextvars
log = logging.getLogger("ouroboros.project_naming")
@ -33,9 +33,11 @@ def _light_use_local(explicit: Optional[bool]) -> bool:
otherwise follow the runtime ``USE_LOCAL_LIGHT`` flag — naming runs on the LIGHT model,
so it must route local/remote like every other light-lane caller (e.g. the safety
check at ``ouroboros/safety.py::_resolve_safety_routing``) instead of hardcoding remote."""
from ouroboros.config import runtime_setting
if explicit is not None:
return bool(explicit)
return str(os.environ.get("USE_LOCAL_LIGHT", "") or "").lower() in ("true", "1")
return str(runtime_setting("USE_LOCAL_LIGHT", "") or "").lower() in ("true", "1")
# Mirror gateway ``_MAX_DERIVED_NAME`` so heuristic and LLM names share one cap.
MAX_PROJECT_NAME = 60
@ -94,11 +96,13 @@ def _light_naming_model() -> str:
def _naming_timeout_sec() -> float:
"""Provider-call transport timeout for the naming LIGHT call. SSOT: config
SETTINGS_DEFAULTS (no duplicated literal — the default IS the SSOT value)."""
from ouroboros.config import runtime_setting
from ouroboros.config import SETTINGS_DEFAULTS
default = SETTINGS_DEFAULTS["OUROBOROS_PROJECT_NAMING_TIMEOUT_SEC"]
try:
return float(os.environ.get("OUROBOROS_PROJECT_NAMING_TIMEOUT_SEC", default))
return float(runtime_setting("OUROBOROS_PROJECT_NAMING_TIMEOUT_SEC", default))
except (TypeError, ValueError):
return float(default)
@ -106,17 +110,21 @@ def _naming_timeout_sec() -> float:
def _naming_async_timeout_sec() -> float:
"""Gateway HARD wait for the inline turn-into-project name. SSOT: config
SETTINGS_DEFAULTS (no duplicated literal — the default IS the SSOT value)."""
from ouroboros.config import runtime_setting
from ouroboros.config import SETTINGS_DEFAULTS
default = SETTINGS_DEFAULTS["OUROBOROS_PROJECT_NAMING_ASYNC_TIMEOUT_SEC"]
try:
return float(os.environ.get("OUROBOROS_PROJECT_NAMING_ASYNC_TIMEOUT_SEC", default))
return float(runtime_setting("OUROBOROS_PROJECT_NAMING_ASYNC_TIMEOUT_SEC", default))
except (TypeError, ValueError):
return float(default)
def _project_naming_usage_scope(drive_root: Optional[Any], task_id: str):
"""Bind a naming send to its task tree even from a daemon/gateway thread."""
from ouroboros.config import runtime_setting
from ouroboros.usage_accounting import UsageScope, current_usage_scope
active = current_usage_scope()
@ -140,7 +148,7 @@ def _project_naming_usage_scope(drive_root: Optional[Any], task_id: str):
global_limit = resolve_total_budget_usd()
try:
root_limit = float(os.environ.get("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
root_limit = float(runtime_setting("OUROBOROS_PER_TASK_COST_USD", "0") or 0)
except (TypeError, ValueError):
root_limit = 0.0
return UsageScope(
@ -293,6 +301,8 @@ def spawn_proactive_namer(
write. Skips cleanly unless ``drive_root`` is a real directory (test safety: a stub /
MagicMock drive must never materialise a stray path — chat_observed persists BEFORE the
LLM call). Fail-soft."""
from ouroboros.settings_integrity import copy_task_settings_context
body = " ".join(str(text or "").split())
if not body:
return
@ -334,7 +344,9 @@ def spawn_proactive_namer(
if _detached.is_set():
_refresh_detached_once()
inner = threading.Thread(target=_call, name=f"namer-call-{task_id}", daemon=True)
settings_context = contextvars.Context()
copy_task_settings_context(settings_context)
inner = threading.Thread(target=settings_context.run, args=(_call,), name=f"namer-call-{task_id}", daemon=True)
inner.start()
if not _finished.wait(timeout=max(0.0, _naming_timeout_sec() + 30.0)):
_detached.set()
@ -380,7 +392,9 @@ def spawn_proactive_namer(
log.debug("proactive namer failed for %s", task_id, exc_info=True)
try:
threading.Thread(target=_work, name=f"namer-{task_id}", daemon=True).start()
settings_context = contextvars.Context()
copy_task_settings_context(settings_context)
threading.Thread(target=settings_context.run, args=(_work,), name=f"namer-{task_id}", daemon=True).start()
except Exception:
log.debug("proactive namer thread spawn failed for %s", task_id, exc_info=True)

View file

@ -5,10 +5,10 @@ deep_self_review.py)."""
from __future__ import annotations
import os
from ouroboros.model_slots import ResolvedModelTarget, parse_fallback_chain
from ouroboros.settings_defaults import OPENROUTER_DEFAULTS, OPENROUTER_REVIEW_DEFAULTS, SETTINGS_DEFAULTS # noqa: F401
from ouroboros.settings_integrity import runtime_setting
# MiniMax exposes the same OpenAI-compatible API on two regional hosts. Keep the
# mapping centralized so transport, capability evidence, and settings diagnostics
@ -213,17 +213,17 @@ def provider_has_credentials(provider: str) -> bool:
if provider == "local":
return True
if provider == "openai-compatible":
compat = str(os.environ.get("OPENAI_COMPATIBLE_API_KEY", "") or "").strip()
legacy_key = str(os.environ.get("OPENAI_API_KEY", "") or "").strip()
legacy_base = str(os.environ.get("OPENAI_BASE_URL", "") or "").strip()
compat = str(runtime_setting("OPENAI_COMPATIBLE_API_KEY", "") or "").strip()
legacy_key = str(runtime_setting("OPENAI_API_KEY", "") or "").strip()
legacy_base = str(runtime_setting("OPENAI_BASE_URL", "") or "").strip()
return bool(compat or (legacy_key and legacy_base))
if provider == "gigachat":
creds = str(os.environ.get("GIGACHAT_CREDENTIALS", "") or "").strip()
user = str(os.environ.get("GIGACHAT_USER", "") or "").strip()
password = str(os.environ.get("GIGACHAT_PASSWORD", "") or "").strip()
creds = str(runtime_setting("GIGACHAT_CREDENTIALS", "") or "").strip()
user = str(runtime_setting("GIGACHAT_USER", "") or "").strip()
password = str(runtime_setting("GIGACHAT_PASSWORD", "") or "").strip()
return bool(creds or (user and password))
env_key = PROVIDER_ENV_KEYS.get(provider, "OPENROUTER_API_KEY")
return bool(str(os.environ.get(env_key, "") or "").strip())
return bool(str(runtime_setting(env_key, "") or "").strip())
def provider_has_credentials_in_settings(provider: str, settings: dict) -> bool:
@ -261,7 +261,7 @@ def model_has_credentials(model: str) -> bool:
def local_only_review_route_env() -> bool:
"""Whether review slots must inherit the configured local Main route."""
local_main = str(os.environ.get("USE_LOCAL_MAIN", "") or "").strip().lower()
local_main = str(runtime_setting("USE_LOCAL_MAIN", "") or "").strip().lower()
if local_main not in {"1", "true", "yes", "on"}:
return False
return not any(
@ -290,12 +290,12 @@ def resolve_credentialed_model(default_model: str) -> str:
# instead of testing the whole comma-string as one broken model id. Empty Light
# (default -> Main) simply contributes nothing here.
candidates: list[str] = []
light = str(os.environ.get("OUROBOROS_MODEL_LIGHT", "") or "").strip()
light = str(runtime_setting("OUROBOROS_MODEL_LIGHT", "") or "").strip()
if light:
candidates.append(light)
candidates.extend(parse_fallback_chain())
for env_name in ("OUROBOROS_MODEL",):
raw = str(os.environ.get(env_name, "") or "").strip()
raw = str(runtime_setting(env_name, "") or "").strip()
if raw:
candidates.append(raw)
for candidate in candidates:

View file

@ -17,7 +17,6 @@ migration in ``config.py``).
from __future__ import annotations
import os
import time
from typing import Any, Optional
@ -101,10 +100,12 @@ def get_gc_retention_days() -> int:
Precedence: ``OUROBOROS_GC_RETENTION_DAYS`` -> first set legacy key
(backward-compat) -> configured default. Always clamped to ``[1, 365]``."""
raw = os.environ.get("OUROBOROS_GC_RETENTION_DAYS", "")
from ouroboros.config import runtime_setting
raw = runtime_setting("OUROBOROS_GC_RETENTION_DAYS", "")
if str(raw or "").strip():
return clamp_retention_days(raw)
seed = pick_legacy_retention_seed(lambda key: os.environ.get(key, ""))
seed = pick_legacy_retention_seed(lambda key: runtime_setting(key, ""))
if seed is not None and str(seed).strip():
return clamp_retention_days(seed)
return clamp_retention_days(_default_gc_days())

View file

@ -55,7 +55,6 @@ only through the environment is not migrated and no longer binds. No new state f
from __future__ import annotations
import logging
import os
import pathlib
from typing import Any, Optional
@ -64,6 +63,7 @@ from ouroboros.config import SETTINGS_DEFAULTS
# enforcement" (owner D10/D27) — SSOT beside the acceptance-decision vocabulary.
from ouroboros.outcomes import REASON_REVIEW_CYCLES_EXHAUSTED # noqa: F401 — re-export
from ouroboros.utils import append_jsonl, emit_log_event, utc_now_iso
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -132,7 +132,7 @@ def review_max_cycles() -> Optional[int]:
Env-or-default like every other getter; a malformed value fails CLOSED to
the shipped default (bounded) and is reported once per process."""
default_text = str(SETTINGS_DEFAULTS[REVIEW_MAX_CYCLES_KEY])
raw = os.environ.get(REVIEW_MAX_CYCLES_KEY, "") or default_text
raw = runtime_setting(REVIEW_MAX_CYCLES_KEY, "") or default_text
try:
return parse_review_max_cycles(raw)
except (TypeError, ValueError):
@ -149,7 +149,7 @@ def review_max_cycles_source() -> str:
present in the environment (``config.apply_settings_to_env`` projects saved
settings there, so an owner edit and an env override are the same fact),
else ``shipped_default``. Provenance only — never a second parse."""
return "owner_setting" if os.environ.get(REVIEW_MAX_CYCLES_KEY, "") else "shipped_default"
return "owner_setting" if runtime_setting(REVIEW_MAX_CYCLES_KEY, "") else "shipped_default"
def acceptance_max_improvement_passes_from_cycles() -> Optional[int]:

View file

@ -10,13 +10,13 @@ The dependency runs one way: this module never imports the coordinator.
from __future__ import annotations
from ouroboros.config import runtime_setting
from ouroboros.model_wait import monotonic_now
import asyncio
import hashlib
import json
import logging
import os
import time
from dataclasses import dataclass
from enum import Enum
@ -512,7 +512,7 @@ def review_session_route() -> Any:
"""
from ouroboros.subagents import get_subagent_harness, parse_subagent_harness
raw = str(os.environ.get(REVIEW_SESSION_ROUTE_ENV, "")).strip()
raw = str(runtime_setting(REVIEW_SESSION_ROUTE_ENV, "")).strip()
route = parse_subagent_harness(raw)
if route is not None: return route
if raw and raw.lower() != "off":

View file

@ -14,7 +14,6 @@ review family.
from __future__ import annotations
import dataclasses
import os
from ouroboros.model_slots import ResolvedModelTarget, _main_model, _parse_model_list
from ouroboros.provider_models import (
@ -25,21 +24,22 @@ from ouroboros.provider_models import (
review_model_uses_local,
)
from ouroboros.settings_defaults import OPENROUTER_REVIEW_DEFAULTS, SETTINGS_DEFAULTS
from ouroboros.settings_integrity import runtime_setting
_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())
has_openrouter = bool(str(runtime_setting("OPENROUTER_API_KEY", "") or "").strip())
has_openai = bool(str(runtime_setting("OPENAI_API_KEY", "") or "").strip())
has_anthropic = bool(str(runtime_setting("ANTHROPIC_API_KEY", "") or "").strip())
has_minimax = bool(str(runtime_setting("MINIMAX_API_KEY", "") or "").strip())
has_legacy_base = bool(str(runtime_setting("OPENAI_BASE_URL", "") or "").strip())
has_compatible = bool(str(runtime_setting("OPENAI_COMPATIBLE_BASE_URL", "") or "").strip())
has_cloudru = bool(str(runtime_setting("CLOUDRU_FOUNDATION_MODELS_API_KEY", "") or "").strip())
has_gigachat = bool(str(runtime_setting("GIGACHAT_CREDENTIALS", "") or "").strip()) or (
bool(str(runtime_setting("GIGACHAT_USER", "") or "").strip())
and bool(str(runtime_setting("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
@ -50,7 +50,7 @@ def _exclusive_direct_remote_provider_env() -> str:
direct = [name for name, present in (
("openai", has_openai), ("anthropic", has_anthropic), ("minimax", has_minimax),
("cloudru", has_cloudru), ("gigachat", has_gigachat),
("deepseek", bool(str(os.environ.get("DEEPSEEK_API_KEY", "") or "").strip())),
("deepseek", bool(str(runtime_setting("DEEPSEEK_API_KEY", "") or "").strip())),
) if present]
return direct[0] if len(direct) == 1 else ""
@ -60,10 +60,10 @@ def direct_provider_review_models_fallback(provider: str) -> list[str]:
if provider not in ("openai", "anthropic", "minimax", "cloudru", "gigachat", "deepseek"):
return []
main_model = str(
os.environ.get("OUROBOROS_MODEL", SETTINGS_DEFAULTS["OUROBOROS_MODEL"]) or ""
runtime_setting("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()
user_light_raw = str(runtime_setting("OUROBOROS_MODEL_LIGHT", "") or "").strip()
return compute_direct_review_models_fallback(
provider,
main_model,
@ -84,14 +84,14 @@ def adaptive_quorum(n_slots: int) -> int:
def get_review_models() -> list[str]:
"""Return the effective triad model list from the derived env plane."""
default_str = ",".join(OPENROUTER_REVIEW_DEFAULTS["triad"])
models_str = os.environ.get("OUROBOROS_REVIEW_MODELS", default_str) or default_str
models_str = runtime_setting("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 = str(runtime_setting("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):
@ -143,18 +143,18 @@ def get_scope_review_targets() -> tuple[ResolvedModelTarget, ...]:
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()
raw = (runtime_setting("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 effective scope reviewer models, preserving duplicate model IDs."""
default_str = ",".join(OPENROUTER_REVIEW_DEFAULTS["scope"])
raw = os.environ.get("OUROBOROS_SCOPE_REVIEW_MODELS", "") or ""
raw = runtime_setting("OUROBOROS_SCOPE_REVIEW_MODELS", "") or ""
if not raw.strip():
raw = os.environ.get("OUROBOROS_SCOPE_REVIEW_MODEL", default_str) or default_str
raw = runtime_setting("OUROBOROS_SCOPE_REVIEW_MODEL", default_str) or default_str
models = _parse_model_list(raw)
singular = str(os.environ.get("OUROBOROS_SCOPE_REVIEW_MODEL", OPENROUTER_REVIEW_DEFAULTS["scope"][0]) or "").strip()
singular = str(runtime_setting("OUROBOROS_SCOPE_REVIEW_MODEL", OPENROUTER_REVIEW_DEFAULTS["scope"][0]) or "").strip()
if not models and singular:
models = [singular]
if not models:

View file

@ -17,6 +17,94 @@ from typing import Any, Dict, List, Optional
from ouroboros.review_execution import ReviewRouteKind, delivery_retrieves
# One semantic author-finality record shared by review owners. Surfaces keep
# their existing storage and reviewer evidence; this vocabulary only makes an
# author's final stance explicit and hash-bound when a review is advisory.
AUTHOR_DISPOSITION_VALUES = frozenset({"accepted", "rejected", "partial", "deferred"})
def build_author_disposition(
*,
disposition: str,
rationale: str,
subject_hash: str,
reviewer_signal: str = "",
enforcement: str = "",
source: str = "author",
recorded_at: str = "",
) -> Dict[str, Any]:
"""Build one bounded, current-subject author-finality record.
This is a record helper, not a second review ledger. Callers persist the
returned object in their existing plan/skill/acceptance/commit owners and
continue to retain raw reviewer rows beside it. A missing hash or reason
is rejected so an author finish can never look like an unbound PASS.
"""
value = str(disposition or "").strip().lower()
reason = " ".join(str(rationale or "").split()).strip()
subject = str(subject_hash or "").strip()
if value not in AUTHOR_DISPOSITION_VALUES:
raise ValueError("AUTHOR_DISPOSITION_INVALID: unknown disposition")
if not subject:
raise ValueError("AUTHOR_DISPOSITION_INVALID: subject_hash is required")
if not reason:
raise ValueError("AUTHOR_DISPOSITION_INVALID: rationale is required")
if len(reason) > 8_000:
raise ValueError("AUTHOR_DISPOSITION_INVALID: rationale is too large")
if not recorded_at:
from ouroboros.utils import utc_now_iso
recorded_at = utc_now_iso()
return {
"disposition": value,
"rationale": reason,
"subject_hash": subject,
"reviewer_signal": str(reviewer_signal or "").strip(),
"enforcement": str(enforcement or "").strip().lower(),
"recorded_at": str(recorded_at),
"source": str(source or "author"),
}
def validate_author_disposition(
record: Any,
*,
subject_hash: str = "",
allow_stale: bool = False,
) -> Optional[Dict[str, Any]]:
"""Validate and return a safe copy, rejecting malformed or stale records."""
if not isinstance(record, dict):
return None
try:
normalized = build_author_disposition(
disposition=record.get("disposition", ""),
rationale=record.get("rationale", ""),
subject_hash=record.get("subject_hash", ""),
reviewer_signal=record.get("reviewer_signal", ""),
enforcement=record.get("enforcement", ""),
source=record.get("source", "author"),
recorded_at=record.get("recorded_at", ""),
)
except (TypeError, ValueError):
return None
expected = str(subject_hash or "").strip()
if expected and normalized["subject_hash"] != expected and not allow_stale:
return None
return normalized
def build_author_disposition_from_mapping(
value: Any, *, subject_hash: str, reviewer_signal: str = "", enforcement: str = "",
) -> Dict[str, Any]:
"""Parse the public two-field author finish envelope."""
if not isinstance(value, dict) or set(value) - {"disposition", "rationale"}:
raise ValueError("AUTHOR_DISPOSITION_INVALID: envelope fields are invalid")
return build_author_disposition(
disposition=value.get("disposition", ""), rationale=value.get("rationale", ""),
subject_hash=subject_hash, reviewer_signal=reviewer_signal, enforcement=enforcement,
)
def apply_review_model_override(slot: Any, overrides: Dict[str, dict], *, slot_id: str = "") -> Any:
"""Project an explicit owner model choice onto one frozen reviewer row.

View file

@ -108,6 +108,8 @@ def _commit_attempt_from_dict(d: Dict[str, Any]) -> CommitAttemptRecord:
else {} if raw_scope is None
else {"raw_results": [_malformed_roster_row("scope_review")]}
),
author_disposition=(dict(d.get("author_disposition"))
if isinstance(d.get("author_disposition"), dict) else {}),
paid=bool(d.get("paid", False)),
review_owner_pid=_coerce_int(d.get("review_owner_pid", 0)),
raw_stripped=bool(d.get("raw_stripped", False)),
@ -329,6 +331,12 @@ def _save_state_unlocked(drive_root: pathlib.Path, state: AdvisoryReviewState) -
path = drive_root / _STATE_RELPATH
path.parent.mkdir(parents=True, exist_ok=True)
_prepare_state_for_persistence(state)
# Legacy/in-memory callers may construct pre-author-disposition
# CommitAttemptRecord objects directly. Normalize the additive field before
# dataclasses.asdict so persistence remains backward compatible.
for attempt in state.attempts:
if not hasattr(attempt, "author_disposition"):
setattr(attempt, "author_disposition", {})
data: Dict[str, Any] = {
"state_version": _STATE_SCHEMA_VERSION,
"schema_version": _STATE_SCHEMA_VERSION,

View file

@ -341,6 +341,9 @@ class CommitAttemptRecord:
# free text) were compacted because the preserved accounting row fell
# outside the newest-50 ledger window (see _strip_attempt_heavy_payload).
raw_stripped: bool = False
# Optional canonical author-finish stance for an advisory commit. Raw
# reviewer evidence remains in the same attempt row beside this record.
author_disposition: Dict[str, Any] = field(default_factory=dict)
def _attempt_identity_tuple(attempt: CommitAttemptRecord) -> tuple[str, str, str, str]:
@ -419,6 +422,7 @@ def _merge_attempt(existing: CommitAttemptRecord, incoming: CommitAttemptRecord)
triad_models=list(incoming.triad_models or existing.triad_models),
triad_raw_results=list(getattr(incoming, "triad_raw_results", None) or getattr(existing, "triad_raw_results", None) or []),
scope_raw_result=dict(getattr(incoming, "scope_raw_result", None) or getattr(existing, "scope_raw_result", None) or {}),
author_disposition=dict(incoming.author_disposition or existing.author_disposition),
# Once an attempt physically dispatched a paid triad/scope wave the fact is
# durable: a later terminal update on the same row must never launder it.
paid=bool(getattr(incoming, "paid", False) or getattr(existing, "paid", False)),

View file

@ -8,11 +8,11 @@ reviewer slots.
from __future__ import annotations
from ouroboros.config import runtime_setting
from ouroboros.model_wait import monotonic_now
from dataclasses import asdict, replace
import logging
import os
import pathlib
import time
from typing import Any, Dict, List, Optional
@ -287,7 +287,7 @@ class ReviewCoordinator:
else:
try:
configured_root_limit = float(
os.environ.get("OUROBOROS_PER_TASK_COST_USD", "0") or 0
runtime_setting("OUROBOROS_PER_TASK_COST_USD", "0") or 0
)
root_limit = configured_root_limit if configured_root_limit > 0 else None
except (TypeError, ValueError):

View file

@ -451,13 +451,15 @@ def build_improvement_capsule(
# improves the result; otherwise produce your normal final answer" tail
# was the measured cause of the do-nothing resubmit loop (SWE 1b311217:
# 7 passes, zero tool calls). The anti-derailment guards stay verbatim.
"Three real moves are available: (1) FIX — change the work/answer so the next panel is "
"Four real moves are available: (1) FIX — change the work/answer so the next panel is "
"clean; (2) REBUT — file obligation_dispositions (rejected + your reason) via the "
"task_acceptance_review tool for findings you can show are wrong; the reviewer "
"adjudicates the argument; (3) DECLARE UNREACHABLE — dispose an obligation as "
"unsatisfiable in this environment (rejected + the concrete gap), and the reviewer "
"judges reachability. Resubmitting the same answer with none of these moves changes "
"nothing. "
"nothing. (4) AUTHOR FINISH — under advisory enforcement, record accepted, rejected, "
"partial, or deferred with a rationale; the first panel's raw findings remain durable, "
"no reviewer PASS is fabricated, and Blocking enforcement still requires its own gate. "
"Do not mention this review or the reviewer unless the user asked. "
"The assessment tier above is an internal ledger label — never emit an internal ledger "
"identifier as the deliverable itself."

View file

@ -59,6 +59,7 @@ from __future__ import annotations
import contextlib as _contextlib
import json
import os
from ouroboros.settings_integrity import runtime_environ, runtime_setting
import pathlib
import threading
from dataclasses import dataclass
@ -204,7 +205,7 @@ class ReviewerSlotConfig:
def structured_reviewer_slots_raw() -> str:
return str(os.environ.get(REVIEWER_SLOTS_ENV, "") or "").strip()
return str(runtime_setting(REVIEWER_SLOTS_ENV, "") or "").strip()
def structured_reviewer_slots_present() -> bool:
@ -283,7 +284,7 @@ def _resolve_actor_slot(
# process env, which concurrent review dispatch could observe.
_override = _ROSTER_ENV_OVERRIDE.get()
snapshot, _legacy = select_subagent_snapshot(
_override if _override is not None else os.environ,
_override if _override is not None else runtime_environ(),
subagent_id=subagent_id,
)
except SubagentSelectionError as exc:
@ -946,11 +947,11 @@ def reviewer_slot_save_check(
@_contextlib.contextmanager
def roster_env_override(subagents_raw: str):
def roster_env_override(subagents_raw: str, *, environ=None):
"""Parse reviewer rows against THIS roster instead of the process env —
the save handler's incoming roster, or a benchmark container's one-model
roster — without mutating the environment concurrent dispatch observes."""
overlay = dict(os.environ)
overlay = dict(runtime_environ() if environ is None else environ)
overlay["OUROBOROS_SUBAGENTS"] = str(subagents_raw)
token = _ROSTER_ENV_OVERRIDE.set(overlay)
try:
@ -959,7 +960,7 @@ def roster_env_override(subagents_raw: str):
_ROSTER_ENV_OVERRIDE.reset(token)
def project_reviewer_slots_into_env() -> None:
def project_reviewer_slots_into_env(*, environ=None) -> None:
"""Project the structured config into the legacy comma keys, at env-apply time.
No review surface reads these keys while the structured key is present —
@ -985,10 +986,12 @@ def project_reviewer_slots_into_env() -> None:
"""
from ouroboros.settings_defaults import OPENROUTER_REVIEW_DEFAULTS
raw = structured_reviewer_slots_raw()
environ = os.environ if environ is None else environ
raw = str(environ.get(REVIEWER_SLOTS_ENV, "") or "").strip()
if raw:
try:
config = parse_reviewer_slots(raw)
with roster_env_override(str(environ.get("OUROBOROS_SUBAGENTS", "")), environ=environ):
config = parse_reviewer_slots(raw)
except ValueError:
import logging
@ -1001,18 +1004,18 @@ def project_reviewer_slots_into_env() -> None:
api_triad = [r.target_id for r in config.triad if not r.is_session]
api_scope = [r.target_id for r in config.scope if not r.is_session]
if api_triad:
os.environ["OUROBOROS_REVIEW_MODELS"] = ",".join(api_triad)
environ["OUROBOROS_REVIEW_MODELS"] = ",".join(api_triad)
else:
os.environ.pop("OUROBOROS_REVIEW_MODELS", None)
environ.pop("OUROBOROS_REVIEW_MODELS", None)
if api_scope:
os.environ["OUROBOROS_SCOPE_REVIEW_MODELS"] = ",".join(api_scope)
environ["OUROBOROS_SCOPE_REVIEW_MODELS"] = ",".join(api_scope)
else:
os.environ.pop("OUROBOROS_SCOPE_REVIEW_MODELS", None)
os.environ.pop("OUROBOROS_SCOPE_REVIEW_MODEL", None)
if not os.environ.get("OUROBOROS_REVIEW_MODELS"):
os.environ["OUROBOROS_REVIEW_MODELS"] = ",".join(OPENROUTER_REVIEW_DEFAULTS["triad"])
if not os.environ.get("OUROBOROS_SCOPE_REVIEW_MODELS") and not os.environ.get("OUROBOROS_SCOPE_REVIEW_MODEL"):
os.environ["OUROBOROS_SCOPE_REVIEW_MODELS"] = ",".join(OPENROUTER_REVIEW_DEFAULTS["scope"])
environ.pop("OUROBOROS_SCOPE_REVIEW_MODELS", None)
environ.pop("OUROBOROS_SCOPE_REVIEW_MODEL", None)
if not environ.get("OUROBOROS_REVIEW_MODELS"):
environ["OUROBOROS_REVIEW_MODELS"] = ",".join(OPENROUTER_REVIEW_DEFAULTS["triad"])
if not environ.get("OUROBOROS_SCOPE_REVIEW_MODELS") and not environ.get("OUROBOROS_SCOPE_REVIEW_MODEL"):
environ["OUROBOROS_SCOPE_REVIEW_MODELS"] = ",".join(OPENROUTER_REVIEW_DEFAULTS["scope"])
# ---------------------------------------------------------------------------

View file

@ -147,7 +147,7 @@ def reviewer_route(model_id: str, *, session: bool = False) -> tuple:
harness IS the provider here, exactly as the reviewer-slot SSOT spells it,
which is what makes the ack reachable and the record honest. The caller
passes the ROW's configured kind; nothing sniffs the string."""
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
from ouroboros.provider_models import provider_for_model
if session:
@ -159,14 +159,14 @@ def reviewer_route(model_id: str, *, session: bool = False) -> tuple:
from ouroboros.provider_models import resolve_minimax_base_url
return provider, str(
resolve_minimax_base_url(load_settings().get("MINIMAX_REGION") or "") or "")
resolve_minimax_base_url(runtime_settings().get("MINIMAX_REGION") or "") or "")
settings_key = {
"openai": "OPENAI_BASE_URL",
"openai-compatible": "OPENAI_COMPATIBLE_BASE_URL",
"cloudru": "CLOUDRU_FOUNDATION_MODELS_BASE_URL",
"gigachat": "GIGACHAT_BASE_URL",
}.get(provider, "")
base_url = str(load_settings().get(settings_key) or "") if settings_key else ""
base_url = str(runtime_settings().get(settings_key) or "") if settings_key else ""
return provider, base_url
@ -229,7 +229,7 @@ def resolve_reviewer_window(
# /models is authenticated); every other provider probes keyless.
_probe_api_key = None
if effective_provider == "minimax":
from ouroboros.config import load_settings as _ls
from ouroboros.config import runtime_settings as _ls
_probe_api_key = str(_ls().get("MINIMAX_API_KEY") or "") or None
with _route_probe_lock(route_fp):
ev = probe(

View file

@ -8,14 +8,15 @@ 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 (
FINALIZATION_GRACE_DEFAULT_SEC,
PACING_INTERVAL_DEFAULT_SEC,
SETTINGS_DEFAULTS,
SUPERVISOR_LIVENESS_DEADLINE_DEFAULT_SEC,
)
from ouroboros.settings_integrity import runtime_setting
# Local model-operation status polling; not a provider deadline or quota timer.
CLAUDEXOR_MODEL_POLL_INTERVAL_SEC = 0.25
@ -64,7 +65,7 @@ def _clamped_number_setting(key: str, *, low, high=float("inf"), cast=float):
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])
value = cast(runtime_setting(key, "") or SETTINGS_DEFAULTS[key])
except (TypeError, ValueError):
value = cast(SETTINGS_DEFAULTS[key])
return max(low, min(value, high))
@ -73,7 +74,7 @@ def _clamped_number_setting(key: str, *, low, high=float("inf"), cast=float):
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))
raw = runtime_setting(key, SETTINGS_DEFAULTS.get(key, default))
try:
parsed = int(raw)
except (TypeError, ValueError):
@ -183,7 +184,7 @@ def get_vision_caption_timeout_sec() -> 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")
raw = runtime_setting("OUROBOROS_PACING_INTERVAL_SEC")
if raw is None and isinstance(settings, dict):
raw = settings.get("OUROBOROS_PACING_INTERVAL_SEC")
try:
@ -195,7 +196,7 @@ def get_pacing_interval_sec(settings: Optional[dict] = None) -> int:
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")
raw = runtime_setting("OUROBOROS_SUPERVISOR_LIVENESS_DEADLINE_SEC")
if raw is None and isinstance(settings, dict):
raw = settings.get("OUROBOROS_SUPERVISOR_LIVENESS_DEADLINE_SEC")
try:
@ -259,3 +260,19 @@ def get_search_code_wall_sec() -> float:
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)
def get_finalization_grace_sec(settings: Optional[dict] = None) -> int:
"""Grace window in seconds: env, else the ``settings`` argument, else the
shipped default — the ``_clamped_number_setting`` shape. Deliberately NO
``load_settings()`` fallback: a READ must never persist settings, and that
call runs the context-mode compatibility migration, which can WRITE a
normalized file under read-only observers (``task_pacing._reserve_sec``)."""
raw = runtime_setting("OUROBOROS_FINALIZATION_GRACE_SEC")
if raw is None and isinstance(settings, dict):
raw = settings.get("OUROBOROS_FINALIZATION_GRACE_SEC")
try:
parsed = int(raw)
except (TypeError, ValueError):
parsed = int(FINALIZATION_GRACE_DEFAULT_SEC)
return max(0, min(parsed, 300))

View file

@ -8,10 +8,205 @@ triad + scope review gate.
from __future__ import annotations
import ast
import pathlib
import shlex
from dataclasses import dataclass
from typing import Iterable
from ouroboros.settings_scales import _RUNTIME_MODE_RANK
def runtime_mode_rank(runtime_mode: str) -> int:
"""Return the ordered runtime-mode rank without duplicating the vocabulary.
``settings_scales`` is the owner of the persisted enum and rank. Unknown
values remain below every known mode.
"""
return int(_RUNTIME_MODE_RANK.get(str(runtime_mode or "").strip().lower(), -1))
def runtime_mode_at_least(runtime_mode: str, minimum: str) -> bool:
"""Whether ``runtime_mode`` meets the named ordered capability floor."""
mode_rank = runtime_mode_rank(runtime_mode)
minimum_rank = runtime_mode_rank(minimum)
return mode_rank >= 0 and minimum_rank >= 0 and mode_rank >= minimum_rank
def protected_bible_history_delete_reason(
raw_cmd: object, *,
protect_bible: bool = True, identity_path: pathlib.Path | None = None,
cwd: pathlib.Path | None = None, bible_path: pathlib.Path | None = None,
) -> str:
"""Return a refusal for physical BIBLE deletion or repository history rewrites.
This is deliberately a small argv/verb predicate at the existing shell
guard seam. It does not classify arbitrary content or restrict ordinary
``rm`` commands elsewhere.
"""
try:
from ouroboros.shell_parse import collect_leading_env, shell_segments
delete_heads = {"rm", "unlink", "mv"}
history_verbs = {"filter-branch", "filter-repo", "rebase", "replace"}
work_dir = cwd or pathlib.Path.cwd()
bible_target = (bible_path or work_dir / "BIBLE.md").resolve(strict=False)
def _protected_target(candidate: str) -> bool:
path = pathlib.Path(candidate.replace("\\", "/"))
target = (work_dir / path).resolve(strict=False)
if protect_bible and str(target).casefold() == str(bible_target).casefold():
return True
return bool(identity_path is not None and (
str(target).casefold() == str(identity_path.resolve(strict=False)).casefold()
))
def _bible_path(
words: list[str], *, path_flag_only: bool = False,
) -> bool:
"""Recognize an explicit BIBLE.md path, including --path= forms."""
candidates: list[str] = []
expect_value = False
for word in words:
token = str(word).strip("'\"")
if expect_value:
candidates.append(token)
expect_value = False
continue
if token in {"--path", "--path-file", "--paths"}:
expect_value = True
continue
if token.startswith("--path="):
candidates.append(token.split("=", 1)[1])
continue
if not path_flag_only:
candidates.append(token)
return any(
_protected_target(candidate)
for candidate in candidates
)
def _python_delete_paths(body: str) -> list[str]:
"""Extract literal targets of structural Python deletion calls."""
try:
from ouroboros.tools.shell_guards import python_body_ast
tree = python_body_ast(body)
if tree is None:
return []
found: list[str] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
deletion = False
if isinstance(func, ast.Attribute):
attr = str(func.attr or "")
receiver = func.value
if attr in {"remove", "unlink", "rmtree", "removedirs"}:
deletion = (
isinstance(receiver, ast.Name)
and receiver.id in {"os", "shutil"}
) or (
isinstance(receiver, ast.Call)
and isinstance(receiver.func, ast.Name)
and receiver.func.id in {"Path", "PurePath"}
)
if attr in {"run", "call", "check_call", "check_output", "Popen"}:
deletion = any(
isinstance(item, ast.Constant)
and str(item.value).strip().lower() in {"rm", "unlink"}
for item in ast.walk(node)
)
for item in ast.walk(node):
if not isinstance(item, ast.Constant) or not isinstance(item.value, str):
continue
try:
tokens = shlex.split(item.value)
except ValueError:
continue
if tokens and pathlib.PurePath(tokens[0]).name.lower() in {"rm", "unlink", "mv"}:
deletion = True
found.extend(tokens[1:])
if isinstance(func, ast.Name) and func.id in {"remove", "unlink"}:
deletion = True
if deletion:
found.extend(
str(item.value)
for item in ast.walk(node)
if isinstance(item, ast.Constant)
and isinstance(item.value, str)
)
return found
except Exception:
return []
for segment in shell_segments(raw_cmd):
_env, argv = collect_leading_env(segment)
if not argv:
continue
head = pathlib.PurePath(str(argv[0])).name.lower().removesuffix(".exe")
words = [str(item).replace("\\", "/") for item in argv[1:]]
if head in {"sh", "bash", "zsh"}:
nested = ""
for index, word in enumerate(words[:-1]):
if word in {"-c", "--command"}:
nested = words[index + 1]
break
if nested:
nested_reason = protected_bible_history_delete_reason(
nested, protect_bible=protect_bible,
identity_path=identity_path, cwd=cwd, bible_path=bible_target,
)
if nested_reason:
return nested_reason
continue
operands = [word for word in words if not word.startswith("-")]
bible = _bible_path(operands[:-1] if head == "mv" else words)
if head in delete_heads and bible:
label = "IDENTITY" if any(
pathlib.PurePath(word.strip("'\"")).name.casefold() == "identity.md"
for word in words
) else "BIBLE"
return f"{label}_DELETE_BLOCKED: protected identity history must remain physically present."
if head == "git":
verbs = [word.lower() for word in words if not word.startswith("-")]
deleting = bool(verbs and (
verbs[0] in {"rm", "mv"}
or verbs[0] == "update-index" and any(flag in words for flag in ("--remove", "--force-remove"))
))
git_targets = operands[1:-1] if verbs and verbs[0] == "mv" else operands[1:]
if deleting and _bible_path(git_targets):
label = "IDENTITY" if any(
pathlib.PurePath(word.strip("'\"")).name.casefold() == "identity.md"
for word in words
) else "BIBLE"
return f"{label}_DELETE_BLOCKED: git rm/git mv cannot remove or rename protected identity files."
if verbs and verbs[0] in history_verbs and _bible_path(
words, path_flag_only=(verbs[0] in {"filter-branch", "filter-repo"})
):
return "BIBLE_HISTORY_REWRITE_BLOCKED: BIBLE history must remain physically recoverable."
head_name = pathlib.PurePath(str(argv[0])).name.lower().removesuffix(".exe")
if head_name.startswith(("python", "python3")):
try:
from ouroboros.tools.shell_guards import interpreter_inline_code
for body in interpreter_inline_code([str(item) for item in argv]):
deleted = _python_delete_paths(body)
if any(
_protected_target(str(path))
for path in deleted
):
target = "identity.md" if any(
pathlib.PurePath(path).name.casefold() == "identity.md" for path in deleted
) else "bible.md"
return f"{target.upper().replace('.MD', '')}_DELETE_BLOCKED: protected identity history must remain physically present."
except Exception:
pass
return ""
except Exception:
return ""
SAFETY_CRITICAL_PATHS = frozenset({
"BIBLE.md",
@ -168,7 +363,7 @@ def protected_paths_in(paths: Iterable[str]) -> list[ProtectedPath]:
def mode_allows_protected_write(runtime_mode: str) -> bool:
return str(runtime_mode or "").strip().lower() == "pro"
return runtime_mode_at_least(runtime_mode, "pro")
def format_protected_paths(paths: Iterable[ProtectedPath | str]) -> str:
@ -193,17 +388,18 @@ def protected_write_block_message(
) -> str:
norm = normalize_repo_path(path)
category = protected_path_category(norm)
target_modes = "runtime_mode='pro' or 'cyber_pro'" if str(runtime_mode).strip().lower() == "cyber_pro" else "runtime_mode='pro'"
return (
f"⚠️ CORE_PROTECTION_BLOCKED: runtime_mode={runtime_mode!r} refuses "
f"to {action} protected {category or 'core'} path: {norm}. "
"Switch to runtime_mode='pro' and let the normal triad + scope review "
f"Switch to {target_modes} and let the normal triad + scope review "
"cover the protected core/contract/release change before commit."
)
def core_patch_notice(paths: Iterable[ProtectedPath | str]) -> str:
return (
"⚠️ CORE_PATCH_NOTICE: runtime_mode='pro' is editing protected "
"⚠️ CORE_PATCH_NOTICE: runtime_mode='pro' or 'cyber_pro' is editing protected "
"Ouroboros core/contract/release surface(s): "
f"{format_protected_paths(paths)}. These changes can be committed only "
"through the normal triad + scope review pipeline."

View file

@ -22,6 +22,7 @@ from ouroboros.llm import LLMClient
from ouroboros.loop_llm_call import classify_llm_exception, is_rate_limit_text
from ouroboros.pricing import emit_llm_usage_event, estimate_cost_optional, infer_provider_from_model
from ouroboros.utils import sanitize_tool_result_for_log, utc_now_iso
from ouroboros.config import runtime_setting
log = logging.getLogger(__name__)
@ -616,12 +617,12 @@ _PROVIDER_KEY_ENV = {
def _any_remote_provider_configured() -> bool:
return any(str(os.environ.get(k, "") or "").strip() for k in _REMOTE_PROVIDER_KEYS)
return any(str(runtime_setting(k, "") or "").strip() for k in _REMOTE_PROVIDER_KEYS)
def _any_local_routing_enabled() -> bool:
return any(
str(os.environ.get(k, "") or "").lower() in ("true", "1")
str(runtime_setting(k, "") or "").lower() in ("true", "1")
for k in _LOCAL_ROUTING_KEYS
)
@ -635,20 +636,20 @@ def _light_model_has_reachable_provider(light_model: str) -> bool:
return True # don't over-block on classifier failure
if key_type == "gigachat":
# GigaChat accepts either an authorization key (OAuth) or user/password.
has_creds = bool(str(os.environ.get("GIGACHAT_CREDENTIALS", "") or "").strip())
has_basic = bool(str(os.environ.get("GIGACHAT_USER", "") or "").strip()) and bool(
str(os.environ.get("GIGACHAT_PASSWORD", "") or "").strip()
has_creds = bool(str(runtime_setting("GIGACHAT_CREDENTIALS", "") or "").strip())
has_basic = bool(str(runtime_setting("GIGACHAT_USER", "") or "").strip()) and bool(
str(runtime_setting("GIGACHAT_PASSWORD", "") or "").strip()
)
return has_creds or has_basic
env_key = _PROVIDER_KEY_ENV.get(key_type)
if env_key is None:
return True
if not str(os.environ.get(env_key, "") or "").strip():
if not str(runtime_setting(env_key, "") or "").strip():
return False
if key_type == "openai-compatible":
base_url = (
str(os.environ.get("OPENAI_COMPATIBLE_BASE_URL", "") or "").strip()
or str(os.environ.get("OPENAI_BASE_URL", "") or "").strip()
str(runtime_setting("OPENAI_COMPATIBLE_BASE_URL", "") or "").strip()
or str(runtime_setting("OPENAI_BASE_URL", "") or "").strip()
)
if not base_url:
return False
@ -673,7 +674,7 @@ def _safety_deadline_epoch(ctx: Optional[Any]) -> Optional[float]:
def _resolve_safety_routing() -> Tuple[bool, bool, Optional[str]]:
"""Choose local/remote safety backend; unreachable fallback fails open."""
if str(os.environ.get("USE_LOCAL_LIGHT", "") or "").lower() in ("true", "1"):
if str(runtime_setting("USE_LOCAL_LIGHT", "") or "").lower() in ("true", "1"):
return True, False, None
light_model = get_light_model()

View file

@ -14,9 +14,102 @@ import hashlib
import json
import os
import sys
import contextlib
import contextvars
import copy
import threading
from dataclasses import dataclass
from pathlib import Path
from types import MappingProxyType
from typing import Mapping
SETTINGS_INTEGRITY_ENV = "OUROBOROS_SETTINGS_SHA256"
_TASK_SETTINGS = contextvars.ContextVar("ouroboros_task_settings", default=None)
# Only capture/projection holds this lock, never a task's execution lifetime.
SETTINGS_ENV_LOCK = threading.RLock()
@dataclass(frozen=True, repr=False)
class TaskSettingsSnapshot:
"""Private in-memory views; document values and env absence are distinct."""
settings: Mapping
environ: Mapping
def _next_task_setting(key: str) -> bool:
from ouroboros.settings_scales import IMMEDIATE_SETTINGS, RESTART_REQUIRED_SETTINGS
return key not in IMMEDIATE_SETTINGS and key not in RESTART_REQUIRED_SETTINGS and key != "OUROBOROS_RUNTIME_MODE"
def _projected_keys() -> set[str]:
from ouroboros.settings_defaults import RETIRED_COMMA_LIST_SETTING_KEYS, settings_env_keys
from ouroboros.model_slots import _LEGACY_SLOT_RENAMES
return (set(settings_env_keys()) | set(RETIRED_COMMA_LIST_SETTING_KEYS)
| {old for old, _new in _LEGACY_SLOT_RENAMES})
@contextlib.contextmanager
def task_settings_scope(snapshot):
"""Bind one task's settings in memory only; concurrent tasks keep their own view."""
token = _TASK_SETTINGS.set(snapshot)
try:
yield
finally:
_TASK_SETTINGS.reset(token)
def copy_task_settings_context(context) -> None:
"""Carry settings through context transfers that intentionally omit Main call state."""
context.run(_TASK_SETTINGS.set, _TASK_SETTINGS.get())
def runtime_setting(key: str, default=None):
"""Environment-shaped runtime read; absence in the snapshot stays absent."""
snapshot = _TASK_SETTINGS.get()
if snapshot is not None and _next_task_setting(key):
return snapshot.environ.get(key, default)
return os.environ.get(key, default)
def runtime_environ() -> dict[str, str]:
"""Explicit child environment with this task's next-task settings overlaid."""
with SETTINGS_ENV_LOCK:
env = dict(os.environ)
snapshot = _TASK_SETTINGS.get()
if snapshot is not None:
for key in _projected_keys() | snapshot.settings.keys():
if _next_task_setting(key):
if key not in snapshot.environ:
env.pop(key, None)
else:
env[key] = snapshot.environ[key]
return env
def runtime_settings(*, settings_reader=None) -> dict:
"""Runtime document view; owner writers continue using config.load_settings."""
from ouroboros import config
settings = dict((settings_reader or config.load_settings)() or {})
snapshot = _TASK_SETTINGS.get()
if snapshot is not None:
for key in settings.keys() | snapshot.settings.keys():
if not _next_task_setting(key):
continue
if key not in snapshot.settings:
settings.pop(key, None)
else:
settings[key] = copy.deepcopy(snapshot.settings[key])
return settings
def task_settings_snapshot(settings: dict, environ: dict) -> TaskSettingsSnapshot:
"""Keep document-only values and exact projected presence without serializing either."""
return TaskSettingsSnapshot(
MappingProxyType(copy.deepcopy(settings)), MappingProxyType(dict(environ)))
class SettingsIntegrityError(RuntimeError):

View file

@ -8,10 +8,10 @@ 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
from ouroboros.settings_integrity import runtime_setting
# 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). `ultra` = the codex
@ -61,7 +61,7 @@ def resolve_effort(task_type: str) -> str:
key = "OUROBOROS_EFFORT_TASK"
default = "medium"
raw = os.environ.get(key, default)
raw = runtime_setting(key, default)
return raw if raw in EFFORT_SCALE else default
@ -80,15 +80,17 @@ def resolve_prompt_cache_ttl() -> str:
(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()
raw = str(runtime_setting("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")
# Runtime mode and review enforcement are separate axes. ``cyber_pro`` is the
# owner-selected high-power access level; it remains an ordinary member of the
# same closed scale so every consumer shares one vocabulary and rank.
VALID_RUNTIME_MODES = ("light", "advanced", "pro", "cyber_pro")
# Lower rank = stricter scope. ``save_settings`` refuses agent self-elevation.
_RUNTIME_MODE_RANK = {"light": 0, "advanced": 1, "pro": 2}
_RUNTIME_MODE_RANK = {"light": 0, "advanced": 1, "pro": 2, "cyber_pro": 3}
def normalize_runtime_mode(value: Any) -> str:
@ -109,3 +111,46 @@ def normalize_safety_mode(value: Any) -> str:
_SAFETY_MODE_RANK = {"full": 2, "light": 1, "off": 0}
# Effect vocabulary shared by the owner gateway and task-local runtime readers.
IMMEDIATE_SETTINGS = frozenset({
"TOTAL_BUDGET",
# The OUTER per-call tool cap reads settings.json BEFORE env on every tool
# call in every process (loop_tool_execution.py), so a saved change bites
# the currently running task's next tool call. The inner shell subprocess
# timeout still prefers the worker env (next task) — disclosed residual.
"OUROBOROS_TOOL_TIMEOUT_SEC",
"GITHUB_TOKEN",
"GITHUB_REPO",
"OUROBOROS_UPDATE_CHANNEL",
# The save handler hot-reconfigures MCP itself before responding
# (_apply_settings_save_side_effects), and worker processes re-check the
# settings mtime on their next tool-schema read; a reconfigure failure is
# surfaced as a save warning instead of silently keeping the claim.
"MCP_ENABLED",
"MCP_SERVERS",
"MCP_TOOL_TIMEOUT_SEC",
})
RESTART_REQUIRED_SETTINGS = frozenset({
"OUROBOROS_MAX_WORKERS",
"OUROBOROS_SERVER_HOST",
# The host-service port is bound once at server startup.
"OUROBOROS_HOST_SERVICE_PORT",
# Pooled workers load the extension registry once at spawn and never
# reload it per task; the save-time server reload keeps the skills UI
# fresh, but agent tasks see the new repo only after a restart.
"OUROBOROS_SKILLS_REPO_PATH",
"LOCAL_MODEL_SOURCE",
"LOCAL_MODEL_FILENAME",
"LOCAL_MODEL_PORT",
"LOCAL_MODEL_N_GPU_LAYERS",
"LOCAL_MODEL_CONTEXT_LENGTH",
"LOCAL_MODEL_CHAT_FORMAT",
# Background cognition reads these at consciousness __init__, so a change
# only takes effect after restart (Phase 4 Evolution settings group).
"OUROBOROS_BG_WAKEUP_MIN",
"OUROBOROS_BG_WAKEUP_MAX",
"OUROBOROS_BG_MAX_ROUNDS",
})

View file

@ -131,7 +131,7 @@ _MODEL_SLOTS = _rows(("slot", "stateKey", "settingKey", "inputId", "label", "not
))
_REVIEW_MODES = _rows(("value", "label", "tone", "className", "copy"), (
("advisory", "Advisory", "Flexible", "advisory", "Faster and cheaper. Review still runs, but you decide how to handle findings. Best when you want iteration speed and can manually watch for drift."),
("advisory", "Advisory", "Flexible", "advisory", "Review still runs. After feedback, Ouroboros can fix, reject or finish; raw reviewer findings remain visible."),
("blocking", "Blocking", "Strict", "blocking", "Slower and more expensive, but much safer. Critical review findings stop commits, which dramatically reduces the chance of gradual code degradation."),
))
@ -139,6 +139,7 @@ _RUNTIME_MODES = _rows(("value", "label", "tone", "className", "copy"), (
("light", "Light", "Safest", "light", "Self-modification of the main repo is disabled. Best for trying Ouroboros out without repo self-modification."),
("advanced", "Advanced", "Default", "advanced", "Self-modification of the evolutionary layer is allowed (current behaviour). Protected core/contract/release files stay guarded by Advanced mode."),
("pro", "Pro", "Power", "pro", "Direct protected-surface mode. Protected core/contract/release edits are allowed on disk, but commits still require the normal triad + scope review gate."),
("cyber_pro", "Cyber Pro", "Maximum power", "cyber-pro", "Host and configuration authority, including credentials, models, Supervisor and protected rewrites. Review scope and Blocking or Advisory enforcement remain owner-controlled."),
))
_LOCAL_ROUTING_MODES = _rows(("value", "buttonLabel", "label", "flags"), (

View file

@ -128,7 +128,6 @@ BAND_PATHS = {
"ouroboros/gateway/control.py": "Entered the band from 966 lines: the update-flow redesign added the shared stash-first prologue (_stash_local_work_fenced/_unwind_stashed_update) and the review-wave affordability floor to the update apply orchestration (update-flow-redesign sprint, Q9/Q10 owner decisions).",
"ouroboros/gateway/extensions.py": "Extensions HTTP surface re-entered the band when the module endpoint moved to the in-memory reviewed bundle (widgets lifecycle 1a); shrink next touch.",
"ouroboros/gateway/host_service.py": "The one loopback callback boundary for reviewed skills: token auth, the chat/decision/presence/WS-relay routes and, with #667, the operation read/cancel that joins existing chat, routing, turn and task records; one trust boundary, one module.",
"ouroboros/gateway/settings.py": "Retiring persistent auto-Low removed the former giant debt; the remaining owner and reviewer settings endpoints stay centralized while tracked in the shrinking band.",
"ouroboros/gateways/claudexor.py": "The existing owned-engine gateway also owns typed model operations and exact-byte resource transfer; no second control client.",
"ouroboros/launcher_bootstrap.py": "Native seed version resync keeps manifest parsing and equal-version payload diagnostics with the existing bootstrap owner; no separate loader or overwrite policy.",
"ouroboros/loop_acceptance_review.py": "F6 upstream sync: the A-material acceptance family (paid identity, free replay, identical-refusal terminal, dialogue history) folded into the campaign review leaf per the sync principle (upstream leaf acceptance_dialogue.py retired)",
@ -230,14 +229,11 @@ BAND_PATHS = {
"web/modules/chat_activity.js": "Existing task activity renderer consumes the shared quota/auth wait state; no parallel task card or lifecycle.",
"web/modules/harness_accounts.js": None,
"web/modules/log_events.js": None,
"web/modules/onboarding_wizard.js": "Shrank INTO the band: the Claude Runtime onboarding card and its /api/claude-code/* polling were deleted with the retired transport (owner-approved Q4); no new content was added.",
"web/modules/review_presentation.js": "Review Checkpoint read-side grouping, lifecycle/verdict separation, and keyed disclosure reconciliation remain one pure adapter below the 1500-line band cap.",
"web/modules/reviewer_slots.js": "Owner-approved 5A editor: per-row Direct model / Configured subagent source picker with read-only derived disclosure replaces the legacy Claude-SDK advisory input in the same module that owns reviewer-row editing.",
"web/modules/settings.js": None,
"web/modules/skills.js": "One installed-skill page controller owns independently settling primary/optional reads and current-generation menu, identity and badge updates; domain lifecycle, cards, hub truth and shared interactions remain separate owners.",
"web/tests/chat_instance_dom.test.js": "Entered the band from 1000 lines with the alias-free subagent cost pin (stage-2 fix wave): that regression reproduces only through the real createChatInstance card path, and this file owns the DOM harness that drives it; split when the next createChatInstance face lands.",
"web/tests/harness_login_cards.test.js": "Login-card suite grew past 1000 lines with the name-the-account face cases (agy pickup, issue #232); split when the next face lands.",
"web/tests/review_presentation.test.js": "Review Checkpoint lifecycle and verdict reconciliation remain covered by one focused presentation suite.",
}
BYTE_BASELINE_DEBT = {

View file

@ -18,7 +18,7 @@ from ouroboros.skill_lifecycle_queue import LifecycleJobOptions, run_lifecycle_j
from ouroboros.skill_loader import (
discover_skills, find_skill, grant_status_for_skill, requested_core_setting_keys,
requested_skill_permissions, save_enabled, save_skill_grants, skill_conflict_status,
skill_review_gate, skill_state_dir,
skill_state_dir,
)
from ouroboros.tool_access import active_tool_profile, canonical_data_root
from ouroboros.utils import append_jsonl, read_json_dict, utc_now_iso
@ -32,7 +32,7 @@ def _refusal(message: str, status_code: int = 409, **facts: Any) -> dict[str, An
def _review_facts(skill: Any, drive_root: Path) -> dict[str, Any]:
stale = skill.review.is_stale_for(skill.content_hash)
gate = skill_review_gate(skill.review.status, stale=stale, findings=skill.review.findings)
gate = skill.review.gate_for(skill.content_hash)
return {
"skill": skill.name, "source": skill.source, "content_hash": skill.content_hash,
"review_status": skill.review.status, "review_stale": stale,

View file

@ -17,6 +17,9 @@ from datetime import datetime, timezone
from typing import Any, Awaitable, Callable, Deque, Dict, Optional
from ouroboros.utils import utc_now_iso as _now_iso
from ouroboros.config import runtime_setting
import contextvars
from ouroboros.settings_integrity import copy_task_settings_context
log = logging.getLogger(__name__)
@ -226,7 +229,7 @@ def _lifecycle_deadline_sec() -> float:
"""
from ouroboros.config import SETTINGS_DEFAULTS
raw = os.environ.get("OUROBOROS_SKILL_LIFECYCLE_TIMEOUT_SEC", "")
raw = runtime_setting("OUROBOROS_SKILL_LIFECYCLE_TIMEOUT_SEC", "")
try:
parsed = float(raw)
if parsed > 0:
@ -545,7 +548,9 @@ def run_lifecycle_job_blocking(
except BaseException as exc:
box["error"] = exc
thread = threading.Thread(target=_thread_main, name=f"skill-lifecycle-{kind}", daemon=False)
settings_context = contextvars.Context()
copy_task_settings_context(settings_context)
thread = threading.Thread(target=settings_context.run, args=(_thread_main,), name=f"skill-lifecycle-{kind}", daemon=False)
thread.start()
thread.join(timeout=_lifecycle_deadline_sec())
if thread.is_alive():

View file

@ -19,12 +19,10 @@ from ouroboros.contracts.plugin_api import FORBIDDEN_SKILL_SETTINGS
from ouroboros.contracts.schema_versions import with_schema_version
from ouroboros.skill_review_status import STATUS_BLOCKERS, STATUS_CLEAN, STATUS_PENDING, STATUS_WARNINGS, VALID_SKILL_REVIEW_STATUSES, aggregate_skill_review_status, normalize_skill_review_status, skill_review_gate
from ouroboros.utils import append_jsonl, atomic_write_json, read_json_dict, utc_now_iso
from ouroboros.review_records import validate_author_disposition
log = logging.getLogger(__name__)
# Constants
_MANIFEST_NAMES = ("SKILL.md", "skill.json")
# Only metadata/cache names are skipped. Non-metadata dotfiles remain hashed
# and reviewed because a skill subprocess can import/source/read them.
@ -53,16 +51,11 @@ def review_status_allows_execution(status: str) -> bool:
GRANTS_FILENAME = "grants.json"
SELF_AUTHORED_MARKER_FILENAME = ".self_authored.json"
# CPL4-C10: every per-skill owner-state document the runtime authors carries
# the shared ABI-2 stamp on write (review.json, enabled.json, grants.json,
# review_job.json, owner_attestation.json, accepted_rebuttals.json). Readers
# keep legacy-0 tolerance: unstamped files are never retrofitted on read.
# Stamp every authored skill owner-state document (CPL4-C10 / ABI-2);
# legacy-0 readers never retrofit a stamp on read.
SKILL_OWNER_STATE_SCHEMA_VERSION = 1
# Dataclasses
@dataclass
class SkillReviewState:
"""Persisted skill review verdict tied to a content hash."""
@ -78,13 +71,18 @@ class SkillReviewState:
raw_actor_records: List[Dict[str, Any]] = field(default_factory=list)
advisory_result: Dict[str, Any] = field(default_factory=dict)
review_profile: str = ""
# Reviewer identity and current author acceptance are independent evidence.
author_disposition: Dict[str, Any] = field(default_factory=dict)
reviewed_content_hash: str = "" # Original reviewer hash on earlier author-finish records.
def is_stale_for(self, current_hash: str) -> bool:
if not current_hash:
return True
if not self.content_hash:
return True
return self.content_hash != current_hash
return not current_hash or (self.reviewed_content_hash or self.content_hash) != current_hash
def gate_for(self, current_hash: str, *, enforcement: Optional[str] = None) -> Dict[str, Any]:
return {**skill_review_gate(self.status, stale=self.is_stale_for(current_hash),
enforcement=enforcement, findings=self.findings,
author_disposition=self.author_disposition, current_hash=current_hash),
"reviewed_content_hash": self.reviewed_content_hash or self.content_hash}
def to_dict(self) -> Dict[str, Any]:
data = {
@ -101,6 +99,10 @@ class SkillReviewState:
data["review_profile"] = str(self.review_profile)
if self.advisory_result:
data["advisory_result"] = dict(self.advisory_result)
if self.author_disposition:
data["author_disposition"] = dict(self.author_disposition)
if self.reviewed_content_hash:
data["reviewed_content_hash"] = str(self.reviewed_content_hash)
has_review_verdicts = any(
str(f.get("verdict") or "").upper() in {"PASS", "FAIL"}
for f in self.findings
@ -136,9 +138,7 @@ class LoadedSkill:
if not self.manifest.is_script():
# instruction has no payload; extension runs through PluginAPI.
return False
if not review_status_allows_execution(self.review.status):
return False
if self.review.is_stale_for(self.content_hash):
if not self.review.gate_for(self.content_hash)["executable_review"]:
return False
from ouroboros.tools.skill_exec import _resolve_runtime_binary, _resolve_script_path
@ -170,9 +170,6 @@ class _SkillLocationCandidate:
skill_dir: pathlib.Path
# Disk paths
def _skills_state_root(drive_root: pathlib.Path) -> pathlib.Path:
return pathlib.Path(drive_root) / "state" / "skills"
@ -253,9 +250,6 @@ def is_self_authored_skill_dir(
)
# Manifest discovery
class _ManifestUnreadable(RuntimeError):
"""A manifest file exists but could not be read (permissions,
truncation, IO error, etc.). Callers translate this into a
@ -495,9 +489,6 @@ def compute_content_hash(
return reduce_skill_content_hash(file_digests)
# State persistence
def load_enabled(drive_root: pathlib.Path, name: str) -> bool:
state = read_json_dict(skill_state_dir(drive_root, name) / "enabled.json")
if not isinstance(state, dict):
@ -623,6 +614,9 @@ def load_review_state(
if isinstance(data.get("advisory_result"), dict)
else {}
)
author_disposition = validate_author_disposition(
data.get("author_disposition"),
) or {}
try:
prompt_chars = int(data.get("prompt_chars") or 0)
except (TypeError, ValueError):
@ -643,6 +637,8 @@ def load_review_state(
raw_actor_records=[r for r in raw_actor_records if isinstance(r, dict)],
advisory_result=dict(advisory_result),
review_profile=review_profile,
author_disposition=author_disposition,
reviewed_content_hash=str(data.get("reviewed_content_hash") or ""),
)
@ -835,7 +831,7 @@ def grant_status_for_skill(drive_root: pathlib.Path, skill: LoadedSkill) -> Dict
granted_permissions = [perm for perm in requested_permissions if perm in persisted_permissions]
missing = [key for key in requested if key not in set(granted)]
missing_permissions = [perm for perm in requested_permissions if perm not in set(granted_permissions)]
review_ready = review_status_allows_execution(skill.review.status) and not skill.review.is_stale_for(skill.content_hash)
review_ready = skill.review.gate_for(skill.content_hash)["executable_review"]
# Scripts receive core keys via _scrub_env; extensions via PluginAPI.
# Instruction skills cannot receive core keys.
eligible_type = skill.manifest.is_script() or skill.manifest.is_extension()
@ -883,9 +879,7 @@ def auto_grant_if_enabled(drive_root: pathlib.Path, skill: LoadedSkill) -> AutoG
return outcome
if skill.load_error:
return outcome
if skill.review.is_stale_for(skill.content_hash):
return outcome
if not review_status_allows_execution(skill.review.status):
if not skill.review.gate_for(skill.content_hash)["executable_review"]:
return outcome
if normalize_skill_review_status(skill.review.status) == _REVIEW_STATUS_PENDING:
return outcome
@ -909,9 +903,6 @@ def auto_grant_if_enabled(drive_root: pathlib.Path, skill: LoadedSkill) -> AutoG
)
# Discovery / loading
def _safe_listdir(root: pathlib.Path) -> List[pathlib.Path]:
try:
return sorted(p for p in root.iterdir() if p.is_dir() and not p.name.startswith("."))
@ -1495,7 +1486,7 @@ def summarize_skills(drive_root: pathlib.Path) -> Dict[str, Any]:
available = blocked_by_grants = pending_review = blocker_review = warning_review = broken = 0
for s in skills:
stale = s.review.is_stale_for(s.content_hash)
gate = skill_review_gate(s.review.status, stale=stale, findings=s.review.findings)
gate = s.review.gate_for(s.content_hash)
if s.identity_collision:
# Readiness probes include lifecycle/dependency state. A collision
# has no unique lifecycle identity, so its UI projection must stay
@ -1514,7 +1505,7 @@ def summarize_skills(drive_root: pathlib.Path) -> Dict[str, Any]:
blocked_by_grants += int(s.available_for_execution and not grants_usable)
pending_review += int(
s.review.status in (_REVIEW_STATUS_PENDING, "")
or (review_status_allows_execution(s.review.status) and stale)
or (stale and not gate["executable_review"])
)
blocker_review += int(s.review.status == _REVIEW_STATUS_FAIL)
warning_review += int(s.review.status == _REVIEW_STATUS_ADVISORY)
@ -1534,6 +1525,8 @@ def summarize_skills(drive_root: pathlib.Path) -> Dict[str, Any]:
"review_status": s.review.status,
"review_stale": stale,
"review_gate": gate,
"author_disposition": dict(s.review.author_disposition),
"reviewed_content_hash": gate["reviewed_content_hash"],
"executable_review": gate["executable_review"],
"available_for_execution": runnable,
"runnable_via_skill_exec": s.available_for_execution,

View file

@ -251,7 +251,7 @@ def capture_skill_publish_snapshot(loaded: LoadedSkill) -> SkillPublishSnapshot:
"""Capture one candidate and bind it to the stored review hash."""
snapshot = capture_skill_publish_candidate(loaded)
stored_review_hash = str(getattr(loaded.review, "content_hash", "") or "")
stored_review_hash = str(loaded.review.reviewed_content_hash or loaded.review.content_hash or "")
if not stored_review_hash or snapshot.content_hash != stored_review_hash:
raise SkillPublishSnapshotError("snapshot_review_stale")
return snapshot

View file

@ -5,7 +5,6 @@ import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from ouroboros.skill_review_status import skill_review_gate
log = logging.getLogger(__name__)
@ -148,6 +147,7 @@ def acceptance_skill_lifecycle(
"content_hash": skill.content_hash,
"review_status": str(getattr(skill.review, "status", "") or ""),
"review_stale": bool(skill.review.is_stale_for(skill.content_hash)),
"review_gate": skill.review.gate_for(skill.content_hash),
"enabled": bool(getattr(skill, "enabled", False)),
"ready": bool(readiness.ready),
"blockers": list(readiness.blockers),
@ -264,8 +264,8 @@ def skill_readiness_for_execution(
next_actions.append({"phase": "payload", "tool": "skill_preflight", "reason": msg})
stale = skill.review.is_stale_for(skill.content_hash)
gate = skill_review_gate(skill.review.status, stale=stale)
if stale:
gate = skill.review.gate_for(skill.content_hash)
if stale and not gate["executable_review"]:
blockers.append("review_stale")
agent_fixable.append("review_stale")
next_actions.append({"phase": "review", "tool": "skill_review", "reason": "review_stale"})

View file

@ -153,6 +153,12 @@ Skill Review Checklist items permit the behaviour in isolation. Treat
BIBLE.md as the tie-breaker when a skill looks checklist-compliant but
contradicts the runtime's constitutional commitments.
After the first actual review, the author may finish the advisory dialogue for
the exact current content hash.
That author disposition is a separate durable stance beside these raw findings;
it is never a reviewer PASS, never valid for stale content, and never bypasses
deterministic preflight or a blocking enforcement gate.
{bible_text}
{skill_host_context}

View file

@ -196,10 +196,15 @@ def preflight_failed(findings: Any) -> bool:
def skill_review_gate(
status: str, *, stale: bool = False, enforcement: Optional[str] = None,
findings: Any = None,
findings: Any = None, author_disposition: Any = None, current_hash: str = "",
) -> Dict[str, Any]:
"""Structured, agent-facing explanation of whether a review is executable.
A current author acceptance admits changed bytes only in Advisory; stale
still describes the original reviewer evidence, never the author hash.
Author fields are optional and appear only with a valid author disposition;
callers without one retain the frozen gate key set.
Deterministic hard-gate failures (e.g. skill_preflight) are persisted as
STATUS_PENDING by `_run_deterministic_preflight`, so they are non-executable
here under every enforcement mode without needing per-caller findings — only
@ -216,6 +221,11 @@ def skill_review_gate(
actions: the cheap Re-review (which reruns the preflight) stays primary,
with Repair offered based on the last recorded preflight.
"""
from ouroboros.review_records import validate_author_disposition
author = validate_author_disposition(author_disposition)
author_current = bool(current_hash and author and author["subject_hash"] == current_hash
and author["enforcement"] == "advisory")
raw_status = normalize_skill_review_status(status)
if enforcement is None:
try:
@ -228,6 +238,10 @@ def skill_review_gate(
executable = False
reason = "review_pending"
summary = "Review is pending or did not produce an executable verdict."
elif author_current and enforcement == "advisory":
executable = True
reason = "author_accepted_advisory"
summary = "The author accepted the current payload under Advisory; the original reviewer verdict and hash are unchanged."
elif stale:
executable = False
reason = "review_stale"
@ -256,6 +270,8 @@ def skill_review_gate(
return {
"status": raw_status or STATUS_PENDING,
"stale": bool(stale),
**({"author_accepted": reason == "author_accepted_advisory",
"author_disposition": author} if author else {}),
"executable_review": bool(executable),
"blocking_reason": reason,
"review_enforcement": enforcement,

View file

@ -24,6 +24,7 @@ from ouroboros.configured_subagents import (
resolve_configured_subagents,
)
from ouroboros.route_spec import route_spec_dict
from ouroboros.settings_integrity import SETTINGS_ENV_LOCK, TaskSettingsSnapshot, runtime_setting
from ouroboros.utils import utc_now_iso
@ -68,7 +69,7 @@ def effective_runtime_subagent_settings(settings: Mapping[str, Any]) -> dict[str
for key in _RUNTIME_LEGACY_KEYS:
# Absence is meaningful: apply_settings_to_env removes a normalized-empty
# setting, so retaining the raw disk value here would undo normalization.
effective[key] = os.environ.get(key, "")
effective[key] = runtime_setting(key, "")
return effective
@ -132,24 +133,34 @@ def model_visible_subagent_catalog(settings: Mapping[str, Any]) -> dict[str, Any
def current_model_visible_subagent_catalog() -> dict[str, Any]:
"""Read the current normalized settings and return the stable catalog."""
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
return model_visible_subagent_catalog(
effective_runtime_subagent_settings(load_settings())
effective_runtime_subagent_settings(runtime_settings())
)
def apply_task_start_settings() -> None:
"""Project the provider-normalized in-memory snapshot for one task start."""
from ouroboros.config import apply_settings_to_env, load_settings
def apply_task_start_settings() -> TaskSettingsSnapshot:
"""Capture a task's normalized projection before publishing the process view."""
from ouroboros import config
from ouroboros.server_runtime import apply_runtime_provider_defaults
from ouroboros.settings_integrity import task_settings_snapshot
effective, _changed, _keys = apply_runtime_provider_defaults(load_settings())
apply_settings_to_env(effective)
fd = config._acquire_settings_lock()
try:
with SETTINGS_ENV_LOCK:
effective, _changed, _keys = apply_runtime_provider_defaults(
config.load_settings_lock_held(_settings_lock_held=fd is not None))
projected = dict(os.environ)
config.apply_settings_to_env(effective, environ=projected)
snapshot = task_settings_snapshot(effective, projected)
config.apply_settings_to_env(effective)
return snapshot
finally:
config._release_settings_lock(fd)
def apply_task_start_settings_or_disclose(task_id: str, emit_live_log: Any) -> None:
def apply_task_start_settings_or_disclose(task_id: str, emit_live_log: Any) -> TaskSettingsSnapshot:
"""Task-start settings reload with a LOUD failure path (#285).
A silent failure breaks the save-time promise "the saved changes apply
@ -162,6 +173,15 @@ def apply_task_start_settings_or_disclose(task_id: str, emit_live_log: Any) -> N
of raising, which would keep exactly the silence this wrapper exists to
break. A MISSING file is legitimate (defaults-only install), not a fault.
"""
from ouroboros.settings_integrity import task_settings_snapshot
from ouroboros.config import SETTINGS_DEFAULTS, settings_env_keys
with SETTINGS_ENV_LOCK:
previous_env = dict(os.environ)
previous_settings = dict(SETTINGS_DEFAULTS)
previous_settings.update({key: previous_env.get(key, "") for key in settings_env_keys()})
previous = task_settings_snapshot(previous_settings, previous_env)
try:
from ouroboros import config as _config
@ -171,21 +191,23 @@ def apply_task_start_settings_or_disclose(task_id: str, emit_live_log: Any) -> N
raw_settings_text = None
if raw_settings_text is not None:
json.loads(raw_settings_text)
apply_task_start_settings()
return apply_task_start_settings()
except Exception as exc:
import logging
logging.getLogger(__name__).error(
"Task-start settings reload failed; this task runs on the previously applied configuration",
"Task-start settings reload failed; this task uses the environment from the previously applied configuration; document-only values are unavailable",
exc_info=True,
)
emit_live_log(
"task_start_settings_reload_failed",
task_id=task_id,
error=f"{type(exc).__name__}: {exc}",
message=("Settings reload failed at task start: this task runs "
"on the previously applied configuration."),
message=("Settings reload failed at task start: this task uses the environment "
"from the previously applied configuration; document-only values "
"could not be recovered."),
)
return previous
def _resolution(
@ -492,10 +514,10 @@ def current_subagent_alternatives(exclude_id: str = "") -> list[dict[str, Any]]:
"""Project the current saved choices without ranking or probing them."""
try:
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
resolution = resolve_configured_subagents(
effective_runtime_subagent_settings(load_settings())
effective_runtime_subagent_settings(runtime_settings())
)
except Exception:
return []
@ -713,10 +735,10 @@ def exact_start(ctx: Any, prompt: str, spec: Optional[dict[str, Any]] = None) ->
"A fresh delegated start requires subagent_id; only retry_of replays without it.",
)
if selected_id:
from ouroboros.config import load_settings
from ouroboros.config import runtime_settings
selected_snapshot, _legacy = select_subagent_snapshot(
effective_runtime_subagent_settings(load_settings()),
effective_runtime_subagent_settings(runtime_settings()),
subagent_id=selected_id,
)
if selected_snapshot is not None:

View file

@ -23,11 +23,11 @@ exactly as before, through ``config.resolve_effort(task_type)``.
from __future__ import annotations
import logging
import os
from dataclasses import dataclass, field, replace as dataclass_replace
from datetime import datetime, timezone # noqa: F401
from typing import Any, Dict, List, Mapping
from ouroboros.config import runtime_setting
from ouroboros.config import (
SETTINGS_DEFAULTS,
get_heavy_model,
@ -234,7 +234,7 @@ def get_subagent_harness() -> DelegationRoute | None:
pricing, and bench provenance.
"""
raw = str(
os.environ.get("OUROBOROS_SUBAGENT_HARNESS", "")
runtime_setting("OUROBOROS_SUBAGENT_HARNESS", "")
or SETTINGS_DEFAULTS.get("OUROBOROS_SUBAGENT_HARNESS", "")
).strip()
route = parse_subagent_harness(raw)
@ -253,7 +253,7 @@ def get_subagent_harness() -> DelegationRoute | None:
# spelling, and this is the ONLY reader of the pin key. Empty = the
# engine's quota-aware rotation pool (D28).
profile = str(
os.environ.get("OUROBOROS_SUBAGENT_PROFILE", "")
runtime_setting("OUROBOROS_SUBAGENT_PROFILE", "")
or SETTINGS_DEFAULTS.get("OUROBOROS_SUBAGENT_PROFILE", "")
).strip()
if profile:
@ -549,7 +549,7 @@ def normalize_subagent_model_lane(value: Any) -> str:
def _slot_model(key: str) -> str:
return str(os.environ.get(key, "") or SETTINGS_DEFAULTS.get(key, "") or "").strip()
return str(runtime_setting(key, "") or SETTINGS_DEFAULTS.get(key, "") or "").strip()
_LANE_SLOT_KEYS = {
@ -574,7 +574,7 @@ def lane_ran_on_main(lane: str, model: str) -> bool:
"""
if lane not in {"heavy", "light"}:
return False
env_slot = str(os.environ.get(_LANE_SLOT_KEYS[lane][0], "") or "").strip()
env_slot = str(runtime_setting(_LANE_SLOT_KEYS[lane][0], "") or "").strip()
return not env_slot or bool(model and model != env_slot)
@ -587,13 +587,13 @@ def _use_local_for_lane(lane: str, model: str) -> bool:
# Follows Main's local flag, so USE_LOCAL_MAIN governs the effective model
# rather than being silently ignored.
return _use_local_for_lane("main", model)
slot_value = str(os.environ.get(model_key, "") or "").strip()
slot_value = str(runtime_setting(model_key, "") or "").strip()
if lane == "main":
slot_value = slot_value or str(SETTINGS_DEFAULTS.get(model_key, "") or "").strip()
return (
bool(model)
and model == slot_value
and str(os.environ.get(local_key, "") or "").strip().lower() in {"1", "true", "yes", "on"}
and str(runtime_setting(local_key, "") or "").strip().lower() in {"1", "true", "yes", "on"}
)

View file

@ -15,6 +15,7 @@ from ouroboros.cost_projection import (
normalize_task_result_cost_planes,
)
from ouroboros.utils import read_json_dict, update_json_locked, utc_now_iso
from ouroboros.review_records import validate_author_disposition
log = logging.getLogger(__name__)
@ -27,9 +28,7 @@ STATUS_FAILED = "failed"
STATUS_INTERRUPTED = "interrupted"
STATUS_CANCELLED = "cancelled"
# ABI 7.0 (Q8=B) schema admission lives in ouroboros/task_result_schema.py
# (module-size split); re-exported: every caller and test reaches the stamp,
# the classifier and the quarantine through this module (F401 intended).
# ABI 7.0 (Q8=B): re-export schema admission, stamping and quarantine for callers.
from ouroboros.task_result_schema import ( # noqa: F401
QUARANTINED_SCHEMA_REASON, TASK_RESULT_QUARANTINE_DIR, TASK_RESULT_SCHEMA_VERSION,
emit_quarantine_event as _emit_quarantine_event,
@ -264,33 +263,21 @@ def project_task_acceptance_review_capacity(
"reason": f"review_capacity_unknown:{type(exc).__name__}",
}
# Intent latch: the agent/owner asked to cancel, but the supervisor has not yet
# torn the task down. Ranks above running so a late running/scheduled mirror
# cannot resurrect it, but below the truly-terminal statuses so the eventual
# STATUS_CANCELLED write still lands.
# Cancel intent outranks running mirrors but not terminal states: stale progress
# cannot resurrect the task, and the supervisor's final CANCELLED write still lands.
STATUS_CANCEL_REQUESTED = "cancel_requested"
# The flat task-scope cost fields shared by live task events, progress-row
# replay, task_summary chat rows, and the persisted result written here (v6.82
# P1) — one home, so no consumer grows a divergent literal list.
# DERIVED from the cost SSOT (``ouroboros/cost_projection.py``) rather than
# re-typed: the HONEST names only (ABI 7.0/ABI-3: the retired
# ``cost_usd[_with_children]`` aliases are read-tolerance, never carried
# forward — a consumer copying by this list from a possibly-legacy source must
# resolve the pair with ``carry_cost_meta`` instead of a key loop) and EVERY
# accounting openness/integrity marker. Hand-maintained copies are how a
# marker reaches one surface and not the next: ``non_final_rows`` rides with
# ``cost_final`` because it is that flag's DISCLOSED CAUSE (v6.89.0 panel D2),
# and ``ledger_integrity_degraded`` was produced by the authority but named in
# no list at all, so it never reached any surface.
# Events, progress replay, chat summaries and results share the cost-projection
# SSOT's current names and ALL openness/integrity markers. Legacy cost_usd aliases
# are read-only compatibility: use carry_cost_meta, not a key loop, to copy them.
# In particular, non_final_rows explains cost_final; omitting it or
# ledger_integrity_degraded would silently lose the authority's uncertainty.
TASK_COST_META_FIELDS = tuple(dict.fromkeys(
[new for new, _old in COST_ALIAS_PAIRS] + list(COST_OPENNESS_FIELDS)
))
# Monotonic lifecycle ordering. A write that would move a task *backwards* past
# the cancel-intent latch or a terminal status is ignored, so a stale
# scheduled/running mirror can never clobber a cancel/terminal outcome
# (the "ghost subagent" class). Unknown statuses are unranked and never block.
# Monotonic lifecycle: stale scheduled/running mirrors cannot overwrite cancellation
# or terminal outcomes (the ghost-subagent class). Unknown statuses never block.
_TRULY_TERMINAL_STATUSES = frozenset({
STATUS_COMPLETED,
STATUS_FAILED,
@ -970,18 +957,12 @@ def write_task_result(
)
# --------------------------------------------------------------------------- plan review state
#
# ``plan_review_state`` v2 (plan-review redesign, 2026-08-15): the durable record of
# every ``plan_task`` cycle of ONE task. Task level: ``series_id`` (fresh per first v2
# wave), ``cycles_paid`` (paid reviewer panels — the shared cap ``review_max_cycles()``
# binds it), ``need_evidence_seen`` (per-task memory: one locator may be requested once),
# ``current_attempt`` (the fingerprint the gate projects + open|unavailable|rail_degraded),
# ``waves`` (bounded: the last ``_PLAN_REVIEW_FULL_WAVES`` in full, older ones compacted,
# ``waves_omitted`` beyond ``_PLAN_REVIEW_MAX_WAVES``). A v1 record is READ-ONLY: it
# loads without error under ``legacy_v1``; an open v1 wave projects as
# ``legacy_open_requires_resubmission`` (S5 — never auto-closed) until a NEW plan_task
# call starts a fresh v2 series.
# plan_review_state v2 records each task's plan_task cycles: a fresh series_id,
# cycles_paid bounded by review_max_cycles(), need_evidence_seen (one request per
# locator), and current_attempt (fingerprint + open/unavailable/rail_degraded).
# Keep _PLAN_REVIEW_FULL_WAVES whole, compact older waves and count waves_omitted
# beyond _PLAN_REVIEW_MAX_WAVES. v1 remains read-only under legacy_v1; an open wave
# requires resubmission until a new plan_task starts v2, never automatic closure.
_PLAN_REVIEW_FULL_WAVES = 8
_PLAN_REVIEW_MAX_WAVES = 64
@ -1073,6 +1054,10 @@ def _validated_plan_review_state(value: Any) -> Dict[str, Any]:
raise ValueError("PLAN_REVIEW_STATE_INVALID: full wave needs spec and findings")
if not isinstance(wave.get("dispositions", []), list):
raise ValueError("PLAN_REVIEW_STATE_INVALID: dispositions must be a list")
if "author_disposition" in wave and validate_author_disposition(
wave["author_disposition"], subject_hash=fingerprint,
) is None:
raise ValueError("PLAN_REVIEW_STATE_INVALID: author_disposition is malformed or stale")
seen.add(fingerprint)
cycles_paid = value.get("cycles_paid", 0)
if not isinstance(cycles_paid, int) or isinstance(cycles_paid, bool) or cycles_paid < 0:
@ -1414,6 +1399,8 @@ def _compact_plan_review_wave(wave: Dict[str, Any]) -> Dict[str, Any]:
"closed": bool(wave.get("closed")),
"paid": bool(wave.get("paid")),
"wave_artifact": copy.deepcopy(wave.get("wave_artifact") or {}),
**({"author_disposition": copy.deepcopy(wave["author_disposition"])}
if isinstance(wave.get("author_disposition"), dict) else {}),
**({"spec_source_ref": copy.deepcopy(wave["spec_source_ref"])} if wave.get("spec_source_ref") else {}),
**({"reviewed_at": str(wave["reviewed_at"])} if wave.get("reviewed_at") else {}),
}
@ -1556,6 +1543,7 @@ def record_plan_review_dispositions(
closure_notes: Optional[List[str]] = None,
wave_artifact: Optional[Dict[str, Any]] = None,
recorded_at: str = "",
author_disposition: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Store the agent's dispositions on one FULL wave and its resulting closure.
Only note-only closed waves accept annotations. Closure authority remains
@ -1576,6 +1564,14 @@ def record_plan_review_dispositions(
wave["closure_notes"] = list(closure_notes)
if wave_artifact is not None:
wave["wave_artifact"] = copy.deepcopy(wave_artifact)
if author_disposition is not None:
author = validate_author_disposition(
author_disposition,
subject_hash=fingerprint,
)
if author is None:
raise ValueError("PLAN_REVIEW_AUTHOR_DISPOSITION_INVALID: stale or malformed record")
wave["author_disposition"] = author
if closed and str(wave.get("aggregate") or "") == "REVIEW_REQUIRED":
wave["closed"] = True
state["current_attempt"] = {"fingerprint": fingerprint, "status": "open", "reason": ""}

View file

@ -90,7 +90,7 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
at schedule time (and the child sees first line of its context) what the child
CAN and CANNOT do. Prevents the wasted rounds where a prober child hit
workspace_blocked on run_script because neither side knew shell was off."""
matrix = _POLICY.get(profile, {})
matrix = _POLICY.get(_effective_policy_profile(profile), {})
shell_roots = sorted(root for root, ops in matrix.items() if "shell" in ops)
write_roots = sorted(root for root, ops in matrix.items() if ops & {"write", "edit"})
has_shell = bool(shell_roots)
@ -105,20 +105,36 @@ def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = ""
return "child capabilities — " + " · ".join(bits)
def _effective_policy_profile(profile: ToolProfile) -> ToolProfile:
"""Map an acting child to the existing full matrix only in Cyber Pro."""
if profile != "acting_subagent":
return profile
try:
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(get_runtime_mode(), "cyber_pro"):
return "operator_control"
except Exception:
pass
return profile
def decide_tool_access(
*,
profile: ToolProfile,
root: ResourceRoot,
operation: Operation,
) -> ToolAccessDecision:
allowed = operation in _POLICY.get(profile, {}).get(root, set())
effective_profile = _effective_policy_profile(profile)
allowed = operation in _POLICY.get(effective_profile, {}).get(root, set())
if allowed:
return ToolAccessDecision(True, guard=f"{profile}:{root}:{operation}")
allowed_roots = ", ".join(sorted(r for r, ops in _POLICY.get(profile, {}).items() if operation in ops)) or "(none)"
return ToolAccessDecision(True, guard=f"{effective_profile}:{root}:{operation}")
allowed_roots = ", ".join(sorted(r for r, ops in _POLICY.get(effective_profile, {}).items() if operation in ops)) or "(none)"
return ToolAccessDecision(
False,
reason=f"profile={profile} cannot {operation} root={root}. Roots your profile can {operation}: {allowed_roots}.",
guard=f"{profile}:{root}:{operation}",
reason=f"profile={effective_profile} cannot {operation} root={root}. Roots your profile can {operation}: {allowed_roots}.",
guard=f"{effective_profile}:{root}:{operation}",
)
@ -131,7 +147,7 @@ def subagent_profile_satisfies(profile: ToolProfile, needs: Iterable[str]) -> tu
operation on at least one root.
"""
ops_by_root = _POLICY.get(profile, {})
ops_by_root = _POLICY.get(_effective_policy_profile(profile), {})
available_ops = {op for ops in ops_by_root.values() for op in ops}
missing: list[str] = []
for need in needs or []:
@ -244,7 +260,7 @@ def filesystem_affordance_map(ctx: Any, *, runtime_mode: str = "") -> dict[str,
"""
profile = active_tool_profile(ctx)
policy = _POLICY.get(profile, {})
policy = _POLICY.get(_effective_policy_profile(profile), {})
# H2 (capinv-447): a root is writable iff a MUTATING operation is granted on
# it. Grouping "vcs" as write-like claimed writable roots for the read-only
# child profile (status/diff-only vcs), contradicting summarize_subagent_profile.

View file

@ -59,10 +59,12 @@ def _deliverables_root() -> pathlib.Path:
instead of escaping to the real ``~/Ouroboros/Deliverables`` (which the outside-home
check would then reject). Otherwise the global config default applies.
"""
from ouroboros.config import runtime_setting
from ouroboros.config import get_deliverables_root
jail = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
explicit = (os.environ.get("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
explicit = (runtime_setting("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
if explicit:
return pathlib.Path(explicit).expanduser().resolve(strict=False)
if jail and not explicit:

View file

@ -181,6 +181,18 @@ def user_files_path_block_reason(
# name shapes are never consulted here, so this branch must stay free
# of any credential_shapes import (import-boundary test).
return ""
try:
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
from ouroboros.tool_access import active_tool_profile
# Cyber Pro is the explicit owner authority for credential-file
# mutation. A deliberately readonly child remains excluded; acting
# children inherit the same authority through the existing profile.
if runtime_mode_at_least(get_runtime_mode(), "cyber_pro") and active_tool_profile(ctx) != "local_readonly_subagent":
return ""
except Exception:
pass
from ouroboros.credential_shapes import user_files_mutation_shape_reason
return user_files_mutation_shape_reason(resolved, home)

View file

@ -130,6 +130,32 @@ ACTING_SUBAGENT_TOOL_NAMES: frozenset[str] = frozenset({
"list_available_tools",
})
# Cyber Pro keeps the acting-child lineage and custody contract, while exposing
# the existing review, skill and owner-runtime tools. Commit/live-body tools
# stay outside this extension and explicit task disabled_tools still win.
CYBER_PRO_ACTING_TOOL_NAMES: frozenset[str] = frozenset({
"review_status", "preflight_review", "advisory_review",
"task_acceptance_review", "plan_task",
"list_skills", "skill_preflight", "skill_review", "skill_exec",
"toggle_skill", "skill_owner_action",
"set_tool_timeout", "request_deep_self_review", "toggle_evolution",
"toggle_consciousness",
})
def acting_tool_names_for_context(ctx: object) -> frozenset[str]:
"""Return the acting allowlist after applying the effective Cyber mode."""
names = set(ACTING_SUBAGENT_TOOL_NAMES)
try:
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
if runtime_mode_at_least(get_runtime_mode(), "cyber_pro"):
names.update(CYBER_PRO_ACTING_TOOL_NAMES)
except Exception:
pass
return frozenset(names)
READ_ONLY_PARALLEL_TOOLS: frozenset[str] = frozenset({
"read_file", "list_files",
"search_code", "query_code", "recent_tasks",

View file

@ -20,6 +20,7 @@ except ImportError:
_HAS_STEALTH = False
from ouroboros import browser_policy
from ouroboros.config import runtime_setting
from ouroboros.tool_access import active_tool_profile
from ouroboros.tools.registry import ToolContext, ToolEntry
from ouroboros.tools.tool_result import _compose_execute_result
@ -33,6 +34,16 @@ _MISSING_EXECUTABLE_RE = re.compile(r"Executable doesn't exist at ([^\n]+)")
_SUPPORTED_BROWSER_ENGINES = frozenset({"chromium", "webkit"})
def _runtime_mode_for_browser(ctx: Any) -> str:
"""Read the effective mode for owner-control browser operations."""
try:
from ouroboros.config import get_runtime_mode
return get_runtime_mode()
except Exception:
return "advanced"
def _normalize_browser_engine(engine: str = "") -> str:
value = str(engine or "chromium").strip().lower()
if value not in _SUPPORTED_BROWSER_ENGINES:
@ -445,7 +456,8 @@ def _ensure_browser(ctx: ToolContext, *, engine: str = "chromium", device: str =
def route_request(route: Any) -> None:
try:
reason = browser_policy.browser_request_block_reason(
route.request, ctx, restricted=readonly_subagent)
route.request, ctx, restricted=readonly_subagent,
runtime_mode=_runtime_mode_for_browser(ctx))
except Exception:
log.warning("Browser request policy could not read target identity", exc_info=True)
reason = "BROWSER_POLICY_UNAVAILABLE: runtime service identity could not be read"
@ -642,7 +654,7 @@ def _inject_native_screenshot(ctx: ToolContext, b64: str) -> str:
active_model = (
str(getattr(ctx, "active_model", "") or "")
or str(getattr(ctx, "task_model_override", "") or "")
or str(os.environ.get("OUROBOROS_MODEL", "") or "")
or str(runtime_setting("OUROBOROS_MODEL", "") or "")
)
from ouroboros.model_slots import task_model_binding
from ouroboros.model_wait import current_model_wait
@ -1000,7 +1012,10 @@ def _browser_action(ctx: ToolContext, action: str, selector: str = "",
elif normalized_action == "evaluate":
if not value:
return "Error: value (JS code) required for evaluate"
if reason := browser_policy.browser_evaluate_block_reason(str(getattr(page, "url", "") or ""), value, ctx):
if reason := browser_policy.browser_evaluate_block_reason(
str(getattr(page, "url", "") or ""), value, ctx,
runtime_mode=_runtime_mode_for_browser(ctx),
):
return reason
try:
result = _evaluate_bounded(page, value, effective_default_ms)

View file

@ -523,6 +523,11 @@ def _record_commit_attempt(
scope_model = _req("scope_model")
triad_raw_results = _req("triad_raw_results", None)
scope_raw_result = _req("scope_raw_result", None)
# Ordinary advisory continuation is not an author finish. Only an
# explicit caller-supplied record is persisted here; the review
# findings and advisory override remain the evidence for an unmarked
# successful commit.
author_disposition = _req("author_disposition", None)
block_class = _req("block_class")
rebuttal_sha256 = _req("rebuttal_sha256")
paid = _req("paid", False)
@ -612,6 +617,19 @@ def _record_commit_attempt(
attempt=attempt_no,
)
from ouroboros.review_records import validate_author_disposition
from ouroboros.config import get_review_enforcement
author_record = getattr(existing, "author_disposition", {}) or {}
if author_disposition is not None:
subject = pre_review_fingerprint or str(getattr(existing, "pre_review_fingerprint", "") or "")
author_record = validate_author_disposition(author_disposition, subject_hash=subject) or {}
if (not subject or not getattr(existing, "paid", False)
or subject != getattr(existing, "pre_review_fingerprint", "")
or (post_review_fingerprint and post_review_fingerprint != subject)
or get_review_enforcement() != "advisory"
or author_record.get("enforcement") != "advisory"):
author_record = {}
attempt = CommitAttemptRecord(
ts=_utc_now(),
commit_message=commit_message, # full message; durable evidence
@ -671,6 +689,7 @@ def _record_commit_attempt(
if scope_raw_result is not None
else getattr(existing, "scope_raw_result", None) or {}
),
author_disposition=author_record,
block_class=block_class or str(getattr(existing, "block_class", "") or ""),
rebuttal_sha256=rebuttal_sha256 or str(getattr(existing, "rebuttal_sha256", "") or ""),
paid=bool(paid or getattr(existing, "paid", False)),

View file

@ -261,8 +261,8 @@ def get_tools() -> List[ToolEntry]:
"inherit it), and you verify their combined files with integrate_subagent_patch. Use genesis only when EACH child "
"should own its OWN standalone durable repo (e.g. best-of-N separate builds). "
"Harness-delegated work uses a private snapshot; integrate_delegated_patch handles that separate patch. "
"Mutative children still cannot commit, run "
"review/runtime/skills lifecycle, enable tools, or write cognitive memory. Nested delegation "
"Mutative children cannot commit, enable tools or write cognitive memory. Cyber-effective "
"children inherit selected review, skill and runtime tools; explicit task restrictions remain. Nested delegation "
"is allowed within configured depth/cap limits — use delegation_intent / may_mutate / "
"may_fan_out to tell a child to recurse further, so a 'maximum subagents / grandchildren' "
"request propagates structurally instead of collapsing into one flat layer. "
@ -350,7 +350,7 @@ def get_tools() -> List[ToolEntry]:
}, _update_identity),
ToolEntry("toggle_evolution", {
"name": "toggle_evolution",
"description": "Enable or disable evolution mode. When enabled, Ouroboros runs continuous self-improvement cycles. Enabling requires runtime_mode 'advanced' or 'pro'; it is refused in 'light' mode.",
"description": "Enable or disable evolution mode. When enabled, Ouroboros runs continuous self-improvement cycles. Enabling requires runtime_mode 'advanced', 'pro', or 'cyber_pro'; it is refused in 'light' mode.",
"parameters": {"type": "object", "properties": {
"enabled": {"type": "boolean", "description": "true to enable, false to disable"},
"objective": {"type": "string", "default": "", "description": "Optional Evolution Campaign objective when enabling."},

View file

@ -23,6 +23,7 @@ log = logging.getLogger(__name__)
from pathlib import Path
from ouroboros.config import runtime_setting
def _evolution_restart_block_reason(ctx: ToolContext) -> str:
@ -361,15 +362,14 @@ def _switch_model(ctx: ToolContext, model: str = "", effort: str = "") -> str:
if model not in available:
return _publish_tool_result(ctx, ToolResult(status="error", code="TOOL_ARG_ERROR", text=(f"⚠️ Unknown model: {model}. Available: {', '.join(available)}")))
import os
use_local = False
if model == os.environ.get("OUROBOROS_MODEL") and os.environ.get("USE_LOCAL_MAIN", "").lower() in ("true", "1"):
if model == runtime_setting("OUROBOROS_MODEL") and runtime_setting("USE_LOCAL_MAIN", "").lower() in ("true", "1"):
use_local = True
elif model == os.environ.get("OUROBOROS_MODEL_LIGHT") and os.environ.get("USE_LOCAL_LIGHT", "").lower() in ("true", "1"):
elif model == runtime_setting("OUROBOROS_MODEL_LIGHT") and runtime_setting("USE_LOCAL_LIGHT", "").lower() in ("true", "1"):
use_local = True
else:
from ouroboros.config import get_fallback_models
if model in get_fallback_models() and os.environ.get("USE_LOCAL_FALLBACK", "").lower() in ("true", "1"):
if model in get_fallback_models() and runtime_setting("USE_LOCAL_FALLBACK", "").lower() in ("true", "1"):
use_local = True
ctx.active_model_override = model

View file

@ -59,6 +59,7 @@ from ouroboros.tools.control_subagent_spec import (
from ouroboros.tools.registry import ToolContext, active_repo_dir_for, system_repo_dir_for
from ouroboros.utils import append_jsonl, utc_now_iso
from ouroboros.tools.tool_result import ToolResult, _publish_tool_result
from ouroboros.config import runtime_settings
def _publish_scheduling_refusal(ctx: Any, status: str, code: str, text: str) -> str:
@ -610,7 +611,7 @@ def _schedule_task(ctx: ToolContext, internal: Dict[str, Any] | None = None, /,
may_mutate = fields["may_mutate"]
try:
configured_subagent, legacy_selection = select_subagent_snapshot(
effective_runtime_subagent_settings(_ctl().load_settings()),
effective_runtime_subagent_settings(runtime_settings(settings_reader=_ctl().load_settings)),
subagent_id=str(params.get("subagent_id") or ""),
legacy_model_lane=params.get("model_lane"),
legacy_executor=params.get("executor"),

View file

@ -52,6 +52,7 @@ from ouroboros.contracts.skill_payload_policy import (
from ouroboros.tools.core_file_tools import ( # noqa: F401
_ListingFailure,
_MEMORY_AT_DRIVE_MEMORY,
_raw_owner_secret_access_allowed,
_SKILL_OWNER_STATE_FILENAMES,
_SUBAGENT_SECRET_FILE_NAMES,
_access_or_block,
@ -968,6 +969,8 @@ def _code_search(ctx: ToolContext, query: str, path: str = ".",
# fallback. Names/paths stay; values become ***.
if normalized != "user_files" and not subagent_readonly:
return result_text
if normalized == "user_files" and _raw_owner_secret_access_allowed(ctx):
return result_text
masked_text, masked = mask_secret_bytes(
result_text, mask_opaque=normalized not in {"active_workspace", "system_repo"},
)

View file

@ -50,6 +50,19 @@ log = logging.getLogger(__name__)
_SKILL_OWNER_STATE_FILENAMES = SKILL_OWNER_STATE_FILENAMES
def _raw_owner_secret_access_allowed(ctx: ToolContext) -> bool:
"""Cyber Pro owner mode may inspect explicitly selected home-file bytes."""
if is_restricted_subagent_profile(ctx):
return False
try:
from ouroboros.config import get_runtime_mode
from ouroboros.runtime_mode_policy import runtime_mode_at_least
return runtime_mode_at_least(get_runtime_mode(), "cyber_pro")
except Exception:
return False
def _direct_resource_binding(
ctx: ToolContext,
supplied: Any,
@ -501,9 +514,9 @@ def _profile_roots_hint(ctx: ToolContext, operation: str) -> str:
model turns a dead-end error into a self-correcting retry instead of a
probe loop over blocked roots (v6.70.0)."""
try:
from ouroboros.tool_access import _POLICY
from ouroboros.tool_access import _POLICY, _effective_policy_profile
policy = _POLICY.get(active_tool_profile(ctx), {})
policy = _POLICY.get(_effective_policy_profile(active_tool_profile(ctx)), {})
visible = sorted(root for root, ops in policy.items() if operation in ops)
return f" Roots your profile can {operation}: {', '.join(visible) or '(none)'}."
except Exception:
@ -699,10 +712,13 @@ def _read_file(
))
try:
content = read_text(target)
raw_owner_secret_access = _raw_owner_secret_access_allowed(ctx)
rendered = _render_line_slice(_root_display_path(normalized, path), content,
max_lines=max_lines, start_line=start_line, start_char=start_char,
extent=extent, mask_secrets=is_restricted_subagent_profile(ctx))
if normalized == "user_files":
extent=extent, mask_secrets=(
is_restricted_subagent_profile(ctx) and not raw_owner_secret_access
))
if normalized == "user_files" and not raw_owner_secret_access:
# Egress seam for owner-home reads (#447 X1/В23): the file may be
# read, but raw credential bytes never enter model context/history —
# the masked form (***) may. Masking happens on the rendered slice;

Some files were not shown because too many files have changed in this diff Show more