Consciousness: the light per-task cap binds in every install mode

A capped tree (an Act/Observe consciousness tree) is refused a self_worktree child and a
system-repo patch integration regardless of the install's runtime mode or the owner's
mutative-subagent toggle: the cap is the level's, not the install's. The round-4 gate had
excluded installs already running light, where an explicit toggle admits self_worktree
children (astra scope, round 6).

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-09-16 14:56:43 +03:00
parent 56198be6bb
commit 5dd6737f3f
5 changed files with 61 additions and 11 deletions

View file

@ -161,7 +161,7 @@ The existing uploads and skill-output roots resolve through the task's canonical
Background Consciousness is Ouroboros taking a turn in its own Main chat when nobody wrote to it. There is no second mode: a wake-up is an ORDINARY Main direct turn (`_is_direct_chat`, the same ingress the owner's message uses) with the same system prompt, the same context builder, the same tool loop, the same chat log, the same activity block and the same usage ledger. Only three things are new — an alarm clock (`consciousness.py`, no thread: the supervisor pass calls `tick(now)` and launches the turn through `supervisor.workers.handle_wake_direct`), a wake-up MESSAGE that stands where the owner's words would be (`prompts/CONSCIOUSNESS.md` rendered by `consciousness_wake.py`), and the three owner settings beside the existing interval bounds. Structurally there is no longer a way for consciousness to count its context, its money or its tools differently from Main. Awareness of the body's own version stays ambient: the Runtime context of every task and every wake-up carries `official_update` — running version versus the official target as of the last fetching check, plus the update letter (`update_letter.py`); no wake is forced by a check, and whether to mention an update to the owner is the mind's judgment.
Three facts distinguish a wake-up from an owner's turn, and all three ride its metadata. Its ORIGIN is `metadata.initiator == "consciousness"` — inherited by everything the wake starts, which is what labels its block, its Logs rows, its final bubble and its task cards, and what makes it speak to other tasks as a task (`[Message from independent task <id>]`), never as the owner. Its AUTHORITY is the owner's autonomy level (`consciousness_authority.py`): Observe thinks, keeps memory and knowledge and writes to the owner but starts nothing; Act (the default) is everything the runtime mode allows except editing Ouroboros's own code and prompts, evolution, restart and settings; Full adds evolution. A level has exactly two consequences — an exception list in `task_contract.disabled_tools` and a per-task `runtime_mode_cap` of `light` below Full — and for a consciousness-origin task that list binds at DISPATCH ONLY, so the wake's tool schemas and cached prompt prefix stay byte-identical to an owner turn's while a withheld call is refused when it is made. The stricter of the two binds at the repo-mutation, protected-path, `start_service` and shell-write gates, at the acting-child write-surface selection and subagent patch integration, and in a started task's Runtime block, so the owner's level holds even on a Cyber Pro install. Its MONEY is one rolling 24-hour allowance covering the wake-ups and every root tree they started, read off the usage ledger (`consciousness_allowance.py`) by the alarm before it wakes and by the ONE admission door (`supervisor/queue.py::enqueue_task`) before any work it asks for starts — there is no separate "consciousness task" type, only the existing tools going through the existing door. A Presence cycle a wake starts (`initiate_presence`, Act and above) is an independent root outside this allowance. What is left of that allowance (capped by the owner's per-task cap) is the wake tree's graceful ceiling (`root_cost_ceiling_usd`, honored for the root itself and inherited by its members): the in-task stop lands the wake a planning margin before it, the ledger fence stays at the owner's per-task cap so a wake never dies before its first call, and a remainder at or below the planning margin is treated by the alarm as exhausted.
Three facts distinguish a wake-up from an owner's turn, and all three ride its metadata. Its ORIGIN is `metadata.initiator == "consciousness"` — inherited by everything the wake starts, which is what labels its block, its Logs rows, its final bubble and its task cards, and what makes it speak to other tasks as a task (`[Message from independent task <id>]`), never as the owner. Its AUTHORITY is the owner's autonomy level (`consciousness_authority.py`): Observe thinks, keeps memory and knowledge and writes to the owner but starts nothing; Act (the default) is everything the runtime mode allows except editing Ouroboros's own code and prompts, evolution, restart and settings; Full adds evolution. A level has exactly two consequences — an exception list in `task_contract.disabled_tools` and a per-task `runtime_mode_cap` of `light` below Full — and for a consciousness-origin task that list binds at DISPATCH ONLY, so the wake's tool schemas and cached prompt prefix stay byte-identical to an owner turn's while a withheld call is refused when it is made. The stricter of the two binds at the repo-mutation, protected-path, `start_service` and shell-write gates, at the acting-child write-surface selection and subagent patch integration, and in a started task's Runtime block, so the owner's level holds even on a Cyber Pro install; a capped tree is refused a self_worktree child and a system-repo patch integration in every install mode, the mutative-subagent toggle included. Its MONEY is one rolling 24-hour allowance covering the wake-ups and every root tree they started, read off the usage ledger (`consciousness_allowance.py`) by the alarm before it wakes and by the ONE admission door (`supervisor/queue.py::enqueue_task`) before any work it asks for starts — there is no separate "consciousness task" type, only the existing tools going through the existing door. A Presence cycle a wake starts (`initiate_presence`, Act and above) is an independent root outside this allowance. What is left of that allowance (capped by the owner's per-task cap) is the wake tree's graceful ceiling (`root_cost_ceiling_usd`, honored for the root itself and inherited by its members): the in-task stop lands the wake a planning margin before it, the ledger fence stays at the owner's per-task cap so a wake never dies before its first call, and a remainder at or below the planning margin is treated by the alarm as exhausted.
An active campaign owns an explicit objective, campaign id, transaction, and task claim; `evolution_mode_enabled` is only its scheduling projection. Dispatch, review, commit, publication, and restart revalidate that exact authority; a restored row without a live uncommitted claim is cancelled, and a reviewed commit binds to the claim by exact SHA before publication. If authority changes after commit, the commit moves to a private inspection ref and any attempt-created tag leaves the normal namespace; concurrent index/worktree edits are not reset. Restart verification and boot reconciliation decide whether a cycle is absorbed, abandoned, or still pending, and exact terminal replay resumes only incomplete effects without double-counting. The exact restart-verify claim (`state/pending_restart_verify.json`; one writer helper serves both the supervisor's evolution restart and the agent's `restart` tool) is written whether or not the supervisor restarts automatically — `OUROBOROS_EVOLUTION_AUTO_RESTART` off skips only the restart — so the owner's manual restart verifies the cycle by exact claim rather than by the weaker markerless reconcile.

View file

@ -128,6 +128,15 @@ def task_disabled_tools(task: Mapping[str, Any]) -> frozenset[str]:
return frozenset(names)
def task_mode_capped_light(task_metadata: Any) -> bool:
"""Whether a task carries the light per-task cap (an Act/Observe consciousness tree). The
cap is the LEVEL's, not the install's: such a tree may write, but never into its own
repository — a self_worktree child or a system-repo patch integration is refused in every
install mode, the mutative-subagent toggle included (В21=A, PLAN §5.4)."""
metadata = task_metadata if isinstance(task_metadata, Mapping) else {}
return str(metadata.get("runtime_mode_cap") or "").strip().lower() == "light"
def effective_runtime_mode(install_mode: str, task_metadata: Any) -> str:
"""The stricter of the install's runtime mode and the task's ``runtime_mode_cap``.

View file

@ -354,18 +354,22 @@ def _build_acting_constraint(
"⚠️ TOOL_ARG_ERROR (schedule_subagent): write_surface must be one of "
f"{allowed} (or omit it for a read-only subagent)."
)
from ouroboros.config import get_runtime_mode
from ouroboros.consciousness_authority import effective_runtime_mode
from ouroboros.consciousness_authority import task_mode_capped_light
# A per-task mode cap (a consciousness Act/Observe tree: light) keeps a self_worktree
# child off even where the install mode or the owner's toggle would allow one — the
# tree may write, but never into its own repository, its children included (В21=A).
capped_off_self = (
write_surface == "self_worktree"
and effective_runtime_mode(get_runtime_mode(), getattr(ctx, "task_metadata", None)) == "light"
and str(get_runtime_mode() or "").lower() != "light"
)
if not get_allow_mutative_subagents(write_surface) or capped_off_self:
# child off in EVERY install mode and toggle state — the tree may write, but never into
# its own repository, its children included (В21=A).
if write_surface == "self_worktree" and task_mode_capped_light(getattr(ctx, "task_metadata", None)):
return _publish_tool_result(ctx, ToolResult(
status="blocked", code="ACCESS_BLOCKED",
text=(
"⚠️ MUTATIVE_SUBAGENTS_DISABLED: this task's tree runs under a light cap (a "
"consciousness Act/Observe tree), so a self_worktree child (a checkout of the live "
"body) is never admitted for it — in any runtime mode, whatever the owner's toggle. "
"Schedule a read-only subagent (omit write_surface) or use an external surface."
),
))
if not get_allow_mutative_subagents(write_surface):
return _publish_tool_result(ctx, ToolResult(
status="blocked", code="ACCESS_BLOCKED",
text=(

View file

@ -208,6 +208,22 @@ def _verify_shared_external_workspace(
return True, [], ""
def _capped_self_repo_refusal(ctx: Any, child_task_id: str) -> str:
"""A tree under the light per-task cap (a consciousness Act/Observe tree) may not land a
patch on the Ouroboros repository — protected paths or not, in every install mode. The
empty string when the task is not capped."""
from ouroboros.consciousness_authority import task_mode_capped_light
if not task_mode_capped_light(getattr(ctx, "task_metadata", None)):
return ""
return (
f"⚠️ INTEGRATE_CAPPED_TREE: child {child_task_id} produced a self_worktree patch (against "
"the Ouroboros system repo), but this task's tree runs under a light cap (a consciousness "
"Act/Observe tree): it may not land a patch on the Ouroboros repository, protected paths "
"or not, in any runtime mode."
)
def _integration_runtime_mode(ctx: Any) -> str:
"""The mode the protected-path gate of an integration reads: the stricter of the install
mode and the task's own cap (a consciousness Act/Observe tree is light), so a capped task
@ -813,6 +829,9 @@ def _integrate_subagent_patch(
# own workspace IS a self_worktree checkout stays legitimate top-only
# routing and is not touched by this guard.
if child_surface == "self_worktree":
capped = _capped_self_repo_refusal(ctx, child_task_id)
if capped:
return capped
parent_ws_mode = str(getattr(ctx, "workspace_mode", "") or "").strip().lower()
# Fire STRUCTURALLY whenever the parent's active root is a non-system
# workspace (is_workspace_mode()), so an unrecognized external spelling

View file

@ -97,3 +97,21 @@ def test_a_capped_tree_cannot_schedule_a_self_worktree_child(monkeypatch):
full = types.SimpleNamespace(task_metadata=dict(_wake_task("full")["metadata"]))
assert isinstance(_build_acting_constraint(write_surface="self_worktree", write_root="", protected_paths_grant=False,
external_tool_grants=None, parent_workspace_root="", ctx=full), dict)
# A Light install whose owner explicitly enabled mutative subagents admits self_worktree children —
# but never for a capped tree: the cap is the level's, not the install's (astra scope round 6).
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "light")
monkeypatch.setenv("OUROBOROS_ALLOW_MUTATIVE_SUBAGENTS", "true")
refused = _build_acting_constraint(write_surface="self_worktree", write_root="", protected_paths_grant=False,
external_tool_grants=None, parent_workspace_root="", ctx=capped)
assert "light cap" in str(getattr(refused, "text", refused))
assert isinstance(_build_acting_constraint(write_surface="self_worktree", write_root="", protected_paths_grant=False,
external_tool_grants=None, parent_workspace_root="", ctx=full), dict)
def test_a_capped_tree_may_not_land_a_system_repo_patch_in_any_mode():
from ouroboros.tools.subagent_integration import _capped_self_repo_refusal
capped = types.SimpleNamespace(task_metadata=dict(_wake_task("act")["metadata"]))
assert "INTEGRATE_CAPPED_TREE" in _capped_self_repo_refusal(capped, "child-1")
assert _capped_self_repo_refusal(types.SimpleNamespace(task_metadata=dict(_wake_task("full")["metadata"])), "c") == ""
assert _capped_self_repo_refusal(types.SimpleNamespace(task_metadata={}), "c") == ""