v7next F1: domain D04 - registry and tool_access split, proof-green; core/typed-result organs hot-deferred

Module side: 4 of 8 D04 owners are byte-identical to the reference and merge
base (protected_artifacts, tool_policy, tools/__init__, tool_discovery), one
is pure upstream drift (tool_capabilities - upstream bytes stand), one keeps
upstream bytes because its reference delta is the typed-dispatch cutover
(extension_dispatch, rows 187/188 deferred). The two splits land from TIP
bytes with the transplant tool's triple proof green on every symbol
(ast=tokens=bytes, leaf_invariants=[], exit 0):

- tools/registry.py 3960 -> 2686 (PROTECTED file: pure byte-preserving span
  relocation + one re-export block + noqa on historical imports - proven by
  line-accounting audit): tool_context (2 symbols), tool_catalog (1),
  tool_resolution (28), registry_guards (16), registry_guard_process (27).
  13 spans were byte-falsified as oracle copy sources by pure upstream drift
  and re-emitted from tip bytes; 5 rows whose reference destination carries
  typed-result semantics moved their TIP bodies verbatim (typed deltas ride
  with F2). HOT-DEFERRED with evidence: registry_core.py (ToolRegistry is a
  2252-line class; the reference slimmed it via 17 method->function
  extractions, which are not byte-preserving), tool_result.py (32/33 symbols
  are the D02 typed organ, absent at tip).
- tool_access.py 1591 -> 782: tool_access_types (14), tool_access_paths (10),
  tool_access_roots (9), tool_access_user_files (8); 39/41 spans byte-equal
  to the reference, _skill_payload_base and ResolvedResourceBinding
  re-emitted from tip (upstream refactor/field would have been reverted by an
  oracle copy). The D1 mirror-path defect travels UNFIXED per lane orders.

Protection closure (protective-only, mirrors the reference and the tree's own
LC2 parity rule): the five landed registry leaves join SAFETY_CRITICAL_PATHS
and HOT_CODE_PATHS - code that moved out of the protected, hot registry keeps
both labels; pinned by the new parity test.

Test side: tests/test_tool_capabilities.py 1991 -> 681 splits into 4 siblings
per ledger rows 784-825 from tip bytes (34/42 spans oracle-equal, 8 re-emitted
from tip); lossless 61==61 test functions, tree-wide AST dup scan clean.
Pins carried with disclosed adaptations: test_tool_owner_facades.py (+alias_for
row), test_tool_access_extraction.py (4 adaptations in docstring),
tool_resolution identity test appended to test_workspace_authority_binding.py
(typed companion deliberately not carried).

size-ratchet manifest regenerated with the official tool (test_tool_capabilities
leaves GIANT_PATHS; no new band entries); ratchet lane 5 passed; ruff F clean;
90+518+586+257 tests green in isolation at the lane base; HEAD held through
every pytest run. Ledger corrections: docs/v7next/LEDGER_CORRECTIONS.md D04
section, entries 1-11.

(cherry picked from commit 2321514369b6f003e92bcd84e6a9a7efda6857df)
This commit is contained in:
Ouroboros 2026-08-30 18:27:42 +00:00
parent ec2cc3d188
commit 22c1473117
23 changed files with 4646 additions and 3553 deletions

View file

@ -255,3 +255,105 @@ with evidence, found lane by lane. Applied to the campaign's carried ledger at F
resolved unilaterally); candidate row for the carried ledger at F5.
Disjoint upstream drift a23e12b1 (push_to_remote test retargeted to
`_git_network_bounded`) stands.
## From the D04 lane (base d830cdba, 2026-08-30)
1. Registry-split rows RE-PROVEN against tip bytes for the four landed tools/
leaves (tool_context, tool_catalog, tool_resolution, registry_guards,
registry_guard_process — 74 symbols): 61 spans byte-identical between the
reference leaves and `git show HEAD:ouroboros/tools/registry.py`; 13 spans
BYTE-FALSIFIED as copy sources by PURE UPSTREAM DRIFT (oracle==merge-base,
tip moved): _prepare_public_builtin_args, _executor_backend_candidate_allowed,
_authorized_managed_update_resolver (404B -> 1843B hardening), _disabled_tools,
_detect_runtime_mode_elevation, _SUBAGENT_SHELL_SECRET_MARKERS,
_detect_mutative_toggle_self_change, _detect_evolution_owner_control_self_change,
_detect_context_mode_self_lowering, _DENIED_READ_OPTIONS,
_is_pure_read_inspection, _detect_safety_mode_self_lowering,
_detect_owner_skill_attest_self_call. All re-emitted from tip bytes,
transplant proof green (ast=tokens=bytes on every symbol, exit 0).
2. Rows whose reference destination carries the TYPED-RESULT cutover semantics
(PURE V7 DELTA; tip==merge-base): 144 (_normalize_dispatch_path_args reduced
to a projection), 184 (_binding_error_text native codes), 185
(_payload_dispatch_constraint typed second element), 226
(_managed_update_code_tool_block thin wrapper), 138 (ToolEntry shallow-frozen
— also upstream-drifted: tip added the alias_for field). This lane moved the
TIP bodies verbatim; the typed deltas are deliberately NOT ported — they ride
with the F2 typed-result organ, not with a byte-preserving relocation of a
protected file.
3. HOT-DEFERRED: ouroboros/tools/registry_core.py (rows 156, 167, 170, 171,
174, 175). Evidence: tip ToolRegistry is a 2252-line class (probe: tip span
124364B vs reference 49860B, ast_equal=False); the reference slimmed it via
17 method->function extractions (rows 189, 224, 225, 230, 235-242, 287,
291-293) which change the receiver (self -> registry) and are NOT
byte-preserving relocation — out of bounds for the protected
tools/registry.py under this lane's mandate. ToolRegistry and the four
process/mutation constants stay in the facade; the class also would put the
new leaf straight into the >1500 band. Re-split from the upstream form in F2.
4. HOT-DEFERRED: ouroboros/tools/tool_result.py. 32 of the reference leaf's 33
top-level symbols do not exist at tip (the ToolResult/ToolCodeSpec organ,
D02-family approved deltas); the single registry-sourced verbatim row 139
(_compose_execute_result) also drifted at tip (661B vs 671B). Creating a
one-symbol leaf under the organ's name would falsely anchor the F2 re-split;
_compose_execute_result stays in the facade.
5. HOT-DEFERRED: rows 187/188 (ToolRegistry._dispatch_mcp_tool /
_dispatch_extension_tool -> extension_dispatch typed dispatchers).
tip tools/extension_dispatch.py == merge-base (116 lines); the reference's
+177 lines are the producer-boundary ToolResult typing plus method
retirement. Upstream bytes stand; the methods stay on the class.
6. loop_tool_execution.py D04 rows (157, 159-164, 826-828) are ALL
retire/rename/type rows of the classifier cutover — nothing is emittable as
a byte-preserving span. Shared-monolith convention honored: this lane did
not touch ouroboros/loop_tool_execution.py at all (D01 owns the rest).
7. tools/core.py shared-leaf note (row 353, core.py::active_repo_dir_for ->
tool_resolution.py): already satisfied at tip by an import alias
(core.py:20 imports it from the registry; the registry facade now re-exports
it from tool_resolution — same object). core.py untouched by this lane.
8. tool_access split rows 495-535 RE-PROVEN against tip bytes: 39/41 spans
byte-identical; 2 BYTE-FALSIFIED as copy sources by PURE UPSTREAM DRIFT:
_skill_payload_base (upstream re-homed the body into
skill_payload_binding.resolve_skill_payload_base — copying the reference
leaf would have reverted that refactor) and ResolvedResourceBinding
(upstream added the logical_base_path field). Both re-emitted from tip
bytes, proof green. The D1 mirror-path defect (safe_relpath lstrip('/'),
lying "caller rejects" docstrings) travels in the moved tip bytes UNFIXED,
per the lane instruction — it remains an upstream issue-candidate.
9. Pins carried with disclosed adaptations (identity continuations to tip
bytes): tests/test_tool_owner_facades.py (+ the alias_for row in the
ToolEntry contract — upstream drift); tests/test_tool_access_extraction.py
(4 adaptations, listed in its docstring: tool_module_inventory clause
dropped until that leaf lands, backedge check narrowed to import-time
imports because the D18/D33 call-time handle is deliberate, one-matrix
clause asserts through the facade re-export, size bounds kept);
tests/test_workspace_authority_binding.py gains the reference's
tool_resolution identity test while its typed companion
(_normalize_dispatch_path_args_result) is NOT carried — it pins deferred
machinery. test_registry_core.py, test_tool_result*.py and the
classification-differential suites are NOT carried for the same reason.
10. Test-split rows 784-825 (tests/test_tool_capabilities.py -> 4 siblings)
RE-PROVEN against tip bytes: 34/42 moved spans byte-identical to the
reference siblings, 8 re-emitted from tip (test_search_code_has_result_limit,
test_local_readonly_subagent_execute_blocks_forbidden_tools,
test_local_readonly_subagent_initial_schemas_are_allowlisted,
test_schedule_subagent_in_initial_schemas,
test_schedule_subagent_inherits_workspace_executor_ref, and the three
test_schedule_subagent_required_*_for_readonly tests). Lossless: 61 == 61
test functions, zero lost, zero added, no duplicate names introduced
(tree-wide AST dup scan; the 10 pre-existing identical-body duplicates
between test_review_cycles_dispatch.py and test_review_cycles_skill_dispatch.py
plus the test_tool_registered same-name pair predate this lane — D06/D05
territory, reported not touched). 21 unrowed/kept tip tests remain in the
remainder; 3 header imports that lost their last reader were dropped there.
11. Protection-surface closure (code-side, protective-only): the reference
extends ouroboros/runtime_mode_policy.py::SAFETY_CRITICAL_PATHS and
supervisor/update_merge_policy.py::HOT_CODE_PATHS over the registry split
leaves — without that, guard bodies moved out of the protected registry
become writable in advanced mode and lose the hot-code label (this tree's
own parity rule, tests/test_lc2_owner_facades.py, pins the inverse
direction). This lane mirrored the closure for the five leaves that exist
here (registry_core.py / tool_result.py rows return with their leaves) and
pinned it (tests/test_tool_owner_facades.py::
test_registry_split_leaves_keep_protected_label_parity). NOT mirrored —
for the owner/F5: the reference's prose updates to prompts/SAFETY.md:10
and prompts/SYSTEM.md "Immutable Safety Files" (operator-off-limits
runtime prompts; enforcement is code-side, prose enumerates only the
facade for now), and the reference's extra HOT_CODE_PATHS row for
ouroboros/tools/extension_dispatch.py (nothing moved there on this tree —
adding it is an oracle delta beyond relocation parity).

View file

@ -19,6 +19,15 @@ SAFETY_CRITICAL_PATHS = frozenset({
"ouroboros/runtime_mode_policy.py",
"ouroboros/tools/extension_dispatch.py",
"ouroboros/tools/registry.py",
# The v7 D04 split moved guard/resolution bodies out of the protected
# registry without moving any of the risk, so every inventory that
# protects the parent must cover the leaves (label parity — same rule as
# the git_ops family).
"ouroboros/tools/registry_guard_process.py",
"ouroboros/tools/registry_guards.py",
"ouroboros/tools/tool_catalog.py",
"ouroboros/tools/tool_context.py",
"ouroboros/tools/tool_resolution.py",
"prompts/SAFETY.md",
})

View file

@ -42,7 +42,6 @@ GIANT_PATHS = (
"tests/test_skill_loader.py",
"tests/test_skill_review.py",
"tests/test_task_status_flow.py",
"tests/test_tool_capabilities.py",
"tests/test_ui_smoke_playwright.py",
"web/modules/chat.js",
"web/tests/harness_accounts.test.js",

View file

@ -8,16 +8,16 @@ migrated to neutral tool names.
from __future__ import annotations
import os
import os # noqa: F401 — historical facade surface
import pathlib
import re
from dataclasses import dataclass
from typing import Any, Iterable, Literal, Optional
import re # noqa: F401 — historical facade surface
from dataclasses import dataclass # noqa: F401 — historical facade surface
from typing import Any, Iterable, Literal, Optional # noqa: F401 — historical facade surface
from ouroboros.artifacts import (delegated_capture_read_target,
task_artifact_dir_path, task_id_for_artifacts)
from ouroboros.tool_capabilities import ACTING_SUBAGENT_MODE, LOCAL_READONLY_SUBAGENT_MODE
from ouroboros.contracts.task_constraint import VALID_WRITE_SURFACES, normalize_task_constraint
from ouroboros.tool_capabilities import ACTING_SUBAGENT_MODE, LOCAL_READONLY_SUBAGENT_MODE # noqa: F401 — historical facade surface
from ouroboros.contracts.task_constraint import VALID_WRITE_SURFACES, normalize_task_constraint # noqa: F401 — historical facade surface
from ouroboros import deliverables_paths as _deliverables_paths
from ouroboros.shell_parse import is_absolute_path_text
from ouroboros.utils import safe_relpath
@ -26,345 +26,72 @@ _deliverables_root_lexical = _deliverables_paths._deliverables_root_lexical
_deliverables_root_lexical_alias = _deliverables_paths._deliverables_root_lexical_alias
_lexical_path_is_relative_to_casefold = _deliverables_paths._lexical_path_is_relative_to_casefold
# v7 D04 split: the owners below were extracted VERBATIM from this module
# (see each leaf's header); re-exported here so historical imports and
# monkeypatch targets keep working unchanged.
from ouroboros.tool_access_types import ( # noqa: F401 — re-exported moved surface
Operation,
ResolvedResourceBinding,
ResourceRoot,
SUBAGENT_CAPABILITIES,
SubagentCapability,
ToolAccessDecision,
ToolProfile,
_ALL_ROOTS,
_POLICY,
_READONLY_RESOURCE_ROOTS,
_READ_OPS,
_SUBAGENT_CAPABILITY_TO_OPERATION,
_TOP_LEVEL_PRINCIPAL_POLICY,
_TOP_LEVEL_PRINCIPAL_PROFILES,
)
from ouroboros.tool_access_paths import ( # noqa: F401 — re-exported moved surface
_deliverables_root,
_path_is_relative_to_casefold,
_user_files_root,
canonical_data_root,
normalize_root,
normalize_root_relative,
normalize_runtime_data_path,
path_is_relative_to,
paths_overlap_casefold,
workspace_mode_block_reason,
)
from ouroboros.tool_access_roots import ( # noqa: F401 — re-exported moved surface
_is_subagent_ctx,
_skill_payload_base,
active_tool_profile,
binding_targets_system_repo,
is_external_workspace,
load_bound_skill,
predicted_subagent_profile,
project_room_lens_dir,
resource_root_path,
)
from ouroboros.tool_access_user_files import ( # noqa: F401 — re-exported moved surface
UserFilesPathBlockedError,
_USER_FILES_ALLOWED_DOTNAMES,
_USER_FILES_SECRET_COMPONENTS,
_USER_FILES_SECRET_NAMES,
_USER_FILES_SECRET_RE,
_subagent_projects_read_hint,
resolve_user_file_path,
user_files_path_block_reason,
)
def _user_files_root() -> pathlib.Path:
"""Filesystem base for the ``user_files`` resource root.
Defaults to the owner's real home. A jailed/benchmark runtime can redirect it
to a scratch directory via ``OUROBOROS_USER_FILES_ROOT`` so a task physically
cannot resolve the owner's real home (e.g. ``~/file1.txt`` secret files). Any
unusable value falls back to the real home — fail-safe, never broadens reach.
"""
raw = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
if raw:
try:
return pathlib.Path(raw).expanduser().resolve(strict=False)
except Exception:
# ANY unusable value (bad path, unknown ``~user`` RuntimeError, odd OS error)
# fails safe to the real home — the doc's "any unusable value" contract.
pass
return pathlib.Path.home().resolve(strict=False)
def _deliverables_root() -> pathlib.Path:
"""Container for UNNAMED user deliverables, JAIL-AWARE: when the user_files home is
redirected (``OUROBOROS_USER_FILES_ROOT``) and no explicit
``OUROBOROS_DELIVERABLES_ROOT`` is set, keep unnamed deliverables INSIDE the jail so a
bare ``write_file(root='user_files', path='answer.txt')`` stays reachable and in-bounds
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 get_deliverables_root
jail = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
explicit = (os.environ.get("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
if explicit:
return pathlib.Path(explicit).expanduser().resolve(strict=False)
if jail and not explicit:
return (_user_files_root() / "Deliverables").resolve(strict=False)
return pathlib.Path(get_deliverables_root()).expanduser().resolve(strict=False)
ToolProfile = Literal[
"self_modification",
"workspace_task",
"external_workspace_task",
"acting_subagent",
"skill_repair",
"local_readonly_subagent",
"operator_control",
]
ResourceRoot = Literal[
"active_workspace",
"system_repo",
"runtime_data",
"task_drive",
"skill_payload",
"artifact_store",
"user_files",
"subagent_projects",
"deliverables",
]
Operation = Literal[
"read",
"list",
"search",
"write",
"edit",
"shell",
"vcs",
"review",
"delegate",
"service",
]
SubagentCapability = Literal[
"write",
"edit",
"shell",
"vcs",
"review",
"delegate",
"service",
]
@dataclass(frozen=True)
class ToolAccessDecision:
allow: bool
reason: str = ""
guard: str = ""
@dataclass(frozen=True)
class ResolvedResourceBinding:
"""One dispatch-selected logical root and its exact physical target."""
profile: ToolProfile
root: ResourceRoot
operation: Operation
base_path: pathlib.Path
target_path: pathlib.Path
source: str
skill_name: str
state_drive_root: pathlib.Path
logical_base_path: pathlib.Path | None = None
_ALL_ROOTS: frozenset[str] = frozenset({
"active_workspace",
"system_repo",
"runtime_data",
"task_drive",
"skill_payload",
"artifact_store",
"user_files",
"subagent_projects",
"deliverables",
})
# Deferral 1: orchestrator-visible READ-ONLY roots — durable subagent (genesis) projects
# and the unnamed-deliverables container. Only ever granted {read,list,search}; NEVER
# write/edit/shell/vcs (no mutation, no shell-cwd — deliberately absent from
# resolve_shell_cwd candidates) and NEVER to acting/readonly subagents (a child must not
# read sibling projects). operator_control is capped to read-only on these too.
_READONLY_RESOURCE_ROOTS: frozenset[str] = frozenset({"subagent_projects", "deliverables"})
_TOP_LEVEL_PRINCIPAL_PROFILES: frozenset[str] = frozenset({
"workspace_task",
"external_workspace_task",
"self_modification",
})
_READ_OPS = frozenset({"read", "list", "search"})
_USER_FILES_SECRET_COMPONENTS = frozenset({
".aws",
".azure",
".config",
".docker",
".git", # v6.52.0: VCS internals hold config + stored credentials
".gnupg",
".hg",
".kube",
".local",
".netrc",
".ssh",
".svn",
"library",
})
_USER_FILES_SECRET_NAMES = frozenset({
".env",
# v6.52.0: credential / shell-init / history dotFILES kept blocked AFTER the bare
# `startswith('.')` block was dropped (so benign project dotdirs are readable while
# secret-bearing dotfiles are not).
".bash_history",
".bash_profile",
".bashrc",
".dockercfg",
".git-credentials",
".gitconfig",
".htpasswd",
".npmrc",
".pgpass",
".profile",
".pypirc",
".python_history",
".zsh_history",
".zprofile",
".zshrc",
"auth.json",
"credentials",
"credentials.json",
"secrets.json",
"settings.json",
"token.json",
"tokens.json",
})
_USER_FILES_SECRET_RE = re.compile(r"(?:^|[._-])(api[_-]?key|credential|password|secret|token)(?:[._-]|$)", re.I)
# v6.52.0 (P1): a SMALL allowlist of benign hidden (dot) project components. The dotfile guard
# is DEFAULT-DENY: a credential blocklist can never be exhaustive (e.g. ~/.terraform.d,
# ~/.cargo/credentials.toml, ~/.oci/config, ~/.pip/pip.conf, ~/.m2/settings.xml, ~/.*_history all
# leak under enumeration), so a dotted component is blocked UNLESS it is one of these known-safe
# project-config dirs/files. This serves the goal (read .github/.vscode/.idea project config)
# without opening the whole in-home dotfile space.
_USER_FILES_ALLOWED_DOTNAMES = frozenset({
".github",
".gitlab",
".circleci",
".devcontainer",
".vscode",
".idea",
".gitignore",
".gitattributes",
".gitmodules",
".dockerignore",
".editorconfig",
})
_TOP_LEVEL_PRINCIPAL_POLICY: dict[str, set[str]] = {
"active_workspace": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"system_repo": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"runtime_data": {"read", "list", "search", "write", "edit"},
"task_drive": {"read", "list", "write", "edit", "shell", "service"},
"skill_payload": {"read", "list", "search", "write", "edit", "review", "shell"},
"artifact_store": {"read", "list", "write", "shell", "service"},
"user_files": {"read", "list", "search", "write", "edit", "shell", "service"},
"subagent_projects": {"read", "list", "search"},
"deliverables": {"read", "list", "search"},
}
_POLICY: dict[str, dict[str, set[str]]] = {
"local_readonly_subagent": {
# Read-only child VCS names still need their target binding to resolve.
"active_workspace": set(_READ_OPS) | {"vcs"},
"system_repo": set(_READ_OPS) | {"vcs"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
# v6.70.0 (owner-approved): read-only scouts sent to review a skill were
# structurally blind to its payload — a scout literally reported
# "reviewing blind", and a correct "skill does not exist" answer was
# indistinguishable from an access block. Payloads are skill CODE
# (data/skills/...); grants/secrets live in data/state/skills, which
# stays invisible to this profile.
"skill_payload": {"read", "list", "search"},
},
"skill_repair": {
"skill_payload": {"read", "list", "search", "write", "edit", "review"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
},
# Top-level preset names remain observable, but workspace focus never narrows
# the ordinary principal. Independent path/credential/child/runtime guards
# still apply after this shared operation matrix.
"workspace_task": _TOP_LEVEL_PRINCIPAL_POLICY,
"external_workspace_task": _TOP_LEVEL_PRINCIPAL_POLICY,
# Mutative (acting) subagents write only inside their isolated active
# workspace (self_worktree / external_workspace / genesis). No vcs-commit /
# review here; the parent integrates and commits. self_worktree additionally
# keeps protected-path discipline active in the registry (it is the system
# repo). runtime_data stays read-only.
"acting_subagent": {
# Acting children write ONLY inside their isolated surface (active_workspace =
# the self_worktree / external_workspace / genesis). task_drive / artifact_store
# are read-only here (no extra write surface); the deliverable is a workspace.patch.
"active_workspace": {"read", "list", "search", "write", "edit", "shell", "vcs", "service"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
},
"self_modification": _TOP_LEVEL_PRINCIPAL_POLICY,
# operator_control gets full authority on every mutable root, but the orchestrator
# read-only roots stay read-only even here (they are deliverables/durable projects,
# not a control surface).
"operator_control": {
**{root: {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "delegate", "service"}
for root in _ALL_ROOTS if root not in _READONLY_RESOURCE_ROOTS},
**{root: {"read", "list", "search"} for root in _READONLY_RESOURCE_ROOTS},
},
}
_SUBAGENT_CAPABILITY_TO_OPERATION: dict[str, Operation] = {
"write": "write",
"edit": "edit",
"shell": "shell",
"vcs": "vcs",
"review": "review",
"delegate": "delegate",
"service": "service",
}
SUBAGENT_CAPABILITIES: tuple[str, ...] = tuple(_SUBAGENT_CAPABILITY_TO_OPERATION.keys())
def _is_subagent_ctx(ctx: Any) -> bool:
"""True when the task is a delegated subagent (by lineage metadata)."""
for attr in ("task_metadata", "task_contract"):
data = getattr(ctx, attr, None)
if isinstance(data, dict) and str(data.get("delegation_role") or "").strip() == "subagent":
return True
return False
def is_external_workspace(ctx: Any) -> bool:
"""True for an EXTERNAL-workspace top-level task (not the system repo).
External-workspace tasks operate on a pre-existing working tree somewhere on
the host (container scratch, a repo cloned under ``/tmp`` or ``/build``,
etc.). They legitimately read, run commands, and use git OUTSIDE the user
home, while the Ouroboros runtime (system repo + data drive) and
credential-like files stay protected by the per-path guards. ``self_worktree``
and ``genesis`` are acting-subagent SURFACES (``acting_subagent`` profile),
never this profile, so they keep full home/runtime confinement.
"""
try:
if not bool(getattr(ctx, "is_workspace_mode", lambda: False)()):
return False
except Exception:
return False
return str(getattr(ctx, "workspace_mode", "") or "").strip().lower() == "external"
def active_tool_profile(ctx: Any) -> ToolProfile:
constraint = normalize_task_constraint(getattr(ctx, "task_constraint", None))
mode = str(getattr(constraint, "mode", "") or "").strip()
if mode == LOCAL_READONLY_SUBAGENT_MODE:
return "local_readonly_subagent"
if mode == ACTING_SUBAGENT_MODE:
# Acting subagents require a resolved write surface; otherwise fail
# closed to read-only rather than inheriting a broader profile.
surface = str(getattr(constraint, "surface", "") or "").strip()
if surface in VALID_WRITE_SURFACES:
return "acting_subagent"
return "local_readonly_subagent"
if mode == "skill_repair":
return "skill_repair"
# Fail-closed floor (BIBLE P3), checked BEFORE workspace/direct-chat: a
# delegated subagent without a valid readonly/acting/skill constraint is
# read-only and must never inherit workspace_task / operator_control /
# self_modification. The parent remains the sole local writer/committer.
if _is_subagent_ctx(ctx):
return "local_readonly_subagent"
if bool(getattr(ctx, "is_workspace_mode", lambda: False)()):
# Keep distinct preset names for focus/path diagnostics. Both use the
# shared ordinary principal; external host-scratch reach is a path fact.
if is_external_workspace(ctx):
return "external_workspace_task"
return "workspace_task"
if bool(getattr(ctx, "is_direct_chat", False)):
return "operator_control"
return "self_modification"
def predicted_subagent_profile(*, write_surface: str = "") -> ToolProfile:
"""The tool profile a scheduled subagent will resolve to, from schedule-time
inputs only (v6.57.0, 1.6). A valid write_surface → acting_subagent; otherwise
a read-only subagent. Mirrors active_tool_profile's subagent branches so the
parent's schedule result and the child's start context can preview the profile
without a live ctx. NOT authoritative — the supervisor's _resolve_subagent_
constraint is the real gate; this is a visibility preview."""
surface = str(write_surface or "").strip()
if surface and surface in VALID_WRITE_SURFACES:
return "acting_subagent"
return "local_readonly_subagent"
def summarize_subagent_profile(profile: ToolProfile, *, effective_lane: str = "") -> str:
@ -516,27 +243,6 @@ def _side_effect_free_process_roots(
]
def project_room_lens_dir(ctx: Any) -> Optional[pathlib.Path]:
"""Return a direct-chat room's verified project cwd, otherwise ``None``.
Promoted/workspace/subagent tasks carry their own workspace; only a direct
chat without one may use the injected existing ``_project_room_dir``.
"""
if not bool(getattr(ctx, "is_direct_chat", False)):
return None
if getattr(ctx, "workspace_root", None):
return None
meta = getattr(ctx, "task_metadata", None)
raw = str(meta.get("_project_room_dir") or "").strip() if isinstance(meta, dict) else ""
if not raw:
return None
try:
candidate = pathlib.Path(raw).resolve(strict=False)
return candidate if candidate.is_dir() else None
except OSError:
return None
def filesystem_affordance_map(ctx: Any, *, runtime_mode: str = "") -> dict[str, Any]:
"""A compact, side-effect-free projection of filesystem/tool access affordances.
@ -662,86 +368,6 @@ def shell_cwd_block_message(ctx: Any, cwd: str = "", *, operation: Operation = "
)
def normalize_root(root: str | None, *, default: ResourceRoot = "active_workspace") -> ResourceRoot:
candidate = str(root or default).strip() or default
if candidate not in _ALL_ROOTS:
raise ValueError(f"unknown root {candidate!r}; expected one of {sorted(_ALL_ROOTS)}")
return candidate # type: ignore[return-value]
def path_is_relative_to(path: pathlib.Path, root: pathlib.Path) -> bool:
try:
pathlib.Path(path).resolve(strict=False).relative_to(pathlib.Path(root).resolve(strict=False))
return True
except (OSError, ValueError):
return False
def normalize_root_relative(root: pathlib.Path, path: str) -> str:
"""Map a model-supplied path to a root-relative string when it redundantly
encodes the root, so structural/read tools accept the paths an agent
naturally writes: an absolute path inside the active root (e.g. ``/app/foo``
under a workspace rooted at ``/app``) and a single redundant root-basename
prefix (``app/foo``). Returns a RELATIVE string only — it never widens
access: callers still apply ``safe_relpath`` + a ``relative_to`` confinement
check, so a genuine escape is still rejected downstream.
- absolute & inside root -> stripped to relative
- absolute & outside root -> returned unchanged (caller's check rejects it)
- redundant root-basename prefix, existence-guarded -> stripped
- otherwise -> unchanged
"""
text = str(path or "").strip().replace("\\", "/")
if not text or text in (".", "./"):
return text
try:
root_resolved = pathlib.Path(root).resolve(strict=False)
except (OSError, ValueError):
return text
# (A) absolute path that already points inside the root.
if is_absolute_path_text(text):
try:
return pathlib.Path(text).resolve(strict=False).relative_to(root_resolved).as_posix()
except (OSError, ValueError):
return text # outside root -> let the caller's confinement reject it
# (B) redundant root-basename prefix ('app' or 'app/x' when root basename is
# 'app'). Strip it UNLESS the root contains a real same-named subdir (then
# 'app/x' is ambiguously a genuine nested path and is kept). Gating on the
# absence of that subdir — not on the target existing — lets NEW write/create
# targets ('app/new.py' -> 'new.py') normalize too, while a real 'app/'
# subdir is never mis-stripped. Only ever shortens toward root (no escape).
base = root_resolved.name
if base and (text == base or text.startswith(base + "/")):
try:
if not (root_resolved / base).is_dir():
return text[len(base):].lstrip("/") or "."
except (ValueError, OSError):
# `..`/traversal or stat error: leave unchanged so the caller's
# confinement produces the canonical (not a generic) error.
return text
return text
def _path_is_relative_to_casefold(path: pathlib.Path, root: pathlib.Path) -> bool:
try:
path_parts = pathlib.Path(path).resolve(strict=False).parts
root_parts = pathlib.Path(root).resolve(strict=False).parts
except (OSError, ValueError):
return False
if len(path_parts) < len(root_parts):
return False
return tuple(part.casefold() for part in path_parts[: len(root_parts)]) == tuple(
part.casefold() for part in root_parts
)
def paths_overlap_casefold(left: pathlib.Path, right: pathlib.Path) -> bool:
"""Return True when two paths overlap under case-insensitive path semantics."""
return _path_is_relative_to_casefold(left, right) or _path_is_relative_to_casefold(right, left)
def light_cognitive_or_root_redirect(tool_name: str, args: dict[str, Any]) -> str | None:
"""Precise light-mode redirect for write attempts that should use a cognitive
tool or an explicit ``user_files`` root. Returns the message, or ``None``.
@ -804,313 +430,6 @@ def light_cognitive_or_root_redirect(tool_name: str, args: dict[str, Any]) -> st
return None
def workspace_mode_block_reason(ctx: Any) -> str:
mode = str(getattr(ctx, "workspace_mode", "") or "").strip()
workspace_root = getattr(ctx, "workspace_root", None)
if not mode or workspace_root is None:
return ""
try:
workspace = pathlib.Path(workspace_root).resolve(strict=False)
except (OSError, TypeError, ValueError):
return "workspace_root is invalid"
protected_values = (
("Ouroboros system repo", getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir", None)),
("Ouroboros repo", getattr(ctx, "repo_dir", None)),
("Ouroboros data drive", getattr(ctx, "drive_root", None)),
(
"Ouroboros parent data drive",
(getattr(ctx, "task_metadata", {}) or {}).get("budget_drive_root")
if isinstance(getattr(ctx, "task_metadata", {}), dict)
else "",
),
)
for label, value in protected_values:
if not value:
continue
try:
protected = pathlib.Path(value).resolve(strict=False)
except (OSError, TypeError, ValueError):
continue
if (
path_is_relative_to(workspace, protected)
or path_is_relative_to(protected, workspace)
or paths_overlap_casefold(workspace, protected)
):
return f"workspace_root overlaps the {label}"
return ""
def _subagent_projects_read_hint(
ctx: Any,
resolved: pathlib.Path,
hard_protected_roots: list[pathlib.Path],
) -> str:
"""A targeted refusal for a user_files path that actually lives inside the
subagent-projects area: name root=subagent_projects with the exact relative
path instead of steering the model at roots that cannot reach the target.
Empty when the target is not there, the active profile cannot read that root,
or the projects root is misconfigured to overlap a HARD drive (never steer a
read at the control plane)."""
try:
profile_policy = _POLICY.get(active_tool_profile(ctx), {})
if "read" not in profile_policy.get("subagent_projects", set()):
return ""
projects_root = resource_root_path(ctx, "subagent_projects")
if any(
path_is_relative_to(projects_root, hard) or _path_is_relative_to_casefold(projects_root, hard)
for hard in hard_protected_roots
):
return ""
if not (
path_is_relative_to(resolved, projects_root)
or _path_is_relative_to_casefold(resolved, projects_root)
):
return ""
try:
rel = str(resolved.relative_to(projects_root))
except ValueError:
rel = os.path.relpath(str(resolved), str(projects_root))
rel = rel if rel not in ("", ".") else "."
return (
"this path is inside root=subagent_projects (the durable child-project "
f"area); read it via root=subagent_projects, path={rel!r} "
"(read/list/search only — no write/shell there by design: children "
"write via write_surface=external_workspace, and the host "
"checkpoint-commits dirty coop trees at root finalization)"
)
except Exception:
return ""
def user_files_path_block_reason(
ctx: Any,
candidate: pathlib.Path,
*,
allow_protected_descendants: bool = False,
) -> str:
"""Return a block reason when candidate is not an external user file."""
resolved = pathlib.Path(candidate).expanduser().resolve(strict=False)
home = _user_files_root()
outside_home = not path_is_relative_to(resolved, home) and not _path_is_relative_to_casefold(resolved, home)
# External-workspace tasks may reach host scratch outside home (/tmp, /build,
# sibling checkouts). The runtime-overlap and credential guards BELOW still
# run on the full path, so the Ouroboros repo/data drive and secret-like
# files stay protected even when home confinement is lifted.
if outside_home and not is_external_workspace(ctx):
return f"path is outside user home {home}"
# The Ouroboros runtime/control surface is the system repo PLUS every data
# drive the task touches: the parent drive (ctx.drive_root) and any child /
# budget drive carried in task_metadata. External-workspace mode lifts home
# confinement, so these must be enumerated explicitly here — otherwise a
# child-drive control path (e.g. <child_drive>/memory) would slip through.
protected_values: list[Any] = [
getattr(ctx, "drive_root", None),
getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir", None),
]
meta = getattr(ctx, "task_metadata", {})
if isinstance(meta, dict):
for key in ("drive_root", "child_drive_root", "headless_child_drive_root", "budget_drive_root"):
if meta.get(key):
protected_values.append(meta.get(key))
protected_roots: list[pathlib.Path] = []
hard_protected_roots: list[pathlib.Path] = [] # the data/repo/budget drives THEMSELVES
for value in protected_values:
try:
root = pathlib.Path(value).resolve(strict=False)
except (OSError, TypeError, ValueError):
continue
protected_roots.append(root)
hard_protected_roots.append(root)
parent = root.parent.resolve(strict=False)
if root.name in {"repo", "data"} and path_is_relative_to(parent, home):
# The workspace PARENT is a SOFT boundary (keeps user_files out of ~/Ouroboros at large);
# it is deliberately NOT a hard root, so the Deliverables sibling under it stays allowed.
protected_roots.append(parent)
# The configured Deliverables container is an INTENDED user-output root, allowed past the
# workspace-overlap guard — but ONLY when it is a genuine sibling: a misconfigured
# OUROBOROS_DELIVERABLES_ROOT that overlaps or contains a HARD data/repo/budget drive must NOT
# open a bypass. The outside-home, credential, and hidden-name checks still apply regardless.
in_deliverables = False
try:
_deliverables = _deliverables_root()
_deliverables_safe = not any(
path_is_relative_to(_deliverables, pr) or _path_is_relative_to_casefold(_deliverables, pr)
or path_is_relative_to(pr, _deliverables) or _path_is_relative_to_casefold(pr, _deliverables)
for pr in hard_protected_roots
)
if _deliverables_safe and (
path_is_relative_to(resolved, _deliverables) or _path_is_relative_to_casefold(resolved, _deliverables)
):
in_deliverables = True
except Exception:
in_deliverables = False
if not in_deliverables:
for protected in protected_roots:
overlaps_protected = path_is_relative_to(resolved, protected) or _path_is_relative_to_casefold(resolved, protected)
contains_protected = path_is_relative_to(protected, resolved) or _path_is_relative_to_casefold(protected, resolved)
if overlaps_protected or (
not allow_protected_descendants and contains_protected
):
# Name the root that ACTUALLY contains the target (the v6.54.3
# shell_cwd_block_message lesson applied to this surface): the
# subagent-projects area lives under the SOFT ~/Ouroboros parent,
# so every coop-tree read used to get a message naming four roots
# that cannot reach it while omitting the one that can. MESSAGE
# ONLY — subagent_projects stays a read-only root (no user_files
# write carve-out), and a target inside a HARD drive never takes
# this branch.
projects_hint = _subagent_projects_read_hint(ctx, resolved, hard_protected_roots)
if projects_hint:
return projects_hint
return (
"path overlaps the Ouroboros repo/runtime workspace; use "
"root=active_workspace, root=task_drive, root=artifact_store, "
"or root=skill_payload instead"
)
try:
parts = resolved.relative_to(home).parts
except ValueError:
parts = resolved.parts
for part in parts:
if not part:
continue
part_lower = part.lower()
# v6.52.0 (P1): DEFAULT-DENY hidden (dot) components. Known secret/credential/VCS dirs
# are always blocked; ANY OTHER dotted component is blocked too UNLESS it is in the small
# benign allowlist (.github/.vscode/.idea/...). Benign project dotdirs become readable
# (the owner's goal) while the in-home dotfile space stays safe-by-default — an enumerated
# blocklist would leak credential stores like ~/.terraform.d, ~/.cargo, ~/.pip, etc.
if part_lower in _USER_FILES_SECRET_COMPONENTS:
return "path is hidden or credential-like (secret/credential directory)"
if part.startswith(".") and part_lower not in _USER_FILES_ALLOWED_DOTNAMES:
return "path is hidden or credential-like (non-allowlisted hidden component)"
name = resolved.name
name_lower = name.lower()
if (
name_lower in _USER_FILES_SECRET_NAMES
or _USER_FILES_SECRET_RE.search(name)
or name_lower.endswith((".key", ".pem", ".p12", ".pfx"))
):
return "path name is credential-like"
return ""
class UserFilesPathBlockedError(ValueError):
"""Typed user_files confinement refusal (a POLICY denial, not an I/O failure).
Subclasses ``ValueError`` so every existing generic handler keeps working;
the read-surface wrappers (read_file/list_files/search_code) render it with
the typed ``⚠️ USER_FILES_PATH_BLOCKED`` prefix so the outcome axis can
partition it into ``execution.policy_denials`` (v6.57.0) instead of the
generic ``error`` status that falsely degraded a shipped task to
``tool_failure`` (the submarine wave-3 incident)."""
def resolve_user_file_path(
ctx: Any,
path: str,
*,
allow_protected_descendants: bool = False,
allow_outside_home: bool = False,
) -> pathlib.Path:
"""Resolve a user_files path under the user's home and outside Ouroboros control-plane roots.
Absolute paths OUTSIDE the user_files home (and the Deliverables container) are
rejected EARLY with an actionable error instead of resolving to a foreign root
and failing later with an opaque ``relative_to`` crash (v6.54.3 — the TB2.1
``'/app' is not in the subpath of '/root'`` class). ``allow_outside_home=True``
(the ``query_code`` external-target caller) skips only this EARLY actionable
check; ``user_files_path_block_reason`` below remains the outside-home
AUTHORITY, and it permits outside-home only for external-workspace contexts —
the mode the documented query_code contract (benchmark ``/app``) runs in.
Neither flag expands authority: a non-external context could not reach
outside-home before this check existed either."""
raw_text = str(path or ".").strip() or "."
try:
raw = pathlib.Path(raw_text).expanduser()
except Exception:
# expanduser() raises RuntimeError for an unknown '~user'; leave it unexpanded —
# the '~' branch below maps it into the jail home (raw is only used elsewhere for
# absolute paths, where expanduser is a no-op anyway).
raw = pathlib.Path(raw_text)
home = _user_files_root()
# is_absolute_path_text gives consistent cross-platform absolute detection
# (drive-less "/x" roots and "C:\\x"/"\\\\unc" are all absolute) so Windows
# does not silently treat a rooted path as home-relative.
if is_absolute_path_text(raw_text):
candidate = raw.resolve(strict=False)
# External-workspace tasks legitimately reach host scratch outside home
# (/tmp, /build, sibling checkouts) — for them the generic
# user_files_path_block_reason below stays the authority, mirroring its
# own is_external_workspace carve-out.
if not allow_outside_home and not is_external_workspace(ctx):
home_resolved = home.resolve(strict=False)
# Case-insensitive-platform parity with the user_files_path_block_reason
# authority: a differently-cased safe home path must not be rejected
# early where the casefold-aware guard would accept it (review round 7).
inside_home = path_is_relative_to(candidate, home_resolved) or _path_is_relative_to_casefold(
candidate, home_resolved
)
inside_deliverables = False
if not inside_home:
try:
deliverables_resolved = _deliverables_root().resolve(strict=False)
inside_deliverables = path_is_relative_to(
candidate, deliverables_resolved
) or _path_is_relative_to_casefold(candidate, deliverables_resolved)
except (OSError, ValueError):
inside_deliverables = False
if not inside_home and not inside_deliverables:
raise UserFilesPathBlockedError(
"user_files path blocked: absolute path "
f"{raw_text!r} is outside the user_files home ({home_resolved}). "
"Use root='active_workspace' for workspace paths, or a "
"home-relative path (e.g. 'Desktop/file.txt') for user files."
)
elif raw_text.startswith("~"):
# '~' / '~user' must expand to the CONFIGURED user_files home (the jail), NOT the
# real OS home — otherwise OUROBOROS_USER_FILES_ROOT isolation is bypassed by a
# '~/...' path. The jail has a single home, so '~user/sub' maps to '<home>/sub'.
_after = raw_text[1:]
if _after[:1] in ("/", "\\"):
_rel = _after[1:]
elif "/" in _after or "\\" in _after:
_rel = _after.replace("\\", "/").split("/", 1)[1]
else:
_rel = "" # bare '~' or '~user' -> the home directory itself
candidate = (home / safe_relpath(_rel)).resolve(strict=False) if _rel else home.resolve(strict=False)
else:
# safe_relpath has already normalized any Windows backslash to a POSIX '/', so the
# directory test below is separator-correct on every platform.
rel = safe_relpath(raw_text)
home_candidate = home / rel
if "/" in rel.strip("/") or home_candidate.exists():
# An explicit placement (a path WITH a directory — Desktop/..., Downloads/..., a subdir)
# OR a bare name that ALREADY EXISTS under home (an existing file or directory such as
# `Desktop`) is honored under the owner home exactly as given. This keeps read/list/search
# of existing user files and directory names home-relative — only a genuinely NEW unnamed
# output is containerized.
candidate = home_candidate.resolve(strict=False)
else:
# A bare name with no directory that does NOT already exist under home is an unnamed NEW
# deliverable: route it into the visible Deliverables container instead of cluttering the
# home root (a later read of the same bare name resolves there too, staying consistent).
candidate = (_deliverables_root() / rel).resolve(strict=False)
reason = user_files_path_block_reason(
ctx,
candidate,
allow_protected_descendants=allow_protected_descendants,
)
if reason:
raise UserFilesPathBlockedError(f"user_files path blocked: {reason}")
return candidate
def _select_process_target(
ctx: Any,
cwd: str,
@ -1220,45 +539,6 @@ def resolve_shell_cwd(
return target, root, allowed
def canonical_data_root(ctx: Any) -> pathlib.Path:
"""Return canonical skill data: task budget → context budget → task drive."""
metadata = getattr(ctx, "task_metadata", None)
metadata = metadata if isinstance(metadata, dict) else {}
for candidate in (metadata.get("budget_drive_root"), getattr(ctx, "budget_drive_root", "")):
text = str(candidate or "").strip()
if text:
return pathlib.Path(text).resolve(strict=False)
return pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False)
def normalize_runtime_data_path(data_root: pathlib.Path, path: str) -> str:
"""Normalize historical runtime-data prefixes before physical binding."""
norm = str(path or ".").strip().replace("\\", "/")
norm = norm[2:] if norm.startswith("./") else norm
stripped = norm.lstrip("/")
root_text = str(pathlib.Path(data_root)).rstrip("/").lstrip("/")
if root_text and stripped.startswith(root_text):
return stripped[len(root_text):].lstrip("/") or "."
if stripped.startswith(".tmp-data-"):
_prefix, separator, after = stripped.partition("/")
if separator:
return after[len("data/"):] if after.startswith("data/") else after
return norm or "."
def load_bound_skill(binding: ResolvedResourceBinding) -> Any:
"""Load the frozen payload target while preserving lifecycle provenance."""
from ouroboros.skill_loader import _classify_skill_source, load_skill
loaded = load_skill(binding.base_path, binding.state_drive_root)
if loaded is not None:
loaded.source = _classify_skill_source(
binding.base_path,
location=binding.source,
drive_root=binding.state_drive_root,
)
return loaded
def canonical_repo_relative_path(ctx: Any, root: str, path: str) -> str:
"""Normalize repo paths so guards and mutations judge the same target."""
if root not in {"active_workspace", "system_repo"}:
@ -1273,95 +553,6 @@ def canonical_repo_relative_path(ctx: Any, root: str, path: str) -> str:
return path
def _skill_payload_base(
ctx: Any,
*,
profile: ToolProfile,
operation: Operation,
location: str,
skill_name: str,
allow_missing: bool = False,
) -> tuple[pathlib.Path, str, str]:
"""Select one physical package and project its effective source."""
from ouroboros.skill_payload_binding import resolve_skill_payload_base
return resolve_skill_payload_base(
ctx,
drive_root=canonical_data_root(ctx),
profile=profile,
top_level=profile in _TOP_LEVEL_PRINCIPAL_PROFILES,
operation=operation,
location=location,
skill_name=skill_name,
allow_missing=allow_missing,
)
def resource_root_path(
ctx: Any,
root: ResourceRoot,
*,
bucket: str = "",
skill_name: str = "",
) -> pathlib.Path:
if root == "active_workspace":
active = getattr(ctx, "active_repo_dir", None)
candidate = None
if callable(active):
try:
candidate = active()
except Exception:
candidate = None
if candidate is None or candidate.__class__.__module__.startswith("unittest.mock"):
candidate = getattr(ctx, "repo_dir")
return pathlib.Path(candidate).resolve(strict=False)
if root == "system_repo":
return pathlib.Path(getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir")).resolve(strict=False)
if root == "runtime_data":
return pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False)
if root == "task_drive":
return (pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False) / "task_drives" / task_id_for_artifacts(ctx)).resolve(strict=False)
if root == "artifact_store":
return task_artifact_dir_path(pathlib.Path(getattr(ctx, "drive_root")), task_id_for_artifacts(ctx), create=False).resolve(strict=False)
if root == "user_files":
return _user_files_root()
if root == "subagent_projects":
from ouroboros.config import get_subagent_projects_root
return pathlib.Path(get_subagent_projects_root()).expanduser().resolve(strict=False)
if root == "deliverables":
return _deliverables_root()
if root == "skill_payload":
b = str(bucket or "").strip()
s = str(skill_name or "").strip()
if not b or not s:
raise ValueError("root=skill_payload requires bucket and skill_name")
base, _source, _name = _skill_payload_base(
ctx,
profile=active_tool_profile(ctx),
operation="read",
location=b,
skill_name=s,
)
return base
raise ValueError(f"unknown root {root!r}")
def binding_targets_system_repo(
ctx: Any, binding: ResolvedResourceBinding | None,
) -> bool:
"""Whether a selected logical root physically lands on Ouroboros source."""
if binding is None:
return False
try:
return pathlib.Path(binding.base_path).resolve(strict=False) == resource_root_path(
ctx, "system_repo",
)
except (OSError, TypeError, ValueError):
return False
def _resolve_target_in_selected_base(
ctx: Any,
*,

View file

@ -0,0 +1,213 @@
"""Physical path primitives for the access matrix.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import os
import pathlib
from ouroboros.tool_access_types import _ALL_ROOTS
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from ouroboros.tool_access_types import ResourceRoot
from typing import Any
def _tool_access():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros import tool_access
return tool_access
def _user_files_root() -> pathlib.Path:
"""Filesystem base for the ``user_files`` resource root.
Defaults to the owner's real home. A jailed/benchmark runtime can redirect it
to a scratch directory via ``OUROBOROS_USER_FILES_ROOT`` so a task physically
cannot resolve the owner's real home (e.g. ``~/file1.txt`` secret files). Any
unusable value falls back to the real home — fail-safe, never broadens reach.
"""
raw = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
if raw:
try:
return pathlib.Path(raw).expanduser().resolve(strict=False)
except Exception:
# ANY unusable value (bad path, unknown ``~user`` RuntimeError, odd OS error)
# fails safe to the real home — the doc's "any unusable value" contract.
pass
return pathlib.Path.home().resolve(strict=False)
def _deliverables_root() -> pathlib.Path:
"""Container for UNNAMED user deliverables, JAIL-AWARE: when the user_files home is
redirected (``OUROBOROS_USER_FILES_ROOT``) and no explicit
``OUROBOROS_DELIVERABLES_ROOT`` is set, keep unnamed deliverables INSIDE the jail so a
bare ``write_file(root='user_files', path='answer.txt')`` stays reachable and in-bounds
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 get_deliverables_root
jail = (os.environ.get("OUROBOROS_USER_FILES_ROOT") or "").strip()
explicit = (os.environ.get("OUROBOROS_DELIVERABLES_ROOT") or "").strip()
if explicit:
return pathlib.Path(explicit).expanduser().resolve(strict=False)
if jail and not explicit:
return (_user_files_root() / "Deliverables").resolve(strict=False)
return pathlib.Path(get_deliverables_root()).expanduser().resolve(strict=False)
def normalize_root(root: str | None, *, default: ResourceRoot = "active_workspace") -> ResourceRoot:
candidate = str(root or default).strip() or default
if candidate not in _ALL_ROOTS:
raise ValueError(f"unknown root {candidate!r}; expected one of {sorted(_ALL_ROOTS)}")
return candidate # type: ignore[return-value]
def path_is_relative_to(path: pathlib.Path, root: pathlib.Path) -> bool:
try:
pathlib.Path(path).resolve(strict=False).relative_to(pathlib.Path(root).resolve(strict=False))
return True
except (OSError, ValueError):
return False
def normalize_root_relative(root: pathlib.Path, path: str) -> str:
"""Map a model-supplied path to a root-relative string when it redundantly
encodes the root, so structural/read tools accept the paths an agent
naturally writes: an absolute path inside the active root (e.g. ``/app/foo``
under a workspace rooted at ``/app``) and a single redundant root-basename
prefix (``app/foo``). Returns a RELATIVE string only — it never widens
access: callers still apply ``safe_relpath`` + a ``relative_to`` confinement
check, so a genuine escape is still rejected downstream.
- absolute & inside root -> stripped to relative
- absolute & outside root -> returned unchanged (caller's check rejects it)
- redundant root-basename prefix, existence-guarded -> stripped
- otherwise -> unchanged
"""
text = str(path or "").strip().replace("\\", "/")
if not text or text in (".", "./"):
return text
try:
root_resolved = pathlib.Path(root).resolve(strict=False)
except (OSError, ValueError):
return text
# (A) absolute path that already points inside the root.
if _tool_access().is_absolute_path_text(text):
try:
return pathlib.Path(text).resolve(strict=False).relative_to(root_resolved).as_posix()
except (OSError, ValueError):
return text # outside root -> let the caller's confinement reject it
# (B) redundant root-basename prefix ('app' or 'app/x' when root basename is
# 'app'). Strip it UNLESS the root contains a real same-named subdir (then
# 'app/x' is ambiguously a genuine nested path and is kept). Gating on the
# absence of that subdir — not on the target existing — lets NEW write/create
# targets ('app/new.py' -> 'new.py') normalize too, while a real 'app/'
# subdir is never mis-stripped. Only ever shortens toward root (no escape).
base = root_resolved.name
if base and (text == base or text.startswith(base + "/")):
try:
if not (root_resolved / base).is_dir():
return text[len(base):].lstrip("/") or "."
except (ValueError, OSError):
# `..`/traversal or stat error: leave unchanged so the caller's
# confinement produces the canonical (not a generic) error.
return text
return text
def _path_is_relative_to_casefold(path: pathlib.Path, root: pathlib.Path) -> bool:
try:
path_parts = pathlib.Path(path).resolve(strict=False).parts
root_parts = pathlib.Path(root).resolve(strict=False).parts
except (OSError, ValueError):
return False
if len(path_parts) < len(root_parts):
return False
return tuple(part.casefold() for part in path_parts[: len(root_parts)]) == tuple(
part.casefold() for part in root_parts
)
def paths_overlap_casefold(left: pathlib.Path, right: pathlib.Path) -> bool:
"""Return True when two paths overlap under case-insensitive path semantics."""
return _path_is_relative_to_casefold(left, right) or _path_is_relative_to_casefold(right, left)
def workspace_mode_block_reason(ctx: Any) -> str:
mode = str(getattr(ctx, "workspace_mode", "") or "").strip()
workspace_root = getattr(ctx, "workspace_root", None)
if not mode or workspace_root is None:
return ""
try:
workspace = pathlib.Path(workspace_root).resolve(strict=False)
except (OSError, TypeError, ValueError):
return "workspace_root is invalid"
protected_values = (
("Ouroboros system repo", getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir", None)),
("Ouroboros repo", getattr(ctx, "repo_dir", None)),
("Ouroboros data drive", getattr(ctx, "drive_root", None)),
(
"Ouroboros parent data drive",
(getattr(ctx, "task_metadata", {}) or {}).get("budget_drive_root")
if isinstance(getattr(ctx, "task_metadata", {}), dict)
else "",
),
)
for label, value in protected_values:
if not value:
continue
try:
protected = pathlib.Path(value).resolve(strict=False)
except (OSError, TypeError, ValueError):
continue
if (
path_is_relative_to(workspace, protected)
or path_is_relative_to(protected, workspace)
or paths_overlap_casefold(workspace, protected)
):
return f"workspace_root overlaps the {label}"
return ""
def canonical_data_root(ctx: Any) -> pathlib.Path:
"""Return canonical skill data: task budget → context budget → task drive."""
metadata = getattr(ctx, "task_metadata", None)
metadata = metadata if isinstance(metadata, dict) else {}
for candidate in (metadata.get("budget_drive_root"), getattr(ctx, "budget_drive_root", "")):
text = str(candidate or "").strip()
if text:
return pathlib.Path(text).resolve(strict=False)
return pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False)
def normalize_runtime_data_path(data_root: pathlib.Path, path: str) -> str:
"""Normalize historical runtime-data prefixes before physical binding."""
norm = str(path or ".").strip().replace("\\", "/")
norm = norm[2:] if norm.startswith("./") else norm
stripped = norm.lstrip("/")
root_text = str(pathlib.Path(data_root)).rstrip("/").lstrip("/")
if root_text and stripped.startswith(root_text):
return stripped[len(root_text):].lstrip("/") or "."
if stripped.startswith(".tmp-data-"):
_prefix, separator, after = stripped.partition("/")
if separator:
return after[len("data/"):] if after.startswith("data/") else after
return norm or "."

View file

@ -0,0 +1,229 @@
"""Who is acting and where each resource root physically lives.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from ouroboros.tool_access_types import Operation
from ouroboros.tool_access_types import ResolvedResourceBinding
from ouroboros.tool_access_types import ResourceRoot
from ouroboros.tool_access_types import ToolProfile
from typing import Any
from typing import Optional
def _tool_access():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros import tool_access
return tool_access
def _is_subagent_ctx(ctx: Any) -> bool:
"""True when the task is a delegated subagent (by lineage metadata)."""
for attr in ("task_metadata", "task_contract"):
data = getattr(ctx, attr, None)
if isinstance(data, dict) and str(data.get("delegation_role") or "").strip() == "subagent":
return True
return False
def is_external_workspace(ctx: Any) -> bool:
"""True for an EXTERNAL-workspace top-level task (not the system repo).
External-workspace tasks operate on a pre-existing working tree somewhere on
the host (container scratch, a repo cloned under ``/tmp`` or ``/build``,
etc.). They legitimately read, run commands, and use git OUTSIDE the user
home, while the Ouroboros runtime (system repo + data drive) and
credential-like files stay protected by the per-path guards. ``self_worktree``
and ``genesis`` are acting-subagent SURFACES (``acting_subagent`` profile),
never this profile, so they keep full home/runtime confinement.
"""
try:
if not bool(getattr(ctx, "is_workspace_mode", lambda: False)()):
return False
except Exception:
return False
return str(getattr(ctx, "workspace_mode", "") or "").strip().lower() == "external"
def active_tool_profile(ctx: Any) -> ToolProfile:
constraint = _tool_access().normalize_task_constraint(getattr(ctx, "task_constraint", None))
mode = str(getattr(constraint, "mode", "") or "").strip()
if mode == _tool_access().LOCAL_READONLY_SUBAGENT_MODE:
return "local_readonly_subagent"
if mode == _tool_access().ACTING_SUBAGENT_MODE:
# Acting subagents require a resolved write surface; otherwise fail
# closed to read-only rather than inheriting a broader profile.
surface = str(getattr(constraint, "surface", "") or "").strip()
if surface in _tool_access().VALID_WRITE_SURFACES:
return "acting_subagent"
return "local_readonly_subagent"
if mode == "skill_repair":
return "skill_repair"
# Fail-closed floor (BIBLE P3), checked BEFORE workspace/direct-chat: a
# delegated subagent without a valid readonly/acting/skill constraint is
# read-only and must never inherit workspace_task / operator_control /
# self_modification. The parent remains the sole local writer/committer.
if _is_subagent_ctx(ctx):
return "local_readonly_subagent"
if bool(getattr(ctx, "is_workspace_mode", lambda: False)()):
# Keep distinct preset names for focus/path diagnostics. Both use the
# shared ordinary principal; external host-scratch reach is a path fact.
if is_external_workspace(ctx):
return "external_workspace_task"
return "workspace_task"
if bool(getattr(ctx, "is_direct_chat", False)):
return "operator_control"
return "self_modification"
def predicted_subagent_profile(*, write_surface: str = "") -> ToolProfile:
"""The tool profile a scheduled subagent will resolve to, from schedule-time
inputs only (v6.57.0, 1.6). A valid write_surface → acting_subagent; otherwise
a read-only subagent. Mirrors active_tool_profile's subagent branches so the
parent's schedule result and the child's start context can preview the profile
without a live ctx. NOT authoritative — the supervisor's _resolve_subagent_
constraint is the real gate; this is a visibility preview."""
surface = str(write_surface or "").strip()
if surface and surface in _tool_access().VALID_WRITE_SURFACES:
return "acting_subagent"
return "local_readonly_subagent"
def project_room_lens_dir(ctx: Any) -> Optional[pathlib.Path]:
"""Return a direct-chat room's verified project cwd, otherwise ``None``.
Promoted/workspace/subagent tasks carry their own workspace; only a direct
chat without one may use the injected existing ``_project_room_dir``.
"""
if not bool(getattr(ctx, "is_direct_chat", False)):
return None
if getattr(ctx, "workspace_root", None):
return None
meta = getattr(ctx, "task_metadata", None)
raw = str(meta.get("_project_room_dir") or "").strip() if isinstance(meta, dict) else ""
if not raw:
return None
try:
candidate = pathlib.Path(raw).resolve(strict=False)
return candidate if candidate.is_dir() else None
except OSError:
return None
def load_bound_skill(binding: ResolvedResourceBinding) -> Any:
"""Load the frozen payload target while preserving lifecycle provenance."""
from ouroboros.skill_loader import _classify_skill_source, load_skill
loaded = load_skill(binding.base_path, binding.state_drive_root)
if loaded is not None:
loaded.source = _classify_skill_source(
binding.base_path,
location=binding.source,
drive_root=binding.state_drive_root,
)
return loaded
def _skill_payload_base(
ctx: Any,
*,
profile: ToolProfile,
operation: Operation,
location: str,
skill_name: str,
allow_missing: bool = False,
) -> tuple[pathlib.Path, str, str]:
"""Select one physical package and project its effective source."""
from ouroboros.skill_payload_binding import resolve_skill_payload_base
return resolve_skill_payload_base(
ctx,
drive_root=_tool_access().canonical_data_root(ctx),
profile=profile,
top_level=profile in _tool_access()._TOP_LEVEL_PRINCIPAL_PROFILES,
operation=operation,
location=location,
skill_name=skill_name,
allow_missing=allow_missing,
)
def resource_root_path(
ctx: Any,
root: ResourceRoot,
*,
bucket: str = "",
skill_name: str = "",
) -> pathlib.Path:
if root == "active_workspace":
active = getattr(ctx, "active_repo_dir", None)
candidate = None
if callable(active):
try:
candidate = active()
except Exception:
candidate = None
if candidate is None or candidate.__class__.__module__.startswith("unittest.mock"):
candidate = getattr(ctx, "repo_dir")
return pathlib.Path(candidate).resolve(strict=False)
if root == "system_repo":
return pathlib.Path(getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir")).resolve(strict=False)
if root == "runtime_data":
return pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False)
if root == "task_drive":
return (pathlib.Path(getattr(ctx, "drive_root")).resolve(strict=False) / "task_drives" / _tool_access().task_id_for_artifacts(ctx)).resolve(strict=False)
if root == "artifact_store":
return _tool_access().task_artifact_dir_path(pathlib.Path(getattr(ctx, "drive_root")), _tool_access().task_id_for_artifacts(ctx), create=False).resolve(strict=False)
if root == "user_files":
return _tool_access()._user_files_root()
if root == "subagent_projects":
from ouroboros.config import get_subagent_projects_root
return pathlib.Path(get_subagent_projects_root()).expanduser().resolve(strict=False)
if root == "deliverables":
return _tool_access()._deliverables_root()
if root == "skill_payload":
b = str(bucket or "").strip()
s = str(skill_name or "").strip()
if not b or not s:
raise ValueError("root=skill_payload requires bucket and skill_name")
base, _source, _name = _skill_payload_base(
ctx,
profile=active_tool_profile(ctx),
operation="read",
location=b,
skill_name=s,
)
return base
raise ValueError(f"unknown root {root!r}")
def binding_targets_system_repo(
ctx: Any, binding: ResolvedResourceBinding | None,
) -> bool:
"""Whether a selected logical root physically lands on Ouroboros source."""
if binding is None:
return False
try:
return pathlib.Path(binding.base_path).resolve(strict=False) == resource_root_path(
ctx, "system_repo",
)
except (OSError, TypeError, ValueError):
return False

View file

@ -0,0 +1,207 @@
"""The closed access vocabulary and the profile x root x operation policy matrix.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
import pathlib
def _tool_access():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros import tool_access
return tool_access
ToolProfile = Literal[
"self_modification",
"workspace_task",
"external_workspace_task",
"acting_subagent",
"skill_repair",
"local_readonly_subagent",
"operator_control",
]
ResourceRoot = Literal[
"active_workspace",
"system_repo",
"runtime_data",
"task_drive",
"skill_payload",
"artifact_store",
"user_files",
"subagent_projects",
"deliverables",
]
Operation = Literal[
"read",
"list",
"search",
"write",
"edit",
"shell",
"vcs",
"review",
"delegate",
"service",
]
SubagentCapability = Literal[
"write",
"edit",
"shell",
"vcs",
"review",
"delegate",
"service",
]
@dataclass(frozen=True)
class ToolAccessDecision:
allow: bool
reason: str = ""
guard: str = ""
@dataclass(frozen=True)
class ResolvedResourceBinding:
"""One dispatch-selected logical root and its exact physical target."""
profile: ToolProfile
root: ResourceRoot
operation: Operation
base_path: pathlib.Path
target_path: pathlib.Path
source: str
skill_name: str
state_drive_root: pathlib.Path
logical_base_path: pathlib.Path | None = None
_ALL_ROOTS: frozenset[str] = frozenset({
"active_workspace",
"system_repo",
"runtime_data",
"task_drive",
"skill_payload",
"artifact_store",
"user_files",
"subagent_projects",
"deliverables",
})
_READONLY_RESOURCE_ROOTS: frozenset[str] = frozenset({"subagent_projects", "deliverables"})
_TOP_LEVEL_PRINCIPAL_PROFILES: frozenset[str] = frozenset({
"workspace_task",
"external_workspace_task",
"self_modification",
})
_READ_OPS = frozenset({"read", "list", "search"})
_TOP_LEVEL_PRINCIPAL_POLICY: dict[str, set[str]] = {
"active_workspace": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"system_repo": {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "service"},
"runtime_data": {"read", "list", "search", "write", "edit"},
"task_drive": {"read", "list", "write", "edit", "shell", "service"},
"skill_payload": {"read", "list", "search", "write", "edit", "review", "shell"},
"artifact_store": {"read", "list", "write", "shell", "service"},
"user_files": {"read", "list", "search", "write", "edit", "shell", "service"},
"subagent_projects": {"read", "list", "search"},
"deliverables": {"read", "list", "search"},
}
_POLICY: dict[str, dict[str, set[str]]] = {
"local_readonly_subagent": {
# Read-only child VCS names still need their target binding to resolve.
"active_workspace": set(_READ_OPS) | {"vcs"},
"system_repo": set(_READ_OPS) | {"vcs"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
# v6.70.0 (owner-approved): read-only scouts sent to review a skill were
# structurally blind to its payload — a scout literally reported
# "reviewing blind", and a correct "skill does not exist" answer was
# indistinguishable from an access block. Payloads are skill CODE
# (data/skills/...); grants/secrets live in data/state/skills, which
# stays invisible to this profile.
"skill_payload": {"read", "list", "search"},
},
"skill_repair": {
"skill_payload": {"read", "list", "search", "write", "edit", "review"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
},
# Top-level preset names remain observable, but workspace focus never narrows
# the ordinary principal. Independent path/credential/child/runtime guards
# still apply after this shared operation matrix.
"workspace_task": _TOP_LEVEL_PRINCIPAL_POLICY,
"external_workspace_task": _TOP_LEVEL_PRINCIPAL_POLICY,
# Mutative (acting) subagents write only inside their isolated active
# workspace (self_worktree / external_workspace / genesis). No vcs-commit /
# review here; the parent integrates and commits. self_worktree additionally
# keeps protected-path discipline active in the registry (it is the system
# repo). runtime_data stays read-only.
"acting_subagent": {
# Acting children write ONLY inside their isolated surface (active_workspace =
# the self_worktree / external_workspace / genesis). task_drive / artifact_store
# are read-only here (no extra write surface); the deliverable is a workspace.patch.
"active_workspace": {"read", "list", "search", "write", "edit", "shell", "vcs", "service"},
"runtime_data": {"read", "list"},
"task_drive": {"read", "list"},
"artifact_store": {"read", "list"},
},
"self_modification": _TOP_LEVEL_PRINCIPAL_POLICY,
# operator_control gets full authority on every mutable root, but the orchestrator
# read-only roots stay read-only even here (they are deliverables/durable projects,
# not a control surface).
"operator_control": {
**{root: {"read", "list", "search", "write", "edit", "shell", "vcs", "review", "delegate", "service"}
for root in _ALL_ROOTS if root not in _READONLY_RESOURCE_ROOTS},
**{root: {"read", "list", "search"} for root in _READONLY_RESOURCE_ROOTS},
},
}
_SUBAGENT_CAPABILITY_TO_OPERATION: dict[str, Operation] = {
"write": "write",
"edit": "edit",
"shell": "shell",
"vcs": "vcs",
"review": "review",
"delegate": "delegate",
"service": "service",
}
SUBAGENT_CAPABILITIES: tuple[str, ...] = tuple(_SUBAGENT_CAPABILITY_TO_OPERATION.keys())

View file

@ -0,0 +1,367 @@
"""The user_files confinement: secret-name policy and path resolution.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import os
import pathlib
import re
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from typing import Any
def _tool_access():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros import tool_access
return tool_access
_USER_FILES_SECRET_COMPONENTS = frozenset({
".aws",
".azure",
".config",
".docker",
".git", # v6.52.0: VCS internals hold config + stored credentials
".gnupg",
".hg",
".kube",
".local",
".netrc",
".ssh",
".svn",
"library",
})
_USER_FILES_SECRET_NAMES = frozenset({
".env",
# v6.52.0: credential / shell-init / history dotFILES kept blocked AFTER the bare
# `startswith('.')` block was dropped (so benign project dotdirs are readable while
# secret-bearing dotfiles are not).
".bash_history",
".bash_profile",
".bashrc",
".dockercfg",
".git-credentials",
".gitconfig",
".htpasswd",
".npmrc",
".pgpass",
".profile",
".pypirc",
".python_history",
".zsh_history",
".zprofile",
".zshrc",
"auth.json",
"credentials",
"credentials.json",
"secrets.json",
"settings.json",
"token.json",
"tokens.json",
})
_USER_FILES_SECRET_RE = re.compile(r"(?:^|[._-])(api[_-]?key|credential|password|secret|token)(?:[._-]|$)", re.I)
_USER_FILES_ALLOWED_DOTNAMES = frozenset({
".github",
".gitlab",
".circleci",
".devcontainer",
".vscode",
".idea",
".gitignore",
".gitattributes",
".gitmodules",
".dockerignore",
".editorconfig",
})
def _subagent_projects_read_hint(
ctx: Any,
resolved: pathlib.Path,
hard_protected_roots: list[pathlib.Path],
) -> str:
"""A targeted refusal for a user_files path that actually lives inside the
subagent-projects area: name root=subagent_projects with the exact relative
path instead of steering the model at roots that cannot reach the target.
Empty when the target is not there, the active profile cannot read that root,
or the projects root is misconfigured to overlap a HARD drive (never steer a
read at the control plane)."""
try:
profile_policy = _tool_access()._POLICY.get(_tool_access().active_tool_profile(ctx), {})
if "read" not in profile_policy.get("subagent_projects", set()):
return ""
projects_root = _tool_access().resource_root_path(ctx, "subagent_projects")
if any(
_tool_access().path_is_relative_to(projects_root, hard) or _tool_access()._path_is_relative_to_casefold(projects_root, hard)
for hard in hard_protected_roots
):
return ""
if not (
_tool_access().path_is_relative_to(resolved, projects_root)
or _tool_access()._path_is_relative_to_casefold(resolved, projects_root)
):
return ""
try:
rel = str(resolved.relative_to(projects_root))
except ValueError:
rel = os.path.relpath(str(resolved), str(projects_root))
rel = rel if rel not in ("", ".") else "."
return (
"this path is inside root=subagent_projects (the durable child-project "
f"area); read it via root=subagent_projects, path={rel!r} "
"(read/list/search only — no write/shell there by design: children "
"write via write_surface=external_workspace, and the host "
"checkpoint-commits dirty coop trees at root finalization)"
)
except Exception:
return ""
def user_files_path_block_reason(
ctx: Any,
candidate: pathlib.Path,
*,
allow_protected_descendants: bool = False,
) -> str:
"""Return a block reason when candidate is not an external user file."""
resolved = pathlib.Path(candidate).expanduser().resolve(strict=False)
home = _tool_access()._user_files_root()
outside_home = not _tool_access().path_is_relative_to(resolved, home) and not _tool_access()._path_is_relative_to_casefold(resolved, home)
# External-workspace tasks may reach host scratch outside home (/tmp, /build,
# sibling checkouts). The runtime-overlap and credential guards BELOW still
# run on the full path, so the Ouroboros repo/data drive and secret-like
# files stay protected even when home confinement is lifted.
if outside_home and not _tool_access().is_external_workspace(ctx):
return f"path is outside user home {home}"
# The Ouroboros runtime/control surface is the system repo PLUS every data
# drive the task touches: the parent drive (ctx.drive_root) and any child /
# budget drive carried in task_metadata. External-workspace mode lifts home
# confinement, so these must be enumerated explicitly here — otherwise a
# child-drive control path (e.g. <child_drive>/memory) would slip through.
protected_values: list[Any] = [
getattr(ctx, "drive_root", None),
getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir", None),
]
meta = getattr(ctx, "task_metadata", {})
if isinstance(meta, dict):
for key in ("drive_root", "child_drive_root", "headless_child_drive_root", "budget_drive_root"):
if meta.get(key):
protected_values.append(meta.get(key))
protected_roots: list[pathlib.Path] = []
hard_protected_roots: list[pathlib.Path] = [] # the data/repo/budget drives THEMSELVES
for value in protected_values:
try:
root = pathlib.Path(value).resolve(strict=False)
except (OSError, TypeError, ValueError):
continue
protected_roots.append(root)
hard_protected_roots.append(root)
parent = root.parent.resolve(strict=False)
if root.name in {"repo", "data"} and _tool_access().path_is_relative_to(parent, home):
# The workspace PARENT is a SOFT boundary (keeps user_files out of ~/Ouroboros at large);
# it is deliberately NOT a hard root, so the Deliverables sibling under it stays allowed.
protected_roots.append(parent)
# The configured Deliverables container is an INTENDED user-output root, allowed past the
# workspace-overlap guard — but ONLY when it is a genuine sibling: a misconfigured
# OUROBOROS_DELIVERABLES_ROOT that overlaps or contains a HARD data/repo/budget drive must NOT
# open a bypass. The outside-home, credential, and hidden-name checks still apply regardless.
in_deliverables = False
try:
_deliverables = _tool_access()._deliverables_root()
_deliverables_safe = not any(
_tool_access().path_is_relative_to(_deliverables, pr) or _tool_access()._path_is_relative_to_casefold(_deliverables, pr)
or _tool_access().path_is_relative_to(pr, _deliverables) or _tool_access()._path_is_relative_to_casefold(pr, _deliverables)
for pr in hard_protected_roots
)
if _deliverables_safe and (
_tool_access().path_is_relative_to(resolved, _deliverables) or _tool_access()._path_is_relative_to_casefold(resolved, _deliverables)
):
in_deliverables = True
except Exception:
in_deliverables = False
if not in_deliverables:
for protected in protected_roots:
overlaps_protected = _tool_access().path_is_relative_to(resolved, protected) or _tool_access()._path_is_relative_to_casefold(resolved, protected)
contains_protected = _tool_access().path_is_relative_to(protected, resolved) or _tool_access()._path_is_relative_to_casefold(protected, resolved)
if overlaps_protected or (
not allow_protected_descendants and contains_protected
):
# Name the root that ACTUALLY contains the target (the v6.54.3
# shell_cwd_block_message lesson applied to this surface): the
# subagent-projects area lives under the SOFT ~/Ouroboros parent,
# so every coop-tree read used to get a message naming four roots
# that cannot reach it while omitting the one that can. MESSAGE
# ONLY — subagent_projects stays a read-only root (no user_files
# write carve-out), and a target inside a HARD drive never takes
# this branch.
projects_hint = _subagent_projects_read_hint(ctx, resolved, hard_protected_roots)
if projects_hint:
return projects_hint
return (
"path overlaps the Ouroboros repo/runtime workspace; use "
"root=active_workspace, root=task_drive, root=artifact_store, "
"or root=skill_payload instead"
)
try:
parts = resolved.relative_to(home).parts
except ValueError:
parts = resolved.parts
for part in parts:
if not part:
continue
part_lower = part.lower()
# v6.52.0 (P1): DEFAULT-DENY hidden (dot) components. Known secret/credential/VCS dirs
# are always blocked; ANY OTHER dotted component is blocked too UNLESS it is in the small
# benign allowlist (.github/.vscode/.idea/...). Benign project dotdirs become readable
# (the owner's goal) while the in-home dotfile space stays safe-by-default — an enumerated
# blocklist would leak credential stores like ~/.terraform.d, ~/.cargo, ~/.pip, etc.
if part_lower in _USER_FILES_SECRET_COMPONENTS:
return "path is hidden or credential-like (secret/credential directory)"
if part.startswith(".") and part_lower not in _USER_FILES_ALLOWED_DOTNAMES:
return "path is hidden or credential-like (non-allowlisted hidden component)"
name = resolved.name
name_lower = name.lower()
if (
name_lower in _USER_FILES_SECRET_NAMES
or _USER_FILES_SECRET_RE.search(name)
or name_lower.endswith((".key", ".pem", ".p12", ".pfx"))
):
return "path name is credential-like"
return ""
class UserFilesPathBlockedError(ValueError):
"""Typed user_files confinement refusal (a POLICY denial, not an I/O failure).
Subclasses ``ValueError`` so every existing generic handler keeps working;
the read-surface wrappers (read_file/list_files/search_code) render it with
the typed ``⚠️ USER_FILES_PATH_BLOCKED`` prefix so the outcome axis can
partition it into ``execution.policy_denials`` (v6.57.0) instead of the
generic ``error`` status that falsely degraded a shipped task to
``tool_failure`` (the submarine wave-3 incident)."""
def resolve_user_file_path(
ctx: Any,
path: str,
*,
allow_protected_descendants: bool = False,
allow_outside_home: bool = False,
) -> pathlib.Path:
"""Resolve a user_files path under the user's home and outside Ouroboros control-plane roots.
Absolute paths OUTSIDE the user_files home (and the Deliverables container) are
rejected EARLY with an actionable error instead of resolving to a foreign root
and failing later with an opaque ``relative_to`` crash (v6.54.3 — the TB2.1
``'/app' is not in the subpath of '/root'`` class). ``allow_outside_home=True``
(the ``query_code`` external-target caller) skips only this EARLY actionable
check; ``user_files_path_block_reason`` below remains the outside-home
AUTHORITY, and it permits outside-home only for external-workspace contexts —
the mode the documented query_code contract (benchmark ``/app``) runs in.
Neither flag expands authority: a non-external context could not reach
outside-home before this check existed either."""
raw_text = str(path or ".").strip() or "."
try:
raw = pathlib.Path(raw_text).expanduser()
except Exception:
# expanduser() raises RuntimeError for an unknown '~user'; leave it unexpanded —
# the '~' branch below maps it into the jail home (raw is only used elsewhere for
# absolute paths, where expanduser is a no-op anyway).
raw = pathlib.Path(raw_text)
home = _tool_access()._user_files_root()
# is_absolute_path_text gives consistent cross-platform absolute detection
# (drive-less "/x" roots and "C:\\x"/"\\\\unc" are all absolute) so Windows
# does not silently treat a rooted path as home-relative.
if _tool_access().is_absolute_path_text(raw_text):
candidate = raw.resolve(strict=False)
# External-workspace tasks legitimately reach host scratch outside home
# (/tmp, /build, sibling checkouts) — for them the generic
# user_files_path_block_reason below stays the authority, mirroring its
# own is_external_workspace carve-out.
if not allow_outside_home and not _tool_access().is_external_workspace(ctx):
home_resolved = home.resolve(strict=False)
# Case-insensitive-platform parity with the user_files_path_block_reason
# authority: a differently-cased safe home path must not be rejected
# early where the casefold-aware guard would accept it (review round 7).
inside_home = _tool_access().path_is_relative_to(candidate, home_resolved) or _tool_access()._path_is_relative_to_casefold(
candidate, home_resolved
)
inside_deliverables = False
if not inside_home:
try:
deliverables_resolved = _tool_access()._deliverables_root().resolve(strict=False)
inside_deliverables = _tool_access().path_is_relative_to(
candidate, deliverables_resolved
) or _tool_access()._path_is_relative_to_casefold(candidate, deliverables_resolved)
except (OSError, ValueError):
inside_deliverables = False
if not inside_home and not inside_deliverables:
raise UserFilesPathBlockedError(
"user_files path blocked: absolute path "
f"{raw_text!r} is outside the user_files home ({home_resolved}). "
"Use root='active_workspace' for workspace paths, or a "
"home-relative path (e.g. 'Desktop/file.txt') for user files."
)
elif raw_text.startswith("~"):
# '~' / '~user' must expand to the CONFIGURED user_files home (the jail), NOT the
# real OS home — otherwise OUROBOROS_USER_FILES_ROOT isolation is bypassed by a
# '~/...' path. The jail has a single home, so '~user/sub' maps to '<home>/sub'.
_after = raw_text[1:]
if _after[:1] in ("/", "\\"):
_rel = _after[1:]
elif "/" in _after or "\\" in _after:
_rel = _after.replace("\\", "/").split("/", 1)[1]
else:
_rel = "" # bare '~' or '~user' -> the home directory itself
candidate = (home / _tool_access().safe_relpath(_rel)).resolve(strict=False) if _rel else home.resolve(strict=False)
else:
# safe_relpath has already normalized any Windows backslash to a POSIX '/', so the
# directory test below is separator-correct on every platform.
rel = _tool_access().safe_relpath(raw_text)
home_candidate = home / rel
if "/" in rel.strip("/") or home_candidate.exists():
# An explicit placement (a path WITH a directory — Desktop/..., Downloads/..., a subdir)
# OR a bare name that ALREADY EXISTS under home (an existing file or directory such as
# `Desktop`) is honored under the owner home exactly as given. This keeps read/list/search
# of existing user files and directory names home-relative — only a genuinely NEW unnamed
# output is containerized.
candidate = home_candidate.resolve(strict=False)
else:
# A bare name with no directory that does NOT already exist under home is an unnamed NEW
# deliverable: route it into the visible Deliverables container instead of cluttering the
# home root (a later read of the same bare name resolves there too, staying consistent).
candidate = (_tool_access()._deliverables_root() / rel).resolve(strict=False)
reason = user_files_path_block_reason(
ctx,
candidate,
allow_protected_descendants=allow_protected_descendants,
)
if reason:
raise UserFilesPathBlockedError(f"user_files path blocked: {reason}")
return candidate

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,450 @@
"""Process/shell guard helpers: self-change tripwires, read-only inspection classification and light-mode repo snapshots.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import hashlib
import pathlib
import subprocess
from ouroboros.contracts.skill_payload_policy import SKILL_OWNER_STATE_STEMS
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from typing import Any
from typing import Dict
from typing import Optional
def _registry():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros.tools import registry
return registry
def _detect_runtime_mode_elevation(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script attempts to change ``OUROBOROS_RUNTIME_MODE``."""
has_save = "save_settings" in text_lower
has_mode_key = "ouroboros_runtime_mode" in text_lower
has_dotted_path = "ouroboros.config.save_settings" in text_lower
detected = (has_save and has_mode_key) or has_dotted_path
return _registry()._owner_control_mention_blocks(text_lower, detected, writeish)
_SUBAGENT_SHELL_SECRET_MARKERS = (
# Ouroboros owner secrets/control state. The relative form (no leading slash)
# closes the interpreter-string bypass (CW4, v6.34.0): the whole-command
# substring scan already catches "/data/settings.json" and "../../data/..",
# but a bare "data/settings.json" (e.g. python -c "open('data/settings.json')"
# from a workspace cwd) needs the slash-less marker too.
"/data/settings.json", "data/settings.json", "ouroboros/data/settings", "file1.txt",
# Universal credential/secret/control files (relative or absolute).
# ouroboros-update-tx.json is the managed-update tx marker (.git/…): owner
# control state, mirrored on .git/config. Subagent shell only — the
# authorized resolver is the MAIN agent and the supervisor/host writers go
# through supervisor.update_merge, so neither is affected (synthesis F3).
".env", ".git/config", ".git/credentials", "ouroboros-update-tx.json",
"credentials.json", "tokens.json",
"/.ssh/", ".ssh/", "id_rsa", "id_ed25519", ".netrc", ".npmrc", ".pgpass", ".aws/",
)
def _subagent_shell_targets_secret(cmd_path_lower: str) -> bool:
"""Deterministic guard: a shell command referencing Ouroboros secrets/credentials
or owner-control state (settings.json, ssh keys, token/credential files)."""
return any(marker in cmd_path_lower for marker in _SUBAGENT_SHELL_SECRET_MARKERS)
def _detect_mutative_toggle_self_change(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script/CLI attempts to change the owner-only mutative-subagents toggle."""
has_key = "ouroboros_allow_mutative_subagents" in text_lower
has_write = (
"save_settings" in text_lower
or "settings.json" in text_lower
or "/api/settings" in text_lower
or "settings set" in text_lower # `ouroboros settings set <key> <value>` CLI path
or "ouroboros.cli" in text_lower
)
return _registry()._owner_control_mention_blocks(text_lower, has_key and has_write, writeish)
def _detect_evolution_owner_control_self_change(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script/CLI attempts to set the owner-only self-evolution controls:
the post-task evolution toggle OR the persistent evolution-objective steer (which
biases every evolution campaign, so it is owner-only like the toggle)."""
has_key = (
"ouroboros_post_task_evolution" in text_lower
or "ouroboros_evolution_persistent_objective" in text_lower
)
has_write = (
"save_settings" in text_lower
or "settings.json" in text_lower
or "/api/settings" in text_lower
or "settings set" in text_lower
or "ouroboros.cli" in text_lower
)
return _registry()._owner_control_mention_blocks(text_lower, has_key and has_write, writeish)
def _detect_context_mode_self_lowering(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script attempts to lower the owner-controlled context mode."""
mentions_context_key = "ouroboros_context_mode" in text_lower
mentions_owner_endpoint = "/api/owner/context-mode" in text_lower
mentions_context_endpoint = "context-mode" in text_lower and "/api/owner" in text_lower
mentions_context_cli = "context-mode" in text_lower and (
"ouroboros settings" in text_lower
or "ouroboros.cli" in text_lower
)
mentions_save = "save_settings" in text_lower or "settings.json" in text_lower
mentions_owner_lowering_flag = "allow_context_lowering" in text_lower
detected = (
mentions_owner_endpoint
or mentions_context_endpoint
or mentions_context_cli
or mentions_owner_lowering_flag
or (mentions_context_key and mentions_save)
)
return _registry()._owner_control_mention_blocks(text_lower, detected, writeish)
_READ_ONLY_INSPECTION_COMMANDS = frozenset({
"grep", "egrep", "fgrep", "zgrep", "rg", "ag", "ack", "ripgrep",
"cat", "bat", "head", "tail", "less", "more", "nl", "strings",
"ls", "find", "fd", "stat", "file", "wc", "sort", "uniq", "cut", "tr", "column",
"basename", "dirname", "realpath", "readlink", "diff", "cmp", "jq", "yq",
"echo", "printf", "true", "pwd", "date", "tree",
})
_COMMAND_HEAD_WRAPPERS = frozenset({
"sudo", "env", "command", "builtin", "exec", "nohup", "time", "nice", "ionice",
"stdbuf", "\\",
})
_READ_ONLY_GIT_SUBCOMMANDS = frozenset({
"grep", "log", "show", "diff", "blame", "cat-file", "ls-files", "ls-tree",
"rev-parse", "status", "describe",
})
_SEARCH_TOOL_EXEC_OPTIONS = frozenset({"--pre", "--pre-glob", "--hostname-bin", "--pager"})
_DENIED_READ_OPTIONS: dict = {
# find/fd run and delete: -exec/-execdir/-ok/-okdir/-x, -delete, and the -f* writers.
"find": frozenset({
"-exec", "-execdir", "-ok", "-okdir", "-delete",
"-fls", "-fprint", "-fprint0", "-fprintf",
}),
"fd": frozenset({"-x", "--exec", "--exec-batch"}),
"rg": _SEARCH_TOOL_EXEC_OPTIONS,
"ripgrep": _SEARCH_TOOL_EXEC_OPTIONS,
"ag": _SEARCH_TOOL_EXEC_OPTIONS,
"ack": _SEARCH_TOOL_EXEC_OPTIONS,
# yq edits the named file in place with -i/--inplace; without this the family
# read-carve exempted `yq -i '.OUROBOROS_SAFETY_MODE="off"' settings.json` as
# "pure inspection" (jq has no in-place edit and stays a stdout-only read).
"yq": frozenset({"-i", "--inplace"}),
"sort": frozenset({"-o", "--output", "--compress-program"}),
"less": frozenset({"-o", "--log-file", "-k", "--lesskey-file"}),
"more": frozenset({"-o"}),
"file": frozenset({"-c", "--compile"}),
# git: external diff/textconv helpers execute a configured program, -o/--output and
# git grep -O write or spawn a pager, --exec-path relocates the git binaries.
"git": frozenset({
"-c", "--config-env", "--exec-path", "--ext-diff", "--textconv",
"-o", "--output", "--open-files-in-pager",
}),
}
_TRUSTED_EXECUTABLE_DIRS = frozenset({
"/bin", "/usr/bin", "/usr/local/bin", "/sbin", "/usr/sbin", "/opt/homebrew/bin",
})
def _trusted_read_head(token: str) -> str:
"""The allowlist-comparable command name, or "" when the executable is untrusted."""
if "\\" in token:
return "" # a windows/escaped path is not a form we can resolve — fail closed
directory, sep, name = token.rpartition("/")
if sep and directory not in _TRUSTED_EXECUTABLE_DIRS:
return ""
return name.removesuffix(".exe")
def _denied_read_option(token: str, denied: frozenset) -> bool:
"""True when an argument spells an execution/mutation option of its command."""
if not token.startswith("-") or token in {"-", "--"}:
return False
name = token.split("=", 1)[0]
if name in denied:
return True
if name.startswith("--"):
return False
return any(f"-{letter}" in denied for letter in name[1:]) # bundled short cluster
_NESTED_EXECUTION_MARKERS = ("$(", "`", "<(", ">(")
_NESTED_EXECUTION_TOKENS = frozenset({"$", "(", ")", "<(", ">(", "$("})
def _is_pure_read_inspection(text_lower: str) -> bool:
"""True when EVERY command in a shell line is a read-only source inspection.
Structural, not keyword-based: the line is split into per-command segments with
the shared lexer (``shell_parse.shell_segments``) and each segment's HEAD is
matched against an allowlist. An unknown head — any interpreter, HTTP client,
or shell — is not an inspection, whatever flags or payload spelling it carries.
Head membership is NECESSARY, NOT SUFFICIENT (review round 2): an allowed head can
still execute through its own options (``find -exec``, ``rg --pre``, git's external
diff/textconv) or through what precedes it. So the options are validated per command
(``_DENIED_READ_OPTIONS``), a leading environment assignment is REFUSED rather than
dropped (``PATH=``/``LD_PRELOAD=``/``GIT_EXTERNAL_DIFF=`` change what actually runs),
wrappers may not carry their own flags (``env -i``, ``sudo -e``), and the executable
must resolve to a bare name or a system bin. Anything unrecognised stays fail-closed.
NESTED EXECUTION IS REFUSED BEFORE ANY OF THAT (review round 3). Only the heads the lexer
actually surfaces get validated, so a command substitution hid its command from every check
above: ``echo "$(curl -X POST .../api/owner/scope-review-floor)"`` presented the allowlisted
``echo``, and the write-shape detector does not recognise an HTTP POST, so the exemption was
granted to a line that existed to reach the owner-only endpoint. A quoted substitution is
one opaque argument token to the lexer, which is why this is a check on the TEXT and on the
tokens, not something the per-segment head walk could have caught.
"""
from ouroboros.shell_parse import shell_segments
if any(marker in text_lower for marker in _NESTED_EXECUTION_MARKERS):
return False
segments = shell_segments(text_lower)
if not segments:
return False
for segment in segments:
if any(token in _NESTED_EXECUTION_TOKENS for token in segment):
return False
tokens = [token for token in segment if token]
while tokens and tokens[0] in _COMMAND_HEAD_WRAPPERS:
tokens = tokens[1:]
if tokens and tokens[0].startswith("-"):
return False # a wrapper's own options can rebuild the environment
if not tokens:
continue # a bare wrapper executes nothing
if "=" in tokens[0] and not tokens[0].startswith(("-", "=")):
return False # leading env assignment: never silently discarded
head = _trusted_read_head(tokens[0])
if head == "git":
if len(tokens) < 2 or tokens[1] not in _READ_ONLY_GIT_SUBCOMMANDS:
return False
elif not head or head not in _READ_ONLY_INSPECTION_COMMANDS:
return False
denied = _DENIED_READ_OPTIONS.get(head)
if denied and any(_denied_read_option(token, denied) for token in tokens[1:]):
return False
if head == "uniq" and sum(1 for t in tokens[1:] if t == "-" or not t.startswith("-")) >= 2:
# uniq's SECOND positional operand is its output file ('-' is the
# stdin operand, not a flag): `... | uniq - settings.json` writes.
return False
return True
def _detect_scope_review_floor_self_lowering(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script attempts to REACH the owner-controlled scope-review floor
(CW1, v6.34.0). ``OUROBOROS_SCOPE_REVIEW_FLOOR`` is deprecated and enforcement-inert
since v6.80.0 (scope-review applicability follows the owner context mode), but it is
still an owner-only stored setting behind its dedicated audited endpoint, so the agent
must not write it through any channel. Mirrors the context-mode guard.
POLARITY (v6.80.0): naming the owner endpoint or the floor key in a settings context
is blocked UNLESS the whole command line is demonstrably read-only inspection
(``_is_pure_read_inspection``). The earlier shape — block only on a listed HTTP write
marker — failed OPEN: ``python -c "httpx.request('POST', '.../api/owner/
scope-review-floor', ...)"`` names the endpoint, matches no marker, and mutated the
setting. No substring enumeration of "what a write looks like" is ever complete
(BIBLE P5), so the enumeration was inverted to "what a read looks like", where an
unrecognised entry is refused rather than admitted.
Pure source inspection stays allowed: ``grep OUROBOROS_SCOPE_REVIEW_FLOOR
data/settings.json`` and ``rg '/api/owner/scope-review-floor' ouroboros/gateway``
read and do not act. ``writeish`` is the shell guard's own already-computed
write-shape fact (redirects, writer commands, write-mode ``open()``); it disqualifies
the read exemption, and its default ``True`` keeps a caller that cannot supply the
fact fail-closed."""
import urllib.parse
decoded = urllib.parse.unquote(urllib.parse.unquote(text_lower)).lower()
text = f"{text_lower} {decoded}"
mentions_floor_key = "ouroboros_scope_review_floor" in text
mentions_owner_endpoint = "/api/owner/scope-review-floor" in text
mentions_floor_endpoint = "scope-review-floor" in text and "/api/owner" in text
mentions_floor_cli = "scope-review-floor" in text and (
"ouroboros settings" in text
or "ouroboros.cli" in text
)
mentions_save = "save_settings" in text or "settings.json" in text or "/api/settings" in text
reaches_floor = (
mentions_owner_endpoint
or mentions_floor_endpoint
or mentions_floor_cli
or (mentions_floor_key and mentions_save)
)
if not reaches_floor:
return False
return writeish or not _is_pure_read_inspection(text_lower)
def _detect_safety_mode_self_lowering(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect shell/script attempts to lower the owner-controlled LLM-safety coverage
(v6.54.3). OUROBOROS_SAFETY_MODE gates the LLM safety supervisor layer; the agent
must not switch it to ``light``/``off`` to remove its own supervision friction
(BIBLE P3). Mirrors the context-mode/scope-floor self-lowering guards."""
import urllib.parse
# URL-decode so a percent-encoded endpoint (curl .../safety%2Dmode) cannot
# slip the literal scan (review round 6; mirrors the attest-endpoint guard).
decoded = urllib.parse.unquote(urllib.parse.unquote(text_lower)).lower()
text = f"{text_lower} {decoded}"
mentions_mode_key = "ouroboros_safety_mode" in text
mentions_owner_endpoint = "/api/owner/safety-mode" in text
mentions_mode_endpoint = "safety-mode" in text and "/api/owner" in text
mentions_mode_cli = "safety-mode" in text and (
"ouroboros settings" in text
or "ouroboros.cli" in text
)
mentions_save = "save_settings" in text or "settings.json" in text or "/api/settings" in text
detected = (
mentions_owner_endpoint
or mentions_mode_endpoint
or mentions_mode_cli
or (mentions_mode_key and mentions_save)
)
return _registry()._owner_control_mention_blocks(text_lower, detected, writeish)
def _detect_owner_skill_attest_self_call(text_lower: str, *, writeish: bool = True) -> bool:
"""Detect agent attempts to loopback-call the OWNER-ONLY skill owner-attestation endpoint
(C1, v6.39). Owner-attestation skips the expensive LLM skill review; it MUST be
owner-issued, never agent self-callable — otherwise the agent could self-bypass the
immune system's review for its own skill. Mirrors the context-mode/scope-floor guards.
URL-DECODE first so a percent-encoded path (e.g. ``%61ttest-review`` / ``attest%2Dreview``)
— which Starlette decodes back to ``attest-review`` before routing — cannot slip past the
literal match (decode twice to catch double-encoding)."""
import urllib.parse
decoded = urllib.parse.unquote(urllib.parse.unquote(text_lower)).lower()
text = f"{text_lower} {decoded}"
detected = "/api/owner/skills/" in text and "attest-review" in text
return _registry()._owner_control_mention_blocks(text_lower, detected, writeish)
_SKILL_OWNER_STATE_STEMS = SKILL_OWNER_STATE_STEMS
_DETACHED_PROCESS_MARKERS = ("start_new_session", "new_session", "setsid", "preexec_fn", "nohup")
def _mentions_skill_owner_state(text_lower: str) -> bool:
if "state" not in text_lower or "skills" not in text_lower:
return False
for stem in _SKILL_OWNER_STATE_STEMS:
if f"{stem}.json" in text_lower:
return True
if stem in text_lower and ".json" in text_lower:
return True
return False
def _mentions_detached_process(text_lower: str) -> bool:
return any(marker in text_lower for marker in _DETACHED_PROCESS_MARKERS)
def _light_repo_snapshot(repo_dir: pathlib.Path) -> Optional[Dict[str, Any]]:
"""Worktree tripwire for light-mode shell writes, not rollback machinery."""
try:
repo = pathlib.Path(repo_dir)
status = subprocess.run(
["git", "status", "--porcelain=v1", "--untracked-files=all"],
cwd=str(repo), capture_output=True, text=True, timeout=5,
)
if status.returncode != 0:
return None
unstaged = subprocess.run(
["git", "diff", "--binary", "--no-ext-diff"],
cwd=str(repo), capture_output=True, text=True, timeout=10,
)
staged = subprocess.run(
["git", "diff", "--cached", "--binary", "--no-ext-diff"],
cwd=str(repo), capture_output=True, text=True, timeout=10,
)
paths = _registry().parse_porcelain_paths(status.stdout)
digest = hashlib.sha256()
digest.update((status.stdout or "").encode("utf-8", errors="replace"))
digest.update((unstaged.stdout if unstaged.returncode == 0 else "").encode("utf-8", errors="replace"))
digest.update((staged.stdout if staged.returncode == 0 else "").encode("utf-8", errors="replace"))
for rel in paths:
try:
target = (repo / _registry().safe_relpath(rel)).resolve(strict=False)
target.relative_to(repo.resolve(strict=False))
if target.is_file() and rel in (status.stdout or ""):
stat = target.stat()
digest.update(f"{rel}\0{stat.st_size}\0{stat.st_mtime_ns}".encode("utf-8"))
except Exception:
continue
return {"digest": digest.hexdigest(), "paths": paths}
except Exception:
return None
def _format_light_repo_write_block(before: Dict[str, Any], after: Dict[str, Any], result: str, tool_name: str = "run_command") -> str:
before_paths = set(before.get("paths") or [])
after_paths = set(after.get("paths") or [])
touched = sorted(after_paths | before_paths)
listed = ", ".join(touched[:30]) if touched else "(status changed; no paths parsed)"
if len(touched) > 30:
listed += f", ... (+{len(touched) - 30} more)"
return (
"⚠️ LIGHT_MODE_REPO_WRITE_BLOCKED: runtime_mode=light detected "
f"a mutation of the Ouroboros repository after {tool_name}. "
"The command result is blocked and no automatic rollback was attempted "
"to avoid overwriting concurrent human edits. "
f"Affected/dirty paths: {listed}. Switch to advanced/pro for repo writes.\n\n"
"Original command output:\n"
f"{result}"
)
def _git_ref_snapshot(repo_dir: pathlib.Path) -> Optional[Dict[str, str]]:
try:
repo = pathlib.Path(repo_dir)
head = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=str(repo), capture_output=True, text=True, timeout=5,
)
refs = subprocess.run(
["git", "show-ref", "--head", "--dereference"],
cwd=str(repo), capture_output=True, text=True, timeout=5,
)
if head.returncode != 0 or refs.returncode not in (0, 1):
return None
digest = hashlib.sha256()
digest.update((head.stdout or "").encode("utf-8", errors="replace"))
digest.update((refs.stdout or "").encode("utf-8", errors="replace"))
return {"head": (head.stdout or "").strip(), "digest": digest.hexdigest()}
except Exception:
return None

View file

@ -0,0 +1,418 @@
"""Host-owned pre-dispatch guards: capability/resource, ephemeral, managed-update and skill-payload constraints.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import logging
import os
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from ouroboros.contracts.task_constraint import TaskConstraint
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
# The logger name is pinned to the parent's literal namespace so the
# extraction does not silently rename the log stream.
log = logging.getLogger("ouroboros.tools.registry")
def _registry():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros.tools import registry
return registry
def _executor_backend_candidate_allowed(ctx: Any, candidate: str, allowed_roots: List[pathlib.Path]) -> bool:
try:
from ouroboros.workspace_executor import executor_ref_from_ctx as _executor_ref_from_ctx
from ouroboros.workspace_executor import map_backend_path as _executor_map_backend_path
executor_ref = _executor_ref_from_ctx(ctx)
if executor_ref is None:
return False
resolved = _executor_map_backend_path(executor_ref, candidate)
return any(
resolved.is_relative_to(root) or _registry()._path_is_relative_to_casefold(resolved, root)
for root in allowed_roots
)
except Exception:
return False
def _command_mentions_protected_root(cmd_path_lower: str, root_text: str) -> bool:
"""Boundary-aware path containment for the workspace shell guard.
True only when ``root_text`` (a normalised, lower-cased protected root path)
appears in the command as a whole path or a parent prefix at a real path
boundary — NOT as an incidental substring of an unrelated path that merely
shares the prefix (e.g. protected ``/x/data`` must not match ``/x/database``).
Used as a coarse catch-all for runtime paths embedded in non-tokenised text
(e.g. inside a ``python -c`` string); the precise per-token containment loop
still does the authoritative active/protected classification.
"""
if not root_text:
return False
norm = root_text.rstrip("/")
if not norm:
return False
span = len(norm)
limit = len(cmd_path_lower)
start = 0
while True:
idx = cmd_path_lower.find(norm, start)
if idx < 0:
return False
end = idx + span
nxt = cmd_path_lower[end] if end < limit else ""
# Boundary = end-of-string, a path separator (child path), or a shell
# token delimiter (the exact path). A trailing path char (letter/digit/
# ``.``/``-``/``_``) means a DIFFERENT sibling path → keep scanning.
if nxt == "" or nxt == "/" or nxt in " \t\"')(;:,&|<>":
return True
start = end
def _stray_skill_payload_failsoft(root_arg: str, workspace_mode: bool, task_constraint: Any) -> bool:
"""Whether stray bucket/skill_name on a write tool should be DROPPED rather than
surfaced as SKILL_PAYLOAD_ARG_ERROR. Fail-soft ONLY for a WORKSPACE edit that is
NOT skill-authoring: there bucket/skill_name are model noise (the B2 footgun —
reflexive bucket="external" on an /app edit). In light/advanced non-workspace
skill-authoring (or an explicit root=skill_payload / skill_repair) the specific
error is the intended helpful signal."""
skill_payload_intent = root_arg == "skill_payload" or bool(
task_constraint and getattr(task_constraint, "mode", "") == "skill_repair"
)
return bool(workspace_mode and not skill_payload_intent)
def _managed_update_code_tool_block(ctx: Any, name: str) -> str:
"""Block a repo-mutating code tool while a managed-update assisted merge is staged for
ANOTHER task (P2/SC2). Returns a block message, or "" when allowed (this is the authorized
resolution task, or no managed tx is active). A corrupt tx marker fails closed."""
try:
from supervisor.update_merge import managed_assisted_tx_for
if managed_assisted_tx_for(
getattr(ctx, "task_id", ""),
getattr(ctx, "task_metadata", None),
)[1]:
return (
f"⚠️ MANAGED_UPDATE_IN_PROGRESS: {name!r} is blocked while a managed update merge "
"is being resolved (only its authorized resolution task may write the repo). "
"Retry after the update lands or is rolled back."
)
except Exception:
return (
f"⚠️ MANAGED_UPDATE_STATE_UNAVAILABLE: {name!r} is blocked because the managed "
"update transaction state could not be verified. Retry after the update state is "
"available or repaired."
)
return ""
def _authorized_managed_update_resolver(ctx: Any) -> bool:
"""Whether this task is the durable tx-authorized assisted resolver.
Fail-closed bool for every authority consumer (False = no extra powers).
The AUTHORITY-READ failure is additionally distinguished from an honest
"not the resolver" via a typed ctx marker (``_managed_authority_read_error``:
set on an unreadable read AND on a corrupt tx marker, cleared on every
healthy evaluation), so the review-subject builder can fail LOUDLY instead
of silently reviewing a possibly-managed candidate as an ordinary full
staged capture."""
try:
from supervisor.update_merge import authorized_assisted_task_strict
marker_status, tx = authorized_assisted_task_strict(
getattr(ctx, "task_id", ""),
getattr(ctx, "task_metadata", None),
)
try:
if marker_status == "corrupt":
# A tx marker EXISTS but cannot be parsed: authority stays
# False (fail-closed) for every bool consumer, but the loud
# A4 channel must fire — clearing the marker here would let
# the review subject silently treat a possibly-managed
# candidate as an ordinary full staged diff.
setattr(
ctx, "_managed_authority_read_error",
"update_tx_corrupt: the managed update transaction marker "
"exists but could not be parsed",
)
else:
setattr(ctx, "_managed_authority_read_error", "")
except Exception:
pass
return bool(tx)
except Exception as exc:
try:
setattr(ctx, "_managed_authority_read_error", repr(exc))
except Exception:
pass
return False
def _task_constraint_path_allowed(path_text: str, constraint: Optional[TaskConstraint], drive_root: pathlib.Path) -> bool:
return _registry().is_skill_payload_path(
drive_root,
path_text or "",
constraint=constraint,
allow_short_relative=True,
allow_control_plane=True,
)
def _light_mode_payload_mutation_allowed(
*,
ctx: Any,
tool_name: str,
args: Dict[str, Any],
runtime_mode: str,
effective_constraint: Optional[TaskConstraint],
implicit_skill_cwd_allowed: bool,
allow_short_relative: bool,
) -> bool:
"""Return True for light-mode data skill payload edits that do not touch repo files."""
# apply_patch/edit_batch are DELIBERATELY absent: they refuse data-plane roots
# entirely (repo lanes only), so they can never be a payload edit — in light
# mode they stay under the generic repo-mutation block like any repo write.
if runtime_mode != "light" or tool_name not in {"edit_text", "write_file"}:
return False
requested_root = str(args.get("root", "") or "active_workspace")
try:
requested_root = _registry().normalize_root(requested_root)
except Exception:
requested_root = str(args.get("root", "") or "active_workspace")
if requested_root in {"task_drive", "artifact_store", "user_files"}:
return True
legacy_data_skill_edit = False
if tool_name == "edit_text" and requested_root == "active_workspace":
try:
legacy_target = _registry().resolve_skill_payload_target(
pathlib.Path(ctx.drive_root),
str(args.get("path", "") or ""),
)
legacy_data_skill_edit = legacy_target.target_path.exists() and not legacy_target.control_plane
except Exception:
legacy_data_skill_edit = False
if requested_root not in {"runtime_data", "skill_payload"} and not legacy_data_skill_edit:
return False
return _registry().is_skill_payload_path(
pathlib.Path(ctx.drive_root),
str(args.get("path", "") or ""),
constraint=effective_constraint,
allow_short_relative=allow_short_relative,
allow_control_plane=False,
)
_HEAL_MODE_ALLOWED_TOOLS = frozenset({
"read_file",
"list_files",
"write_file",
"edit_text",
"list_skills",
"skill_review", "skill_preflight",
})
def _heal_protected_payload_sidecar(path_text: str) -> bool:
return _registry().is_skill_payload_control_filename(path_text)
_WEB_TOOLS = frozenset({"web_search", "browse_page", "browser_action", "youtube_transcript"})
def _resource_allowed(ctx: Any, key: str) -> bool:
metadata = getattr(ctx, "task_metadata", {}) if isinstance(getattr(ctx, "task_metadata", {}), dict) else {}
contract = metadata.get("task_contract") if isinstance(metadata.get("task_contract"), dict) else {}
if not contract and isinstance(getattr(ctx, "task_contract", None), dict):
contract = getattr(ctx, "task_contract")
resources = {}
for source in (metadata, contract):
raw = source.get("allowed_resources") if isinstance(source, dict) else None
if isinstance(raw, dict):
resources.update(raw)
if not resources:
return True
for name in (key, f"allow_{key}"):
value = resources.get(name)
if isinstance(value, bool):
return value
if key == "web":
for name in ("network", "allow_network", "internet", "external_network"):
value = resources.get(name)
if isinstance(value, bool) and not value:
return False
if key == "network":
for name in ("web", "allow_web", "internet", "external_network"):
value = resources.get(name)
if isinstance(value, bool) and not value:
return False
return True
def _disabled_tools(ctx: Any) -> frozenset:
"""Tool names the task contract withholds (declarative tool policy).
Independent of ``allowed_resources``: a caller can disable specific tools
(e.g. the agent's web_search/browser/VLM tools for a faithful benchmark)
WITHOUT setting web/network=false — so shell network egress (git/pip) stays
available and the web<->network cross-implication in ``_resource_allowed``
never fires.
"""
metadata = getattr(ctx, "task_metadata", {}) if isinstance(getattr(ctx, "task_metadata", {}), dict) else {}
contract = metadata.get("task_contract") if isinstance(metadata.get("task_contract"), dict) else {}
if not contract and isinstance(getattr(ctx, "task_contract", None), dict):
contract = getattr(ctx, "task_contract")
names: set = set()
for source in (metadata, contract):
raw = source.get("disabled_tools") if isinstance(source, dict) else None
if isinstance(raw, (list, tuple)):
names.update(str(n).strip() for n in raw if str(n).strip())
# D10 compatibility: `claude_code_edit` was retired; saved contracts that
# withheld the external coding gateway keep withholding its SUCCESSOR — the
# delegated coding session's start verb. The dead name stays in the set
# too (harmless: nothing registers it), so old contracts round-trip as-is.
if "claude_code_edit" in names:
names.add("delegate_start")
# Q1 rename compatibility: contracts that withheld `advisory_review` keep
# withholding the SAME organ under its new name, and vice versa (a new
# contract naming only the new spelling must also silence the alias).
if "advisory_review" in names:
names.add("preflight_review")
if "preflight_review" in names:
names.add("advisory_review")
return frozenset(names)
_GITHUB_TOKEN_TOOLS = frozenset({
"list_github_prs",
"get_github_pr",
"comment_on_pr",
"list_github_issues",
"get_github_issue",
"comment_on_issue",
"close_github_issue",
"create_github_issue",
"run_ci_tests",
"submit_skill_to_hub",
"generate_evolution_stats",
})
def _builtin_tool_availability(name: str, ctx: Any = None) -> tuple[bool, str, str]:
"""Return ``(available, reason, detail)`` for built-in tool credential gates.
Predicates are lazy to avoid registry import cycles and discovery-time side effects.
"""
# A bare registry (unit tests, static policy inventory, import-time introspection)
# is a structural surface, not a running task capability envelope.
if not str(getattr(ctx, "task_id", "") or "").strip():
metadata = getattr(ctx, "task_metadata", {}) if ctx is not None else {}
contract = getattr(ctx, "task_contract", {}) if ctx is not None else {}
if not metadata and not contract:
return True, "", ""
tool = str(name or "").strip()
if tool == "web_search":
try:
from ouroboros.tools.search import _available_web_search_backends
if not _available_web_search_backends():
return False, "missing_credential", "web_search_backend"
except ImportError:
return True, "", ""
except Exception:
return True, "", ""
if tool in _GITHUB_TOKEN_TOOLS and not os.environ.get("GITHUB_TOKEN", "").strip():
return False, "missing_credential", "GITHUB_TOKEN"
return True, "", ""
def _payload_dispatch_constraint(
ctx: Any,
*,
name: str,
args: dict[str, Any],
task_constraint: Optional[TaskConstraint],
workspace_mode: bool,
) -> tuple[Optional[TaskConstraint], str]:
"""Preserve repair selectors without letting stray selectors retarget work."""
raw_bucket = str(args.get("bucket", "") or "")
raw_skill_name = str(args.get("skill_name", "") or "")
explicit_skill_root = str(args.get("root", "") or "").strip().lower() == "skill_payload"
short_form_decision = None if explicit_skill_root else _registry().decide_payload_short_form(
bucket=raw_bucket,
skill_name=raw_skill_name,
path_text=str(args.get("path", "") or "."),
repo_dir=pathlib.Path(ctx.repo_dir),
drive_root=pathlib.Path(ctx.drive_root),
)
if explicit_skill_root:
# Binding selection already handled the explicit target. This legacy
# constraint exists only for the light-mode data-payload carve-out.
synthesized = _registry().synthesize_payload_constraint(raw_bucket, raw_skill_name)
else:
synthesized = (
short_form_decision.constraint
if short_form_decision is not None
and task_constraint
and task_constraint.mode == "skill_repair"
else None
)
if (
(raw_bucket or raw_skill_name)
and short_form_decision is not None
and short_form_decision.error
and name in {"write_file", "edit_text"}
):
root_arg = str(args.get("root", "") or "").strip().lower()
if _stray_skill_payload_failsoft(root_arg, workspace_mode, task_constraint):
log.info(
"Ignoring stray bucket/skill_name on %s (workspace edit, root=%s): %s",
name,
root_arg or "active_workspace",
short_form_decision.error[:80],
)
args.pop("bucket", None)
args.pop("skill_name", None)
synthesized = None
else:
return None, f"⚠️ SKILL_PAYLOAD_ARG_ERROR: {short_form_decision.error}"
redirect_err = _registry().cross_skill_redirect_error(task_constraint, synthesized)
if redirect_err and name in {"write_file", "edit_text"}:
return None, f"⚠️ SKILL_REDIRECT_BLOCKED: {redirect_err}"
if task_constraint and task_constraint.mode == "skill_repair":
return task_constraint, ""
return synthesized or task_constraint, ""
_EPHEMERAL_ALLOWED_TOOLS = frozenset({
# read / inspect
"read_file", "query_code", "search_code", "list_files", "web_search", "browse_page",
"chat_history", "recent_tasks", "get_task_result", "vcs_diff", "vcs_status",
"analyze_screenshot", "vlm_query",
# decide / route / spawn-owner-task / reply
"route_to_project", "promote_chat_to_task", "steer_task", "list_projects", "send_photo",
})

View file

@ -0,0 +1,53 @@
"""The immutable first-party tool descriptor (ToolEntry).
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from typing import Any
from typing import Callable
from typing import Dict
def _registry():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros.tools import registry
return registry
@dataclass
class ToolEntry:
"""Single tool descriptor."""
name: str
schema: Dict[str, Any]
handler: Callable # fn(ctx: ToolContext, **args) -> str
is_code_tool: bool = False
timeout_sec: int = 360
# Capability flag: tool can mutate the live repo worktree. The dispatcher
# snapshots `git status --porcelain` around flagged tools and invalidates
# advisory freshness when the worktree ACTUALLY changed — covering error
# and timeout paths uniformly, and never invalidating for read-only runs.
mutates_worktree: bool = False
# Compatibility alias: a renamed tool's old public name. An alias entry is
# CALLABLE (execute dispatches it like any entry) but never advertised —
# schemas()/available_tools() skip it, so the public surface carries only
# the canonical name while saved prompts, memories, and configs that still
# use the old spelling keep working.
alias_for: str = ""

View file

@ -0,0 +1,162 @@
"""Concrete per-task context shared by tool handlers and the registry facade.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import pathlib
from dataclasses import dataclass
from dataclasses import field
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from ouroboros.contracts.task_constraint import TaskConstraint
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
def _registry():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros.tools import registry
return registry
@dataclass
class BrowserState:
"""Per-task Playwright lifecycle state."""
pw_instance: Any = None
browser: Any = None
page: Any = None
last_screenshot_b64: Optional[str] = None
@dataclass
class ToolContext:
"""Tool execution context passed from the agent."""
repo_dir: pathlib.Path
drive_root: pathlib.Path
branch_dev: str = "ouroboros"
system_repo_dir: Optional[pathlib.Path] = None
workspace_root: Optional[pathlib.Path] = None
workspace_mode: str = ""
memory_mode: str = ""
budget_drive_root: str = ""
# Per-project facts scope (Phase 3b): when set, knowledge reads/writes target
# the per-project store under the canonical data dir instead of memory/knowledge.
project_id: str = ""
task_metadata: Dict[str, Any] = field(default_factory=dict)
executor_ref: Dict[str, Any] = field(default_factory=dict)
pending_events: List[Dict[str, Any]] = field(default_factory=list)
current_chat_id: Optional[int] = None
current_task_type: Optional[str] = None
pending_restart_reason: Optional[str] = None
last_push_succeeded: bool = False
last_reviewed_commit_sha: str = ""
emit_progress_fn: Callable[[str], None] = field(default=lambda _: None)
# LLM-driven model/effort switch.
active_model_override: Optional[str] = None
active_effort_override: Optional[str] = None
active_use_local_override: Optional[bool] = None
task_model_override: Optional[str] = None
task_use_local_override: Optional[bool] = None
# CW2 (v6.34.0): the loop publishes the effective context mode each round so
# switch_model can refuse switching to a sub-1M route while the transcript is max-sized.
active_context_mode: str = ""
# Per-task browser state.
browser_state: BrowserState = field(default_factory=BrowserState)
# Budget tracking for usage events.
event_queue: Optional[Any] = None
task_id: Optional[str] = None
# Conversation messages for safety checks.
messages: Optional[List[Dict[str, Any]]] = None
# Structured task constraints, e.g. skill repair payload confinement.
task_constraint: Optional[TaskConstraint] = None
task_contract: Dict[str, Any] = field(default_factory=dict)
# Task depth for fork-bomb protection.
task_depth: int = 0
# True inside handle_chat_direct, not a queued worker task.
is_direct_chat: bool = False
# CW3 (v6.34.0): a SHORT-LIVED same-route "decision" turn (run while the chat
# agent is busy). It may answer / route / spawn / steer, but is barred from
# durable cognitive-memory / evolution / settings / control-plane mutators
# (the WS10 ephemeral contract) — enforced in schemas()/execute().
is_ephemeral_turn: bool = False
# Pre-commit review state.
_review_advisory: List[Any] = field(default_factory=list)
_review_iteration_count: int = 0
_review_history: list = field(default_factory=list)
def active_repo_dir(self) -> pathlib.Path:
if self.is_workspace_mode():
return pathlib.Path(self.workspace_root)
return pathlib.Path(self.repo_dir)
def is_workspace_mode(self) -> bool:
return (
self.workspace_root is not None
and bool(str(self.workspace_mode or "").strip())
and not _registry().workspace_mode_block_reason(self)
)
def repo_path(self, rel: str) -> pathlib.Path:
root = self.active_repo_dir()
# Accept the paths an agent naturally writes against a workspace root:
# an absolute path already INSIDE the root (e.g. /app/out.txt under a
# workspace rooted at /app — otherwise re-nested as /app/app/out.txt) and
# a redundant root-basename prefix ('app/out.txt'). normalize_root_relative
# only ever returns a relative string; paths not under the root fall
# through to safe_relpath (kept inside) and the boundary check below.
rel_str = _registry().normalize_root_relative(root, str(rel))
resolved = (root / _registry().safe_relpath(rel_str)).resolve()
try:
resolved.relative_to(root.resolve())
except ValueError:
raise ValueError(f"Path escapes repo_dir boundary: {rel}")
return resolved
def drive_path(self, rel: str) -> pathlib.Path:
resolved = (self.drive_root / _registry().safe_relpath(rel)).resolve()
try:
resolved.relative_to(self.drive_root.resolve())
except ValueError:
raise ValueError(f"Path escapes drive_root boundary: {rel}")
return resolved
def drive_logs(self) -> pathlib.Path:
return (self.drive_root / "logs").resolve()
def task_drive_root(self) -> pathlib.Path:
return (pathlib.Path(self.drive_root).resolve(strict=False) / "task_drives" / _registry().task_id_for_artifacts(self)).resolve(strict=False)
def workspace_executor_ref(self) -> Dict[str, Any]:
if isinstance(self.executor_ref, dict) and self.executor_ref:
return dict(self.executor_ref)
if isinstance(self.task_metadata, dict) and isinstance(self.task_metadata.get("executor_ref"), dict):
return dict(self.task_metadata["executor_ref"])
return {}

View file

@ -0,0 +1,496 @@
"""Argument normalization and physical target binding for tool dispatch.
Every span is extracted VERBATIM from the parent's tip bytes by
scripts/v7next_transplant.py (D18/D33 module-handle split, proof-checked);
the parent re-exports every moved name, so historical imports and
monkeypatch targets keep working unchanged.
"""
from __future__ import annotations
import inspect
import os
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING: # annotation-only imports (inert at runtime)
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from ouroboros.tools.tool_catalog import ToolEntry
def _registry():
"""The parent module, read at call time.
The parent owns the rebindable module state and the members tests
monkeypatch there; reading them through the module at each call keeps
one binding, where a from-import would freeze the value this leaf saw
at import time (the owner-approved D18/D33 mechanical exception).
"""
from ouroboros.tools import registry
return registry
def _coerce_real_path(value: Any) -> pathlib.Path | None:
if value is None or value.__class__.__module__.startswith("unittest.mock"):
return None
try:
return pathlib.Path(os.fspath(value))
except TypeError:
return None
def active_repo_dir_for(ctx: Any) -> pathlib.Path:
"""Return the active repo/workspace root for real and lightweight test contexts."""
active = getattr(ctx, "active_repo_dir", None)
if callable(active):
try:
candidate = active()
except Exception:
candidate = None
path = _coerce_real_path(candidate)
if path is not None:
return path
workspace_root = getattr(ctx, "workspace_root", None)
workspace_path = _coerce_real_path(workspace_root)
if workspace_path is not None:
workspace_mode = str(getattr(ctx, "workspace_mode", "") or "").strip()
if workspace_mode:
return workspace_path
return pathlib.Path(getattr(ctx, "repo_dir"))
def system_repo_dir_for(ctx: Any) -> pathlib.Path:
"""Return the Ouroboros system repo root, not an external active workspace."""
return pathlib.Path(getattr(ctx, "system_repo_dir", None) or getattr(ctx, "repo_dir"))
_PATH_NORMALIZED_TOOLS = frozenset({"read_file", "write_file", "edit_text", "list_files", "search_code", "query_code"})
_ROOT_ARG_REPO_WRITE_TOOLS = frozenset({"write_file", "edit_text", "apply_patch", "edit_batch"})
def _payload_write_paths(name: str, args: Dict[str, Any]) -> List[str]:
"""Repo paths a write tool will touch, in the spelling its guards must judge.
write_file/edit_text carry `path`/`files[]` and were already canonicalized by
`_normalize_dispatch_path_args`. apply_patch addresses files inside the patch
text (`*** Update File: <path>`) and edit_batch inside `edits[]`, so their
paths reach this point RAW and are canonicalized here — otherwise a
protected-path gate reads `repo/BIBLE.md` (not a protected-table member)
while the write lands on `BIBLE.md`.
"""
paths: List[str] = []
if name == "write_file":
if isinstance(args.get("path"), str) and args["path"]:
paths.append(args["path"])
for entry in args.get("files") or []:
if isinstance(entry, dict) and isinstance(entry.get("path"), str):
paths.append(entry["path"])
elif name == "edit_text":
if isinstance(args.get("path"), str):
paths.append(args["path"])
elif name == "edit_batch":
for entry in args.get("edits") or []:
if isinstance(entry, dict) and isinstance(entry.get("path"), str):
paths.append(entry["path"])
elif name == "apply_patch":
# Derived from the REAL parser (lazy import: edit_ops imports this
# module), so the gate can never drift from what apply_patch will do.
# An unparseable patch yields no paths and is refused by the handler
# before any write, so the gate has nothing to miss.
from ouroboros.tools.edit_ops import patch_target_paths
paths.extend(patch_target_paths(str(args.get("patch") or "")))
return [p for p in paths if str(p or "").strip()]
def _normalize_dispatch_path_args(ctx: Any, name: str, args: Dict[str, Any]) -> str:
"""ROOT-FIX (v6.35.0): normalize an absolute / redundant-root-basename
active_workspace|system_repo path arg IN PLACE at the dispatch boundary, so
the handler AND every downstream guard (protected-path, protected-artifact,
accidental-truncation shrink guard) resolve the SAME target. One authoritative
normalization point is what makes a guard unable to desync from the operation.
v6.54.3 root-label fix: returns a dispatch note ("" when nothing rerouted).
When ``root='user_files'`` carries an ABSOLUTE path that resolves under the
ACTIVE WORKSPACE root, the root label is wrong, not the intent: reads
(read_file/list_files/search_code) are auto-routed to
``root='active_workspace'`` with a visible note appended AFTER the result
(trailing, so first-line failure classification is never masked),
and writes (write_file/edit_text) return an actionable
ROOT_REQUIRED_ACTIVE_WORKSPACE redirect instead of a generic access denial.
The destination root still passes every downstream gate (profile access
decision, protected-path guards, subagent filters) — only the label is
corrected, never the authority. ``query_code`` is excluded: its
root=user_files external-target contract handles absolute paths natively."""
if name not in _PATH_NORMALIZED_TOOLS:
return ""
root_arg = str(args.get("root") or "active_workspace")
if root_arg in ("active_workspace", "system_repo"):
try:
norm_root = active_repo_dir_for(ctx) if root_arg == "active_workspace" else system_repo_dir_for(ctx)
for _key in ("path", "dir"):
if isinstance(args.get(_key), str) and args[_key]:
args[_key] = _registry().normalize_root_relative(norm_root, args[_key])
if isinstance(args.get("files"), list):
for _f in args["files"]:
if isinstance(_f, dict) and isinstance(_f.get("path"), str) and _f["path"]:
_f["path"] = _registry().normalize_root_relative(norm_root, _f["path"])
except Exception:
pass
return ""
if root_arg != "user_files" or name == "query_code":
return ""
try:
workspace = pathlib.Path(active_repo_dir_for(ctx)).resolve(strict=False)
except Exception:
return ""
def _under_workspace(text: str) -> bool:
if not _registry().is_absolute_path_text(text):
return False
try:
pathlib.Path(text).expanduser().resolve(strict=False).relative_to(workspace)
return True
except (ValueError, OSError, RuntimeError):
return False
candidates: list[str] = []
for _key in ("path", "dir"):
if isinstance(args.get(_key), str) and args[_key]:
candidates.append(args[_key])
if isinstance(args.get("files"), list):
for _f in args["files"]:
if isinstance(_f, dict) and isinstance(_f.get("path"), str) and _f["path"]:
candidates.append(_f["path"])
hits = [text for text in candidates if _under_workspace(text)]
if not hits:
return ""
if name in ("write_file", "edit_text"):
return (
"⚠️ ROOT_REQUIRED_ACTIVE_WORKSPACE: absolute path "
f"{hits[0]!r} is under the active workspace, but root='user_files' does not "
"write there. Retry the same call with root='active_workspace' (the same "
"path is accepted)."
)
args["root"] = "active_workspace"
try:
for _key in ("path", "dir"):
if isinstance(args.get(_key), str) and args[_key]:
args[_key] = _registry().normalize_root_relative(workspace, args[_key])
if isinstance(args.get("files"), list):
for _f in args["files"]:
if isinstance(_f, dict) and isinstance(_f.get("path"), str) and _f["path"]:
_f["path"] = _registry().normalize_root_relative(workspace, _f["path"])
except Exception:
pass
return (
"⚠️ AUTO_ROUTED_TO_ACTIVE_WORKSPACE: absolute path "
f"{hits[0]!r} is under the active workspace; the call ran with "
"root='active_workspace'. Pass root='active_workspace' directly for "
"workspace paths."
)
_TOOL_ARG_ALIASES: dict[str, dict[str, str]] = {
"*": {"max_entries": "max_results"},
}
_IGNORE_ROOT_ARG_TOOLS = frozenset({
"commit_reviewed",
"vcs_commit_reviewed",
})
_GENERIC_VCS_TARGET_TOOLS = frozenset({
"vcs_status",
"vcs_diff",
"vcs_pull_ff",
"vcs_restore",
"vcs_revert",
})
_TARGET_BINDING_OPERATIONS = {
"read_file": "read",
"list_files": "list",
"search_code": "search",
"query_code": "search",
"write_file": "write",
"edit_text": "edit",
"apply_patch": "edit",
"edit_batch": "edit",
**{name: "vcs" for name in _GENERIC_VCS_TARGET_TOOLS},
}
_SKILL_LIFECYCLE_TARGET_TOOLS = frozenset({
"skill_review",
"skill_preflight",
"submit_skill_to_hub",
})
_PROCESS_TARGET_TOOLS = frozenset({"run_command", "run_script", "start_service"})
_VERIFY_RUN_KINDS = frozenset({
"visible_verifier",
"explicit_command",
"explicit_metric",
})
def _target_binding_operation(name: str, args: dict[str, Any]) -> str | None:
operation = _TARGET_BINDING_OPERATIONS.get(name)
if operation is not None:
return operation
if name in _SKILL_LIFECYCLE_TARGET_TOOLS:
return "review"
if name in _PROCESS_TARGET_TOOLS:
return "service" if name == "start_service" else "shell"
if name == "verify_and_record" and str(args.get("contract_kind") or "") in _VERIFY_RUN_KINDS:
return "shell"
# CONDITIONAL, never a static map entry (R1 item 1): delegate_start becomes
# target-bound only when it explicitly selects an exact skill payload; a
# plain or retry call keeps its current active-workspace behavior untouched.
# ONLY the known selector value binds here — any other root value falls
# through to the handler's TYPED unsupported_root refusal instead of an
# untyped ValueError from binding construction (gate fix 9).
if (name == "delegate_start"
and str(args.get("root") or "").strip() == "skill_payload"
and not str(args.get("retry_of") or "").strip()):
return "write"
return None
def _handler_public_params(handler: Callable[..., Any]) -> list[str]:
try:
params = list(inspect.signature(handler).parameters)
except (TypeError, ValueError):
return []
return [name for name in params if name not in {"ctx", "_resolved_binding"}]
def _entry_public_params(entry: "ToolEntry") -> list[str]:
try:
params = entry.schema.get("parameters") or {}
props = params.get("properties")
if isinstance(props, dict):
return [str(name) for name in props]
except Exception:
pass
return _handler_public_params(entry.handler)
def _entry_has_public_param_schema(entry: "ToolEntry") -> bool:
try:
params = entry.schema.get("parameters") or {}
return isinstance(params.get("properties"), dict)
except Exception:
return False
def _normalize_tool_call_args(entry: "ToolEntry", args: dict[str, Any]) -> None:
tool_name = entry.name
accepted = set(_entry_public_params(entry))
aliases: dict[str, str] = {}
aliases.update(_TOOL_ARG_ALIASES.get("*", {}))
aliases.update(_TOOL_ARG_ALIASES.get(tool_name, {}))
for alias, canonical in aliases.items():
if alias in args and canonical in accepted and alias not in accepted and canonical not in args:
args[canonical] = args.pop(alias)
if tool_name in _IGNORE_ROOT_ARG_TOOLS and "root" in args and "root" not in accepted:
args.pop("root", None)
def _prepare_public_builtin_args(entry: "ToolEntry", args: dict[str, Any]) -> str:
"""Normalize and validate only the model-visible builtin argument surface.
This runs after capability/lineage availability checks but before path
normalization, target selection, Python predispatch, or target-sensitive
guards. Private dispatch carriers therefore cannot be supplied by the model
and invalid public calls cannot trigger target work before rejection.
"""
_normalize_tool_call_args(entry, args)
public_params = set(_entry_public_params(entry))
# A handler may name a bounded set of execution-only legacy parameters. They
# remain absent from its model-visible schema and are therefore usable only by
# callers replaying the former wire shape through this real registry path. The
# handler still owns deterministic migration/refusal; this generic seam neither
# chooses a route nor special-cases a tool name.
hidden_legacy = {
str(name)
for name in (getattr(entry.handler, "_hidden_legacy_params", ()) or ())
if str(name)
}
accepted_params = public_params | hidden_legacy
if _entry_has_public_param_schema(entry) and any(key not in accepted_params for key in args):
return _format_tool_arg_error(entry)
try:
inspect.signature(entry.handler).bind(object(), **args)
except TypeError:
return _format_tool_arg_error(entry)
return ""
def _build_builtin_target_binding(ctx: Any, name: str, args: dict[str, Any]) -> Any:
"""Build the one private physical-target carrier for a builtin call."""
operation = _target_binding_operation(name, args)
if operation is None:
return None
if name in _SKILL_LIFECYCLE_TARGET_TOOLS:
return _registry().build_resolved_resource_binding(
ctx,
root="skill_payload",
operation="review",
path=".",
skill_name=str(args.get("skill") or ""),
)
if name in _PROCESS_TARGET_TOOLS or name == "verify_and_record":
return _registry().build_resolved_resource_binding(
ctx,
operation=operation,
process_cwd=str(args.get("cwd") or ""),
bucket=str(args.get("bucket") or ""),
skill_name=str(args.get("skill_name") or ""),
)
if name == "delegate_start":
return _registry().build_resolved_resource_binding(
ctx,
root=str(args.get("root") or ""),
operation="write",
path=".",
bucket=str(args.get("bucket") or ""),
skill_name=str(args.get("skill_name") or ""),
)
root = str(args.get("root") or "active_workspace")
bucket = str(args.get("bucket") or "")
skill_name = str(args.get("skill_name") or "")
def _one(path: str) -> Any:
return _registry().build_resolved_resource_binding(
ctx,
root=root,
operation=operation,
path=path or ".",
bucket=bucket,
skill_name=skill_name,
)
if name == "write_file" and args.get("files"):
return tuple(
_one(str(item.get("path") or ""))
for item in args.get("files") or []
if isinstance(item, dict)
)
if name == "apply_patch":
from ouroboros.tools.edit_ops import patch_target_paths
return tuple(_one(path) for path in patch_target_paths(str(args.get("patch") or "")))
if name == "edit_batch":
return tuple(
_one(str(item.get("path") or ""))
for item in args.get("edits") or []
if isinstance(item, dict)
)
return _one(str(args.get("path") or "."))
def _binding_items(binding: Any) -> tuple[Any, ...]:
if binding is None:
return ()
return binding if isinstance(binding, tuple) else (binding,)
def _binding_set_targets_system_repo(ctx: Any, binding: Any) -> bool:
items = _binding_items(binding)
return bool(items) and all(_registry().binding_targets_system_repo(ctx, item) for item in items)
def _binding_set_is_light_restricted(ctx: Any, binding: Any) -> bool:
"""Whether light mode must treat this file/VCS target as internal state."""
items = _binding_items(binding)
return bool(items) and all(
_registry().binding_targets_system_repo(ctx, item)
or (item.root == "runtime_data" and item.source == "runtime_data")
for item in items
)
def _binding_state_drive_root(ctx: Any, binding: Any) -> pathlib.Path:
items = _binding_items(binding)
if items:
return pathlib.Path(items[0].state_drive_root)
return pathlib.Path(ctx.drive_root)
def _light_binding_failure_redirect(name: str, args: dict[str, Any]) -> str:
"""Project an existing light-mode UX redirect after a failed target bind."""
try:
from ouroboros.config import get_runtime_mode
if get_runtime_mode() == "light":
return _registry().light_cognitive_or_root_redirect(name, args) or ""
except Exception:
pass
return ""
def _binding_error_text(name: str, root: str, exc: Exception) -> str:
detail = str(exc)
if detail.startswith("SKILL_REDIRECT_BLOCKED:"):
return f"⚠️ {detail}"
if detail.startswith("profile=") and " cannot " in detail:
return f"⚠️ TOOL_ACCESS_BLOCKED: {detail.rstrip('.')}."
if isinstance(exc, _registry().UserFilesPathBlockedError) and name in {
"read_file", "list_files", "search_code",
}:
return f"⚠️ USER_FILES_PATH_BLOCKED: {detail}"
if root == "skill_payload" and name in {"write_file", "edit_text"}:
return f"⚠️ SKILL_PAYLOAD_ARG_ERROR: {detail}"
prefixes = {
"read_file": "READ_FILE_ERROR",
"list_files": "LIST_FILES_ERROR",
"search_code": "SEARCH_ERROR",
"query_code": "TOOL_ARG_ERROR (query_code)",
"write_file": "WRITE_FILE_ERROR",
"edit_text": "EDIT_TEXT_ERROR",
"vcs_status": "GIT_ERROR",
"vcs_diff": "GIT_ERROR",
"vcs_pull_ff": "PULL_ERROR",
"vcs_restore": "RESTORE_ERROR",
"vcs_revert": "REVERT_ERROR",
"skill_review": "SKILL_REVIEW_ERROR",
"skill_preflight": "SKILL_PREFLIGHT_ERROR",
"submit_skill_to_hub": "SUBMIT_BLOCKED",
"run_command": "SHELL_CWD_BLOCKED",
"run_script": "SCRIPT_CWD_BLOCKED",
"start_service": "SHELL_CWD_BLOCKED",
"verify_and_record": "VERIFY_ERROR",
}
return f"⚠️ {prefixes.get(name, 'TOOL_ERROR')}: {type(exc).__name__}: {detail}"
def _format_tool_arg_error(entry: "ToolEntry") -> str:
params = _entry_public_params(entry)
accepted = ", ".join(params) if params else "none"
return (
f"⚠️ TOOL_ARG_ERROR ({entry.name}): invalid arguments for {entry.name}. "
f"Accepted parameters: {accepted}."
)

View file

@ -19,6 +19,14 @@ HOT_CODE_PATHS = frozenset({
"ouroboros/size_ratchet_manifest.py",
"ouroboros/tools/control.py",
"ouroboros/tools/registry.py",
# v7 D04 split leaves: code that merely moved out of the hot registry
# keeps the label (parity rule pinned by tests/test_lc2_owner_facades.py
# for the inverse direction).
"ouroboros/tools/registry_guard_process.py",
"ouroboros/tools/registry_guards.py",
"ouroboros/tools/tool_catalog.py",
"ouroboros/tools/tool_context.py",
"ouroboros/tools/tool_resolution.py",
"ouroboros/config.py",
"supervisor/queue.py",
"supervisor/events.py",

View file

@ -0,0 +1,160 @@
"""Structural contracts for the semantic-no-op tool_access extraction.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656) with four disclosed
adaptations to THIS tree:
1. The frozen-tool-inventory clause is dropped: ``ouroboros.tool_module_inventory``
is a v7 leaf this tree does not carry yet; the clause returns with that leaf.
2. The no-backedge clause asserts no MODULE-LEVEL (import-time) import of the
facade: on this tree the leaves deliberately read parent-owned rebindable
names through a call-time module handle (the owner-approved D18/D33
mechanical exception), which is not an import-time cycle.
3. The one-matrix clause checks identity through the facade re-export only:
the user_files leaf reads ``_POLICY`` through the call-time handle instead
of binding a module attribute, so the same-object guarantee holds by
construction (there is exactly one binding, on the facade).
4. The facade size bound is kept at the reference's 900; this tree's facade is
the tip monolith minus the moved spans and lands under it.
"""
from __future__ import annotations
import ast
import pathlib
from ouroboros import (
tool_access,
tool_access_paths,
tool_access_roots,
tool_access_types,
tool_access_user_files,
)
REPO = pathlib.Path(__file__).parents[1]
_LEAVES = (
tool_access_types,
tool_access_paths,
tool_access_roots,
tool_access_user_files,
)
_MOVED_OWNERS = {
"Operation": tool_access_types,
"ResolvedResourceBinding": tool_access_types,
"ResourceRoot": tool_access_types,
"SUBAGENT_CAPABILITIES": tool_access_types,
"SubagentCapability": tool_access_types,
"ToolAccessDecision": tool_access_types,
"ToolProfile": tool_access_types,
"_ALL_ROOTS": tool_access_types,
"_POLICY": tool_access_types,
"_READONLY_RESOURCE_ROOTS": tool_access_types,
"_READ_OPS": tool_access_types,
"_SUBAGENT_CAPABILITY_TO_OPERATION": tool_access_types,
"_TOP_LEVEL_PRINCIPAL_POLICY": tool_access_types,
"_TOP_LEVEL_PRINCIPAL_PROFILES": tool_access_types,
"_deliverables_root": tool_access_paths,
"_path_is_relative_to_casefold": tool_access_paths,
"_user_files_root": tool_access_paths,
"canonical_data_root": tool_access_paths,
"normalize_root": tool_access_paths,
"normalize_root_relative": tool_access_paths,
"normalize_runtime_data_path": tool_access_paths,
"path_is_relative_to": tool_access_paths,
"paths_overlap_casefold": tool_access_paths,
"workspace_mode_block_reason": tool_access_paths,
"_is_subagent_ctx": tool_access_roots,
"_skill_payload_base": tool_access_roots,
"active_tool_profile": tool_access_roots,
"binding_targets_system_repo": tool_access_roots,
"is_external_workspace": tool_access_roots,
"load_bound_skill": tool_access_roots,
"predicted_subagent_profile": tool_access_roots,
"project_room_lens_dir": tool_access_roots,
"resource_root_path": tool_access_roots,
"UserFilesPathBlockedError": tool_access_user_files,
"_USER_FILES_ALLOWED_DOTNAMES": tool_access_user_files,
"_USER_FILES_SECRET_COMPONENTS": tool_access_user_files,
"_USER_FILES_SECRET_NAMES": tool_access_user_files,
"_USER_FILES_SECRET_RE": tool_access_user_files,
"_subagent_projects_read_hint": tool_access_user_files,
"resolve_user_file_path": tool_access_user_files,
"user_files_path_block_reason": tool_access_user_files,
}
def test_tool_access_leaves_are_non_catalog_owners_without_import_backedges():
for module in (tool_access, *_LEAVES):
source_path = pathlib.Path(module.__file__)
tree = ast.parse(source_path.read_text(encoding="utf-8"))
assert not any(
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "get_tools"
for node in tree.body
)
for module in _LEAVES:
tree = ast.parse(pathlib.Path(module.__file__).read_text(encoding="utf-8"))
# No import-time backedge: the facade may only be read through the
# call-time module handle inside function bodies (D18/D33 idiom).
for node in tree.body:
assert not (
isinstance(node, ast.ImportFrom)
and node.module in ("ouroboros.tool_access", "ouroboros")
and any(a.name == "tool_access" or node.module == "ouroboros.tool_access"
for a in node.names)
), f"{module.__name__} imports the facade at module level"
assert not (
isinstance(node, ast.Import)
and any(a.name == "ouroboros.tool_access" for a in node.names)
), f"{module.__name__} imports the facade at module level"
def test_tool_access_decision_surface_stays_with_the_matrix_owner():
"""The access decision, its affordance projections, and the binding builder
remain authored by ``tool_access`` itself; the leaves own vocabulary,
physical paths, root resolution, and one root's path policy."""
for name in (
"decide_tool_access",
"subagent_profile_satisfies",
"summarize_subagent_profile",
"filesystem_affordance_map",
"profile_readable_root_paths",
"shell_cwd_block_message",
"resolve_shell_cwd",
"build_resolved_resource_binding",
"resolve_resource_path",
):
assert getattr(tool_access, name).__module__ == "ouroboros.tool_access", name
def test_tool_access_facade_reexports_every_moved_identity():
"""``tool_access`` keeps the exact objects, so the registry, the tool
handlers, the supervisor and every guard that imports these names see no
identity change."""
for name, owner in _MOVED_OWNERS.items():
assert hasattr(tool_access, name), name
assert getattr(tool_access, name) is getattr(owner, name), name
owned = {name for module in _LEAVES for name in vars(module)}
assert set(_MOVED_OWNERS) <= owned
def test_tool_access_policy_matrix_is_one_object_across_owners():
"""Every reader of the matrix must observe the same mapping object, not a
copy. The user_files leaf reads it through the call-time facade handle, so
the facade re-export IS its binding."""
assert tool_access._POLICY is tool_access_types._POLICY
assert set(tool_access._ALL_ROOTS) == set(tool_access_types._ALL_ROOTS)
def test_tool_access_extraction_size_bounds_have_meaningful_headroom():
counts = {
module.__name__: len(
pathlib.Path(module.__file__).read_text(encoding="utf-8").splitlines()
)
for module in (tool_access, *_LEAVES)
}
assert counts["ouroboros.tool_access"] <= 900
assert all(count <= 1000 for count in counts.values())
assert 200 <= counts["ouroboros.tool_access_user_files"] <= 1000

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,468 @@
"""The protected black-box policy over executor artifacts and control state.
Split verbatim out of ``tests/test_tool_capabilities.py`` by theme. This
module owns the introspection fence: which paths the black-box policy
blocks, how it maps executor backend paths recursively, and the runtime
data-write block on workspace executor control state.
"""
import os
import base64
import pathlib
import sys
def test_protected_black_box_artifact_policy_blocks_introspection(tmp_path, monkeypatch):
from ouroboros.contracts.task_contract import build_task_contract
from ouroboros.tools.registry import ToolContext, ToolRegistry
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
if os.name == "nt":
protected = repo / "reference.cmd"
generated = repo / "generated.cmd"
direct_cmd = ["cmd.exe", "/c", str(protected)]
protected.write_text("@echo reference\r\n", encoding="utf-8")
generated.write_text("@echo generated\r\n", encoding="utf-8")
else:
protected = repo / "reference.sh"
generated = repo / "generated.sh"
direct_cmd = [str(protected)]
protected.write_text("#!/bin/sh\nprintf 'reference\\n'\n", encoding="utf-8")
generated.write_text("#!/bin/sh\nprintf 'generated\\n'\n", encoding="utf-8")
protected_dir = repo / "protected_dir"
protected_dir.mkdir()
(protected_dir / "secret.txt").write_text("secret\n", encoding="utf-8")
protected.chmod(0o755)
generated.chmod(0o755)
task_contract = build_task_contract({
"resource_policy": {
"protected_artifacts": [
{
"id": "reference",
"role": "black_box_reference",
"paths": [str(protected)],
"allow": ["execute"],
"deny": ["read_bytes", "copy", "hash", "static_introspection", "dynamic_trace", "debug"],
},
{
"id": "reference-dir",
"role": "black_box_reference",
"paths": [str(protected_dir)],
"allow": ["execute"],
}
]
}
})
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry.set_context(ToolContext(
repo_dir=repo,
drive_root=data,
task_contract=task_contract,
task_metadata={"task_contract": task_contract},
))
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
direct = registry.execute("run_command", {"cmd": direct_cmd})
assert "RESOURCE_POLICY_BLOCKED" not in direct
assert "reference" in direct
assert "RESOURCE_POLICY_BLOCKED" in registry.execute("read_file", {"path": protected.name})
protected_content = protected.read_text(encoding="utf-8")
write_attempt = registry.execute("write_file", {"path": protected.name, "content": "tamper\n"})
assert "RESOURCE_POLICY_BLOCKED" in write_attempt
assert protected.read_text(encoding="utf-8") == protected_content
edit_attempt = registry.execute("edit_text", {"path": protected.name, "old_str": "reference", "new_str": "tamper"})
assert "RESOURCE_POLICY_BLOCKED" in edit_attempt
assert protected.read_text(encoding="utf-8") == protected_content
shell_write_attempt = registry.execute(
"run_command",
{"cmd": ["sh", "-c", f"printf tamper > {protected.name}"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in shell_write_attempt
assert protected.read_text(encoding="utf-8") == protected_content
shell_delete_attempt = registry.execute(
"run_command",
{"cmd": ["rm", protected.name], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in shell_delete_attempt
assert protected.exists()
recursive_delete_attempt = registry.execute(
"run_command",
{"cmd": ["rm", "-rf", "."], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in recursive_delete_attempt
assert protected.exists()
glob_delete_attempt = registry.execute(
"run_command",
{"cmd": ["sh", "-c", "rm -rf *"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in glob_delete_attempt
assert protected.exists()
glob_read_attempt = registry.execute(
"run_command",
{"cmd": ["sh", "-c", "cat *"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in glob_read_attempt
find_exec_read = registry.execute(
"run_command",
{"cmd": ["find", ".", "-type", "f", "-exec", "cat", "{}", "+"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in find_exec_read
find_delete = registry.execute(
"run_command",
{"cmd": ["find", ".", "-delete"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in find_delete
assert protected.exists()
pathless_find_exec_read = registry.execute(
"run_command",
{"cmd": ["find", "-type", "f", "-exec", "cat", "{}", "+"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in pathless_find_exec_read
pathless_find_delete = registry.execute(
"run_command",
{"cmd": ["find", "-delete"], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in pathless_find_delete
assert protected.exists()
safe_interpreter = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
"print(1)",
],
"cwd": str(repo),
},
)
assert "RESOURCE_POLICY_BLOCKED" not in safe_interpreter
assert "1" in safe_interpreter
assert "RESOURCE_POLICY_BLOCKED" in registry.execute("list_files", {"path": protected_dir.name})
interpreter_read = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
f"from pathlib import Path; print(Path(r'{protected}').read_bytes())",
]
},
)
assert "RESOURCE_POLICY_BLOCKED" in interpreter_read
relative_interpreter_read = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
f"from pathlib import Path; print(Path({protected.name!r}).read_bytes())",
],
"cwd": str(repo),
},
)
assert "RESOURCE_POLICY_BLOCKED" in relative_interpreter_read
versioned_interpreter_read = registry.execute(
"run_command",
{
"cmd": [
"python3.12",
"-c",
f"from pathlib import Path; print(Path({protected.name!r}).read_bytes())",
],
"cwd": str(repo),
},
)
assert "RESOURCE_POLICY_BLOCKED" in versioned_interpreter_read
constructed_path_read = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
(
"from pathlib import Path; "
f"print((Path(r'{protected.parent}') / ({protected.stem!r} + {protected.suffix!r})).read_bytes())"
),
]
},
)
assert "RESOURCE_POLICY_BLOCKED" in constructed_path_read
backslash_parent = str(protected.parent).replace("/", "\\")
backslash_constructed_path_read = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
(
"from pathlib import Path; "
f"print((Path({backslash_parent!r}) / ({protected.stem!r} + {protected.suffix!r})).read_bytes())"
),
]
},
)
assert "RESOURCE_POLICY_BLOCKED" in backslash_constructed_path_read
env_assignment_read = registry.execute(
"run_command",
{
"cmd": [
f"REF={protected}",
sys.executable,
"-c",
"import os; print(open(os.environ['REF'], 'rb').read())",
]
},
)
assert "RESOURCE_POLICY_BLOCKED" in env_assignment_read
shell_script_read = registry.execute("run_command", {"cmd": ["sh", str(protected)]})
assert "RESOURCE_POLICY_BLOCKED" in shell_script_read
for cmd in (
["cmd.exe", "/c", "type", protected.name],
["cmd.exe", "/c", "copy", protected.name, str(repo / "copy.cmd")],
["cmd.exe", "/c", "xcopy", protected.name, str(repo / "copy-dir")],
["powershell.exe", "-Command", "Get-Content", protected.name],
["powershell.exe", "-Command", "Select-String", "reference", protected.name],
["powershell.exe", "-Command", "Copy-Item", protected.name, str(repo / "copy.ps1")],
["pwsh", "-Command", "Get-FileHash", protected.name],
["cmd.exe", "/c", "certutil", "-hashfile", protected.name],
):
result = registry.execute("run_command", {"cmd": cmd, "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in result, cmd
encoded_read = base64.b64encode(f"Get-Content {protected.name}".encode("utf-16le")).decode("ascii")
for cmd in (
["powershell.exe", "-EncodedCommand", encoded_read],
["pwsh", "-enc", encoded_read],
):
result = registry.execute("run_command", {"cmd": cmd, "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in result, cmd
search_direct = registry.execute("search_code", {"query": "reference", "path": protected.name})
assert "RESOURCE_POLICY_BLOCKED" in search_direct
search_protected_dir = registry.execute("search_code", {"query": "secret", "path": protected_dir.name})
assert "RESOURCE_POLICY_BLOCKED" in search_protected_dir
query_protected = registry.execute("query_code", {"op": "structural", "query": "reference", "path": protected.name})
assert "RESOURCE_POLICY_BLOCKED" not in query_protected
assert protected.name not in query_protected
for cache in (data / "state" / "code_intel").glob("*/inventory.json"):
assert protected.name not in cache.read_text(encoding="utf-8")
grep_read = registry.execute("run_command", {"cmd": ["grep", "reference", str(protected)]})
assert "RESOURCE_POLICY_BLOCKED" in grep_read
grep_recursive = registry.execute("run_command", {"cmd": ["grep", "-R", "reference", "."], "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in grep_recursive
rg_read = registry.execute("run_command", {"cmd": ["rg", "reference", str(protected)]})
assert "RESOURCE_POLICY_BLOCKED" in rg_read
rg_recursive = registry.execute("run_command", {"cmd": ["rg", "reference", "."], "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in rg_recursive
copy_recursive = registry.execute("run_command", {"cmd": ["cp", "-R", ".", str(repo / "copy")], "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in copy_recursive
for cmd in (
["git", "diff", "--", protected.name],
["git", "diff"],
["git", "show", f"HEAD:{protected.name}"],
["git", "show", "HEAD"],
["git", "grep", "reference", "--", protected.name],
["git", "grep", "reference"],
["git", "cat-file", "-p", f"HEAD:{protected.name}"],
["git", "log", "-p", "--", protected.name],
["git", "log", "-p"],
):
result = registry.execute("run_command", {"cmd": cmd, "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in result, cmd
assert "RESOURCE_POLICY_BLOCKED" in registry.execute("vcs_diff", {"path": protected.name})
assert "RESOURCE_POLICY_BLOCKED" in registry.execute("vcs_diff", {})
import ouroboros.code_intelligence as code_intelligence
original_file_fact = code_intelligence._file_fact
def guarded_file_fact(repo_root, path):
assert pathlib.Path(path).resolve(strict=False) != protected.resolve(strict=False)
return original_file_fact(repo_root, path)
monkeypatch.setattr(code_intelligence, "_file_fact", guarded_file_fact)
digest = registry.execute("query_code", {"op": "digest"})
assert protected.name not in digest
assert generated.name in digest
run_output_export = registry.execute("run_command", {"cmd": direct_cmd, "outputs": [protected.name], "cwd": str(repo)})
assert "ARTIFACT_OUTPUT_ERROR" in run_output_export
assert "RESOURCE_POLICY_BLOCKED" in run_output_export
script_output_export = registry.execute(
"run_script",
{"interpreter": "python3", "script": "print('ok')", "outputs": [protected.name], "cwd": str(repo)},
)
assert "RESOURCE_POLICY_BLOCKED" in script_output_export
service_cmd = ["cmd.exe", "/c", "ping", "127.0.0.1", "-n", "30"] if os.name == "nt" else ["sleep", "30"]
service_start = registry.execute(
"start_service",
{
"name": "protected-output",
"cmd": service_cmd,
"cwd": str(repo),
"outputs": [protected.name],
},
)
assert "protected-output" in service_start
service_stop = registry.execute("stop_service", {"name": "protected-output"})
assert "ARTIFACT_OUTPUT_ERROR" in service_stop
assert "RESOURCE_POLICY_BLOCKED" in service_stop
for cmd in (
["strings", str(protected)],
["objdump", "-d", str(protected)],
["cat", str(protected)],
["sha256sum", str(protected)],
["strace", str(protected)],
["gdb", str(protected)],
["lldb", str(protected)],
["cp", str(protected), str(repo / "copy.sh")],
["dd", f"if={protected}", f"of={repo / 'copy2.sh'}"],
["tar", "-czf", str(repo / "out.tgz"), protected.name],
["tar", "-czf", str(repo / "tree.tgz"), "."],
["zip", str(repo / "out.zip"), protected.name],
["rsync", protected.name, str(repo / "copy.sh")],
):
result = registry.execute("run_command", {"cmd": cmd, "cwd": str(repo)})
assert "RESOURCE_POLICY_BLOCKED" in result, cmd
generated_result = registry.execute("run_command", {"cmd": ["strings", str(generated)]})
assert "RESOURCE_POLICY_BLOCKED" not in generated_result
def test_protected_black_box_recursive_policy_maps_executor_backend_paths(tmp_path, monkeypatch):
from ouroboros.contracts.task_contract import build_task_contract
from ouroboros.tools.registry import ToolContext, ToolRegistry
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True, exist_ok=True)
protected = workspace / "executable"
protected.write_text("reference bytes\n", encoding="utf-8")
task_contract = build_task_contract({
"resource_policy": {
"protected_artifacts": [
{
"id": "reference",
"role": "black_box_reference",
"paths": ["/workspace/executable"],
"allow": ["execute"],
"deny": ["read_bytes", "copy", "hash", "static_introspection", "dynamic_trace", "debug"],
}
]
}
})
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(
ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_contract=task_contract,
task_metadata={"task_contract": task_contract},
executor_ref={
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
},
)
)
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
grep_recursive = registry.execute("run_command", {"cmd": ["grep", "-R", "reference", "."], "cwd": str(workspace)})
copy_recursive = registry.execute("run_command", {"cmd": ["cp", "-R", ".", str(workspace / "copy")], "cwd": str(workspace)})
import ouroboros.code_intelligence as code_intelligence
original_file_fact = code_intelligence._file_fact
def guarded_file_fact(repo_root, path):
assert pathlib.Path(path).resolve(strict=False) != protected.resolve(strict=False)
return original_file_fact(repo_root, path)
monkeypatch.setattr(code_intelligence, "_file_fact", guarded_file_fact)
digest = registry.execute("query_code", {"op": "digest"})
assert "RESOURCE_POLICY_BLOCKED" in grep_recursive
assert "RESOURCE_POLICY_BLOCKED" in copy_recursive
assert "executable" not in digest
def test_runtime_data_write_blocks_workspace_executor_control_state(tmp_path, monkeypatch):
from ouroboros.tools.registry import ToolContext, ToolRegistry
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
state_dir = data / "state" / "workspace_executor_processes"
state_dir.mkdir(parents=True)
existing = state_dir / "foreground-forged.json"
existing.write_text("original", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry.set_context(ToolContext(repo_dir=repo, drive_root=data))
monkeypatch.setattr("ouroboros.safety.check_safety", lambda *a, **k: (True, ""))
direct_write = registry.execute(
"write_file",
{
"root": "runtime_data",
"path": "state/workspace_executor_processes/foreground-forged.json",
"content": "{}",
},
)
assert "DATA_WRITE_BLOCKED" in direct_write
assert existing.read_text(encoding="utf-8") == "original"
nested_write = registry.execute(
"write_file",
{
"root": "runtime_data",
"path": "state/headless_tasks/child/data/state/workspace_executor_processes/foreground-forged.json",
"content": "{}",
},
)
assert "DATA_WRITE_BLOCKED" in nested_write
edit = registry.execute(
"edit_text",
{
"root": "runtime_data",
"path": "state/workspace_executor_processes/foreground-forged.json",
"old_str": "original",
"new_str": "tampered",
},
)
assert "EDIT_TEXT_BLOCKED" in edit
assert existing.read_text(encoding="utf-8") == "original"
shell_write = registry.execute(
"run_command",
{
"cmd": [
sys.executable,
"-c",
(
"from pathlib import Path; "
f"Path(r'{existing}').write_text('{{\"owner\":\"ouroboros_workspace_executor\"}}')"
),
],
},
)
assert "WORKSPACE_EXECUTOR_STATE_WRITE_BLOCKED" in shell_write
assert existing.read_text(encoding="utf-8") == "original"
node_eval_write = registry.execute(
"run_command",
{
"cmd": [
"node",
"-e",
f"require('fs').writeFileSync({str(existing)!r}, '{{}}')",
],
},
)
assert "WORKSPACE_EXECUTOR_STATE_WRITE_BLOCKED" in node_eval_write
assert existing.read_text(encoding="utf-8") == "original"

View file

@ -0,0 +1,352 @@
"""What a local read-only subagent may reach.
Split verbatim out of ``tests/test_tool_capabilities.py`` by theme. This
module owns the read-only subagent profile boundary: forbidden tools at
execute time, the enabled extension tool it may still call, the
allowed-resources block on web/external tools, and the secret-file,
task-drive and skill-payload filters on its data and repo reads.
"""
import os
import pathlib
def test_local_readonly_subagent_execute_blocks_forbidden_tools(tmp_path, monkeypatch):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
import ouroboros.mcp_client as mcp_client
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
registry.set_context(
ToolContext(
repo_dir=tmp_path,
drive_root=tmp_path,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
assert registry.get_schema_by_name("write_file") is None
assert registry.get_schema_by_name("enable_tools") is None
assert registry.get_schema_by_name("schedule_subagent") is not None
# switch_model changes COGNITIVE POWER, not authority: a child that started cheap and
# found the work harder raises its own strength, and nothing about its sandbox moves.
# It was on the blocked list until v6.87.7 purely because power and authority were
# conflated; a read-only child stays read-only at any model.
assert registry.get_schema_by_name("switch_model") is not None
assert "LOCAL_READONLY_SUBAGENT_BLOCKED" not in registry.execute("switch_model", {})
monkeypatch.setattr(mcp_client, "ensure_configured_from_settings", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("MCP touched")))
assert "LOCAL_READONLY_SUBAGENT_BLOCKED" not in registry.execute("list_files", {"path": "."})
assert registry.get_schema_by_name("vcs_status") is not None
assert "TOOL_ACCESS_BLOCKED" not in registry.execute("vcs_status", {"root": "system_repo"})
blocked_tools = [
"write_file",
"edit_text",
"knowledge_write",
"update_scratchpad",
"update_identity",
"commit_reviewed",
"preflight_review",
"task_acceptance_review",
"skill_review",
"request_restart",
"enable_tools",
"run_command",
"skill_exec",
"list_skills",
]
for name in blocked_tools:
assert registry.get_schema_by_name(name) is None
assert "LOCAL_READONLY_SUBAGENT_BLOCKED" in registry.execute(name, {})
def test_local_readonly_subagent_allows_enabled_extension_tool(tmp_path, monkeypatch):
from ouroboros import extension_loader
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
from tests._shared import clean_extension_runtime_state
from tests.test_extension_loader import _mark_isolated_deps_installed, _prepare_extension
monkeypatch.setenv("OUROBOROS_RUNTIME_MODE", "advanced")
clean_extension_runtime_state()
plugin = (
"def _lookup(ctx, query=''):\n"
" return 'external-ok:' + query\n"
"def register(api):\n"
" api.register_tool('lookup', _lookup, description='External lookup', "
"schema={'type': 'object', 'properties': {'query': {'type': 'string'}}}, timeout_sec=5)\n"
)
loaded, skills_repo, parent_drive = _prepare_extension(
tmp_path,
"research",
plugin,
permissions=["tool"],
extra_frontmatter="dependencies:\n - dummy_pkg\n",
)
_mark_isolated_deps_installed(parent_drive, loaded)
child_drive = tmp_path / "child-drive"
child_drive.mkdir()
err = extension_loader.load_extension(loaded, lambda: {}, drive_root=parent_drive)
assert err is None, err
tool_name = extension_loader.extension_surface_name("research", "lookup")
assert extension_loader.is_extension_live("research", parent_drive, repo_path=str(skills_repo))
assert not extension_loader.is_extension_live("research", child_drive, repo_path=str(skills_repo))
assert extension_loader.get_tool(tool_name)["out_of_process"] is True
repo_dir = pathlib.Path(__file__).resolve().parents[1]
registry = ToolRegistry(repo_dir=repo_dir, drive_root=child_drive)
try:
registry.set_context(
ToolContext(
repo_dir=repo_dir,
drive_root=child_drive,
task_metadata={"budget_drive_root": str(parent_drive)},
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
assert registry.get_schema_by_name(tool_name) is not None
assert "external-ok:budget-root" in registry.execute(tool_name, {"query": "budget-root"})
finally:
clean_extension_runtime_state()
def test_allowed_resources_block_web_and_external_tools(tmp_path, monkeypatch):
monkeypatch.setenv("OPENROUTER_API_KEY", "test-key")
from ouroboros import extension_loader
from ouroboros.contracts.task_contract import build_task_contract
from ouroboros.tools.registry import ToolContext, ToolRegistry
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
task_contract = build_task_contract({
"id": "task-resources",
"allowed_resources": {"web": "false", "network": "false"},
})
tool_name = extension_loader.extension_surface_name("research", "lookup")
with extension_loader._lock:
extension_loader._tools[tool_name] = {
"name": tool_name,
"handler": lambda ctx, **kwargs: "external-ok",
"description": "External lookup",
"schema": {"type": "object", "properties": {}},
"timeout_sec": 5,
"skill": "research",
}
monkeypatch.setattr(extension_loader, "is_extension_live", lambda *_a, **_k: True)
try:
registry.set_context(
ToolContext(
repo_dir=tmp_path / "repo",
drive_root=tmp_path / "data",
task_contract=task_contract,
task_metadata={"task_contract": task_contract},
)
)
assert task_contract["allowed_resources"] == {"web": False, "network": False}
assert "RESOURCE_CONSTRAINT_BLOCKED" in registry.execute("web_search", {"query": "x"})
# VLM tools are first-class vision tools, not web egress. Benchmark isolation
# withholds them by name via disabled_tools instead of relying on web=false.
assert "RESOURCE_CONSTRAINT_BLOCKED" not in registry.execute("vlm_query", {"prompt": "x"})
assert "RESOURCE_CONSTRAINT_BLOCKED" in registry.execute(
"vlm_query", {"prompt": "x", "image_url": "https://example.com/a.png"}
)
assert registry.get_schema_by_name(tool_name) is None
assert tool_name not in {schema["function"]["name"] for schema in registry.schemas()}
assert any(item.get("surface") == "extensions" and item.get("reason") == "resource_blocked" for item in registry.capability_omissions())
blocked = registry.execute(tool_name, {})
assert "RESOURCE_CONSTRAINT_BLOCKED" in blocked
assert "network=false" in blocked
alias_contract = build_task_contract({
"id": "task-resource-aliases",
"allowed_resources": {"allow_network": "false"},
})
registry.set_context(
ToolContext(
repo_dir=tmp_path / "repo",
drive_root=tmp_path / "data",
task_contract=alias_contract,
task_metadata={"task_contract": alias_contract},
)
)
assert alias_contract["allowed_resources"] == {"allow_network": False}
assert "RESOURCE_CONSTRAINT_BLOCKED" in registry.execute("web_search", {"query": "x"})
finally:
with extension_loader._lock:
extension_loader._tools.pop(tool_name, None)
def test_local_readonly_subagent_data_read_denies_secret_files(tmp_path):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
(tmp_path / "settings.json").write_text('{"OPENROUTER_API_KEY":"secret"}', encoding="utf-8")
(tmp_path / "settings.tmp").write_text('{"OPENROUTER_API_KEY":"secret"}', encoding="utf-8")
(tmp_path / ".settings.json.tmp.123").write_text('{"OPENROUTER_API_KEY":"secret"}', encoding="utf-8")
(tmp_path / ".env.local").write_text("TOKEN=secret", encoding="utf-8")
(tmp_path / "prod.env").write_text("TOKEN=secret", encoding="utf-8")
(tmp_path / "state" / "skills" / "weather").mkdir(parents=True)
(tmp_path / "state" / "skills" / "weather" / "grants.json").write_text("{}", encoding="utf-8")
(tmp_path / "state" / "skills" / "weather" / ".grants.json.tmp.123").write_text("{}", encoding="utf-8")
(tmp_path / "state" / "skills" / "weather" / "review.json.lock").write_text("{}", encoding="utf-8")
(tmp_path / "logs").mkdir()
(tmp_path / "logs" / "events.jsonl").write_text("{}", encoding="utf-8")
try:
os.symlink("settings.json", tmp_path / "alias.txt")
except (OSError, NotImplementedError):
pass
try:
os.link(tmp_path / "settings.json", tmp_path / "hardlink.txt")
except (OSError, NotImplementedError):
pass
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
registry.set_context(
ToolContext(
repo_dir=tmp_path,
drive_root=tmp_path,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
blocked = registry.execute("read_file", {"root": "runtime_data", "path": "settings.json"})
assert "DATA_READ_BLOCKED" in blocked
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": "settings.tmp"})
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": ".settings.json.tmp.123"})
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": ".env.local"})
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": "prod.env"})
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": "state/skills/weather/.grants.json.tmp.123"})
assert "DATA_READ_BLOCKED" in registry.execute("read_file", {"root": "runtime_data", "path": "state/skills/weather/review.json.lock"})
alias_result = registry.execute("read_file", {"root": "runtime_data", "path": "alias.txt"})
if (tmp_path / "alias.txt").exists():
assert "DATA_READ_BLOCKED" in alias_result
hardlink_result = registry.execute("read_file", {"root": "runtime_data", "path": "hardlink.txt"})
if (tmp_path / "hardlink.txt").exists():
assert "DATA_READ_BLOCKED" in hardlink_result
listing = registry.execute("list_files", {"root": "runtime_data", "path": "."})
assert "settings.json" not in listing
assert "settings.tmp" not in listing
assert ".settings.json.tmp.123" not in listing
assert ".env.local" not in listing
assert "prod.env" not in listing
assert "alias.txt" not in listing
assert "hardlink.txt" not in listing
assert "secret/control" in listing
skill_state_listing = registry.execute("list_files", {"root": "runtime_data", "path": "state/skills/weather"})
assert "grants.json" not in skill_state_listing
assert ".grants.json.tmp.123" not in skill_state_listing
assert "review.json.lock" not in skill_state_listing
assert "secret/control" in skill_state_listing
assert "DATA_LIST_BLOCKED" in registry.execute("list_files", {"root": "runtime_data", "path": "state/skills/weather/grants.json"})
assert "DATA_LIST_BLOCKED" in registry.execute("list_files", {"root": "runtime_data", "path": "state/skills/weather/.grants.json.tmp.123"})
readable = registry.execute("read_file", {"root": "runtime_data", "path": "logs/events.jsonl"})
assert "{}" in readable
def test_local_readonly_subagent_repo_read_denies_secret_files(tmp_path):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
repo = tmp_path / "repo"
data = tmp_path / "data"
(repo / ".git").mkdir(parents=True)
data.mkdir()
(repo / ".git" / "credentials").write_text("https://token@example.invalid\n", encoding="utf-8")
(repo / ".git" / "config").write_text("[credential]\n", encoding="utf-8")
(repo / ".env.local").write_text("TOKEN=secret\nLEAK_MARKER=env\n", encoding="utf-8")
(repo / "auth_token.json").write_text('{"token":"TOKEN_LEAK"}\n', encoding="utf-8")
(repo / "src").mkdir()
(repo / "src" / "public.py").write_text("print('ok')\n", encoding="utf-8")
(repo / "src" / "skill_token.py").write_text("TOKEN_NAME = 'safe source symbol'\n", encoding="utf-8")
try:
os.symlink(".git/credentials", repo / "alias.txt")
except (OSError, NotImplementedError):
pass
try:
os.link(repo / ".git" / "credentials", repo / "hardlink.txt")
except (OSError, NotImplementedError):
pass
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry.set_context(
ToolContext(
repo_dir=repo,
drive_root=data,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
assert "REPO_READ_BLOCKED" in registry.execute("read_file", {"path": ".git/credentials"})
assert "READ_FILE_BLOCKED" in registry.execute("read_file", {"root": "system_repo", "path": ".git/credentials"})
assert "REPO_READ_BLOCKED" in registry.execute("read_file", {"path": ".git/config"})
assert "READ_FILE_BLOCKED" in registry.execute("read_file", {"root": "system_repo", "path": ".git/config"})
assert "REPO_READ_BLOCKED" in registry.execute("read_file", {"path": ".env.local"})
assert "REPO_READ_BLOCKED" in registry.execute("read_file", {"path": "auth_token.json"})
alias_result = registry.execute("read_file", {"path": "alias.txt"})
if (repo / "alias.txt").exists():
assert "REPO_READ_BLOCKED" in alias_result
hardlink_result = registry.execute("read_file", {"path": "hardlink.txt"})
if (repo / "hardlink.txt").exists():
assert "REPO_READ_BLOCKED" in hardlink_result
listing = registry.execute("list_files", {"path": "."})
assert ".git/" not in listing
assert ".env.local" not in listing
assert "auth_token.json" not in listing
assert "alias.txt" not in listing
assert "hardlink.txt" not in listing
assert "src/" in listing
assert "secret/control" in listing
system_listing = registry.execute("list_files", {"root": "system_repo", "path": "."})
assert ".git/" not in system_listing
assert "auth_token.json" not in system_listing
assert "secret/control" in system_listing
assert "REPO_LIST_BLOCKED" in registry.execute("list_files", {"path": ".git"})
readable = registry.execute("read_file", {"path": "src/public.py"})
assert "print('ok')" in readable
source_with_token_name = registry.execute("read_file", {"path": "src/skill_token.py"})
assert "safe source symbol" in source_with_token_name
secret_search = registry.execute("search_code", {"query": "TOKEN_LEAK"})
assert "No matches found" in secret_search
assert "auth_token.json:" not in secret_search
assert "SEARCH_BLOCKED" in registry.execute("search_code", {"query": "TOKEN_LEAK", "path": "auth_token.json"})
public_search = registry.execute("search_code", {"query": "safe source symbol"})
assert "src/skill_token.py" in public_search
digest = registry.execute("query_code", {"op": "digest"})
assert "auth_token.json" not in digest
assert ".env.local" not in digest
assert "src/skill_token.py" in digest
cached = list((data / "state" / "code_intel").glob("*/inventory.json"))
assert not cached
def test_local_readonly_subagent_task_drive_and_skill_payload_filters(tmp_path):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolContext, ToolRegistry
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
data.mkdir()
(data / "settings.json").write_text('{"OPENROUTER_API_KEY":"secret"}', encoding="utf-8")
(data / "skills" / "external" / "alpha").mkdir(parents=True)
(data / "skills" / "external" / "alpha" / "SKILL.md").write_text("hello", encoding="utf-8")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry.set_context(
ToolContext(
repo_dir=repo,
drive_root=data,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
assert "READ_FILE_BLOCKED" in registry.execute("read_file", {"root": "task_drive", "path": "settings.json"})
traversal = registry.execute(
"read_file",
{"root": "skill_payload", "bucket": "external", "skill_name": "../../settings.json", "path": "."},
)
assert "TOOL_ACCESS_BLOCKED" in traversal or "READ_FILE_ERROR" in traversal or "TOOL_ARG_ERROR" in traversal
skill_payload_read = registry.execute(
"read_file",
{"root": "skill_payload", "bucket": "external", "skill_name": "alpha", "path": "SKILL.md"},
)
# v6.70.0 (owner-approved): read-only scouts may READ skill payloads — a scout
# sent to review a skill used to be structurally blind to it. Mutation stays
# blocked (pinned in test_owner_facing_honesty.py).
assert "TOOL_ACCESS_BLOCKED" not in skill_payload_read
assert "hello" in skill_payload_read

View file

@ -0,0 +1,271 @@
"""The search_code tool: classification, registration and behavior.
Split verbatim out of ``tests/test_tool_capabilities.py`` by theme. This
module owns everything search_code: its capability-set membership and
result limit, its schema/registry visibility, the literal/regex/filter
semantics, and the ripgrep path filters, fallback and symlink fence.
"""
import os
import pathlib
import pytest
import sys
import tempfile
# ---------------------------------------------------------------------------
# search_code classification tests
# ---------------------------------------------------------------------------
def test_search_code_in_core_tools():
"""search_code must be in CORE_TOOL_NAMES."""
from ouroboros.tool_capabilities import CORE_TOOL_NAMES
assert "search_code" in CORE_TOOL_NAMES
def test_search_code_is_parallel_safe():
"""search_code must be in READ_ONLY_PARALLEL_TOOLS."""
from ouroboros.tool_capabilities import READ_ONLY_PARALLEL_TOOLS
assert "search_code" in READ_ONLY_PARALLEL_TOOLS
def test_search_code_has_result_limit():
"""search_code must have an explicit result size limit."""
from ouroboros.tool_capabilities import TOOL_RESULT_LIMITS
assert "search_code" in TOOL_RESULT_LIMITS
from ouroboros.tool_capabilities import UNTRUNCATED_TOOL_RESULTS
assert "plan_task" in UNTRUNCATED_TOOL_RESULTS
# Child-handoff tools stay transport-uncapped: wait_task/get_task_result are
# FULL by contract, and wait_tasks' compact projection must not additionally
# be char-capped (child_result_sha256 pins the exact result text seen).
for _handoff_tool in ("wait_task", "wait_tasks", "get_task_result"):
assert _handoff_tool in UNTRUNCATED_TOOL_RESULTS
from ouroboros.tool_capabilities import FOREGROUND_MUTATIVE_TOOLS
# Publication can create a remote branch/commit/PR. Its outer timeout must
# not return while that foreground mutator is still running.
assert FOREGROUND_MUTATIVE_TOOLS == frozenset({"submit_skill_to_hub"})
# ---------------------------------------------------------------------------
# search_code tool behavior tests
# ---------------------------------------------------------------------------
def _make_ctx(tmp_path):
from ouroboros.tools.registry import ToolContext
from unittest.mock import MagicMock
ctx = MagicMock(spec=ToolContext)
ctx.repo_dir = tmp_path
ctx.repo_path = lambda p: tmp_path / p
return ctx
def _populate_repo(tmp_path):
"""Create a mini repo structure for search tests."""
(tmp_path / "foo.py").write_text("def hello():\n return 'world'\n", encoding="utf-8")
(tmp_path / "bar.py").write_text("import os\ndef hello_bar():\n pass\n", encoding="utf-8")
sub = tmp_path / "sub"
sub.mkdir()
(sub / "baz.py").write_text("class MyClass:\n hello = True\n", encoding="utf-8")
# Binary-like file (should be skipped)
(tmp_path / "data.png").write_bytes(b'\x89PNG\r\n\x1a\n' + b'\x00' * 100)
# Cache dir (should be skipped)
cache = tmp_path / "__pycache__"
cache.mkdir()
(cache / "foo.cpython-310.pyc").write_bytes(b'\x00' * 50)
def test_code_search_literal(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, "hello")
assert "foo.py:1:" in result
assert "bar.py:2:" in result
assert "sub/baz.py:2:" in result
def test_code_search_regex(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, r"def \w+\(\)", regex=True)
assert "foo.py:1:" in result
assert "bar.py:2:" in result
def test_code_search_scoped_path(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, "hello", path="sub")
assert "sub/baz.py" in result
assert "foo.py" not in result
def test_code_search_include_filter(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
(tmp_path / "readme.md").write_text("hello from markdown\n", encoding="utf-8")
result = _code_search(ctx, "hello", include="*.md")
assert "readme.md" in result
assert "foo.py" not in result
def test_code_search_no_matches(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, "zzz_nonexistent_zzz")
assert "No matches found" in result
def test_code_search_skips_binaries(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, "PNG")
# .png file should be skipped even though it contains "PNG" bytes
assert "data.png" not in result
def test_code_search_skips_cache_dirs(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
_populate_repo(tmp_path)
result = _code_search(ctx, "foo")
assert "__pycache__" not in result
def test_code_search_max_results(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
# Create many matching lines
lines = "\n".join(f"match_line_{i}" for i in range(50))
(tmp_path / "many.py").write_text(lines, encoding="utf-8")
result = _code_search(ctx, "match_line", max_results=10)
assert "truncated at 10" in result
def test_code_search_empty_query(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
result = _code_search(ctx, "")
assert "SEARCH_ERROR" in result
def test_code_search_invalid_regex(tmp_path):
from ouroboros.tools.core import _code_search
ctx = _make_ctx(tmp_path)
result = _code_search(ctx, "[invalid", regex=True)
assert "SEARCH_ERROR" in result
# ---------------------------------------------------------------------------
# Initial tool visibility
# ---------------------------------------------------------------------------
def test_search_code_in_initial_schemas():
"""search_code must appear in initial tool schemas."""
from ouroboros.tools.registry import ToolRegistry
from ouroboros.tool_policy import initial_tool_schemas
tmp = pathlib.Path(tempfile.mkdtemp())
registry = ToolRegistry(repo_dir=tmp, drive_root=tmp)
names = {s["function"]["name"] for s in initial_tool_schemas(registry)}
assert "search_code" in names
def test_search_code_registered():
"""search_code must be registered in the tool registry."""
from ouroboros.tools.registry import ToolRegistry
tmp = pathlib.Path(tempfile.mkdtemp())
registry = ToolRegistry(repo_dir=tmp, drive_root=tmp)
available = {t["function"]["name"] for t in registry.schemas()}
assert "search_code" in available
def test_search_code_ripgrep_path_filters_protected_files(tmp_path, monkeypatch):
"""The rg fast path must receive only files that passed Ouroboros gates."""
import json
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools.registry import ToolRegistry
from ouroboros.tool_capabilities import LOCAL_READONLY_SUBAGENT_MODE
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
(repo / "safe.py").write_text("needle public\n", encoding="utf-8")
(repo / "auth").mkdir()
(repo / "auth" / "secret.py").write_text("needle secret\n", encoding="utf-8")
seen = tmp_path / "seen.json"
fake_rg_py = tmp_path / "fake_rg.py"
fake_rg_py.write_text(
"#!/usr/bin/env python3\n"
"import json, pathlib, sys\n"
"args=sys.argv[1:]\n"
"needle=args[args.index('--')+1]\n"
"paths=args[args.index('--')+2:]\n"
f"pathlib.Path({str(seen)!r}).write_text(json.dumps(paths))\n"
"for p in paths:\n"
" text=pathlib.Path(p).read_text(errors='replace')\n"
" if needle in text:\n"
" print(json.dumps({'type':'match','data':{'path':{'text':p},'line_number':1,'lines':{'text':text.splitlines()[0]+'\\\\n'}}}))\n",
encoding="utf-8",
)
fake_rg_py.chmod(0o755)
if os.name == "nt":
fake_rg = tmp_path / "fake_rg.cmd"
fake_rg.write_text(f"@echo off\r\n\"{sys.executable}\" \"{fake_rg_py}\" %*\r\n", encoding="utf-8")
else:
fake_rg = fake_rg_py
monkeypatch.setattr("ouroboros.code_search_rg._rg_binary", lambda: str(fake_rg))
registry = ToolRegistry(repo_dir=repo, drive_root=data)
registry._ctx.task_constraint = TaskConstraint(mode=LOCAL_READONLY_SUBAGENT_MODE)
result = registry.execute("search_code", {"query": "needle"})
assert "safe.py" in result
assert "auth/secret.py" not in result
assert all("auth/secret.py" not in path for path in json.loads(seen.read_text(encoding="utf-8")))
def test_search_code_ripgrep_fallback_when_unavailable(tmp_path, monkeypatch):
from ouroboros.tools.registry import ToolRegistry
repo = tmp_path / "repo"
data = tmp_path / "data"
repo.mkdir()
(repo / "safe.py").write_text("needle public\n", encoding="utf-8")
monkeypatch.setattr("ouroboros.code_search_rg._rg_binary", lambda: "")
registry = ToolRegistry(repo_dir=repo, drive_root=data)
result = registry.execute("search_code", {"query": "needle"})
assert "safe.py" in result
assert "files searched" in result
@pytest.mark.skipif(os.name == "nt", reason="POSIX symlink semantics")
def test_search_code_does_not_follow_symlink_outside_root(tmp_path, monkeypatch):
"""A symlink inside the workspace that points OUTSIDE the resource root must not
be read by search_code (rg path resolved-containment + is_search_skippable)."""
from ouroboros.tools.registry import ToolRegistry
repo = tmp_path / "repo"
repo.mkdir()
data = tmp_path / "data"
outside = tmp_path / "outside_secret.txt"
outside.write_text("needle CONFIDENTIAL_OUTSIDE\n", encoding="utf-8")
(repo / "in_root.txt").write_text("needle in_root_ok\n", encoding="utf-8")
(repo / "escape.txt").symlink_to(outside) # symlink whose target escapes the root
registry = ToolRegistry(repo_dir=repo, drive_root=data)
# rg path
result = registry.execute("search_code", {"query": "needle"})
assert "in_root_ok" in result
assert "CONFIDENTIAL_OUTSIDE" not in result
# python fallback path (rg unavailable) must also refuse the symlink
monkeypatch.setattr("ouroboros.code_search_rg._rg_binary", lambda: "")
fallback = registry.execute("search_code", {"query": "needle"})
assert "CONFIDENTIAL_OUTSIDE" not in fallback

View file

@ -0,0 +1,321 @@
"""The subagent scheduling surface: schedule_subagent, wait_task, get_task_result.
Split verbatim out of ``tests/test_tool_capabilities.py`` by theme. This
module owns which control tools a scheduling principal sees and may call:
core membership, registry and schema visibility, the required-capability
fail-fast, the top-level control surface under workspace focus, executor-ref
inheritance, and the capability omission manifest.
"""
import json
# ---------------------------------------------------------------------------
# schedule_subagent core classification tests
# ---------------------------------------------------------------------------
def test_schedule_subagent_in_core():
"""schedule_subagent is core for first-class parallel delegation."""
from ouroboros.tool_capabilities import CORE_TOOL_NAMES
assert "schedule_subagent" in CORE_TOOL_NAMES
def test_wait_task_in_core():
"""wait_task/wait_tasks are core so delegated work can be joined."""
from ouroboros.tool_capabilities import CORE_TOOL_NAMES
assert "wait_task" in CORE_TOOL_NAMES
assert "wait_tasks" in CORE_TOOL_NAMES
def test_get_task_result_in_core():
"""get_task_result is core so child handoffs can be read."""
from ouroboros.tool_capabilities import CORE_TOOL_NAMES
assert "get_task_result" in CORE_TOOL_NAMES
def test_schedule_subagent_available_in_registry():
"""schedule_subagent must still be registered."""
from ouroboros.tools.registry import ToolRegistry
import pathlib, tempfile
tmp = pathlib.Path(tempfile.mkdtemp())
registry = ToolRegistry(repo_dir=tmp, drive_root=tmp)
all_names = {t["function"]["name"] for t in registry.schemas()}
assert "schedule_subagent" in all_names, (
"schedule_subagent must be discoverable via list_available_tools / enable_tools"
)
def test_schedule_subagent_in_initial_schemas():
"""schedule_subagent appears in parent initial schemas as a core tool."""
from ouroboros.tools.registry import ToolRegistry
from ouroboros.tool_policy import initial_tool_schemas
import pathlib, tempfile
tmp = pathlib.Path(tempfile.mkdtemp())
registry = ToolRegistry(repo_dir=tmp, drive_root=tmp)
names = {s["function"]["name"] for s in initial_tool_schemas(registry)}
assert "schedule_subagent" in names
assert {"peek_task", "cancel_task", "discard_child_result"} <= names
schedule_schema = next(s for s in initial_tool_schemas(registry) if s["function"]["name"] == "schedule_subagent")
props = schedule_schema["function"]["parameters"]["properties"]
assert "required_capabilities" in props
assert "shell" in props["required_capabilities"]["items"]["enum"]
def test_schedule_subagent_required_capabilities_fail_fast_for_readonly(tmp_path, monkeypatch):
from ouroboros.tools.control import _schedule_task
from ouroboros.tools.registry import ToolContext
from tests._shared import configure_test_subagent
subagent_id = configure_test_subagent(monkeypatch)
ctx = ToolContext(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
ctx.repo_dir.mkdir(parents=True)
ctx.drive_root.mkdir(parents=True)
result = _schedule_task(
ctx,
subagent_id=subagent_id,
objective="Need git diff",
expected_output="diff summary",
required_capabilities=["shell", "vcs"],
write_surface="read_only",
)
assert "SUBAGENT_CAPABILITY_MISMATCH" in result
def test_schedule_subagent_required_delegate_capability_is_satisfied_for_readonly(tmp_path, monkeypatch):
from ouroboros.tools.control import _schedule_task
from ouroboros.tools.registry import ToolContext
from tests._shared import configure_test_subagent
subagent_id = configure_test_subagent(monkeypatch)
events = []
ctx = ToolContext(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
ctx.repo_dir.mkdir(parents=True)
ctx.drive_root.mkdir(parents=True)
ctx.pending_events = events
ctx.task_id = "parent1"
result = _schedule_task(
ctx,
subagent_id=subagent_id,
objective="Delegate deeper readonly work",
expected_output="child id",
required_capabilities=["delegate"],
write_surface="read_only",
)
assert "SUBAGENT_CAPABILITY_MISMATCH" not in result
assert events and events[0]["type"] == "schedule_subagent"
assert events[0]["required_capabilities"] == ["delegate"]
def test_schedule_subagent_required_vcs_capability_is_satisfied_for_readonly(tmp_path, monkeypatch):
from ouroboros.tools.control import _schedule_task
from ouroboros.tools.registry import ToolContext
from tests._shared import configure_test_subagent
subagent_id = configure_test_subagent(monkeypatch)
events = []
ctx = ToolContext(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
ctx.repo_dir.mkdir(parents=True)
ctx.drive_root.mkdir(parents=True)
ctx.pending_events = events
ctx.task_id = "parent1"
result = _schedule_task(
ctx,
subagent_id=subagent_id,
objective="Inspect git status in readonly child",
expected_output="status summary",
required_capabilities=["vcs"],
write_surface="read_only",
)
assert "SUBAGENT_CAPABILITY_MISMATCH" not in result
assert events and events[0]["required_capabilities"] == ["vcs"]
def test_local_readonly_subagent_initial_schemas_are_allowlisted(tmp_path):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tool_capabilities import LOCAL_READONLY_SUBAGENT_TOOL_NAMES
from ouroboros.tool_policy import initial_tool_schemas, list_non_core_tools
from ouroboros.tools.registry import ToolContext, ToolRegistry
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
registry.set_context(
ToolContext(
repo_dir=tmp_path,
drive_root=tmp_path,
task_constraint=TaskConstraint(mode="local_readonly_subagent", allow_enable=False),
)
)
names = {s["function"]["name"] for s in initial_tool_schemas(registry)}
assert LOCAL_READONLY_SUBAGENT_TOOL_NAMES <= names
assert "enable_tools" not in names
assert "schedule_subagent" in names
assert "verify_and_record" not in names
assert "write_file" not in names
assert "run_command" not in names
assert "browse_page" in names
assert "browser_action" in names
schemas = {s["function"]["name"]: s["function"] for s in initial_tool_schemas(registry)}
for tool_name in ("read_file", "list_files", "search_code"):
root_enum = schemas[tool_name]["parameters"]["properties"]["root"]["enum"]
assert "user_files" not in root_enum
assert set(schemas["search_code"]["parameters"]["properties"]["root"]["enum"]) == {"active_workspace", "system_repo", "skill_payload"}
action_schema = schemas["browser_action"]["parameters"]["properties"]["action"]
assert "evaluate" not in action_schema["enum"]
assert "send_photo" not in schemas["browse_page"]["description"]
assert "analyze_screenshot" in schemas["browse_page"]["description"]
assert schemas["browse_page"]["parameters"]["properties"]["engine"]["enum"] == ["chromium", "webkit"]
assert "device" in schemas["browse_page"]["parameters"]["properties"]
assert list_non_core_tools(registry) == []
def test_workspace_parent_keeps_the_ordinary_top_level_control_surface(tmp_path, monkeypatch):
from ouroboros.tool_policy import initial_tool_schemas
from ouroboros.tools.registry import ToolContext, ToolRegistry
import ouroboros.mcp_client as mcp_client
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True)
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
registry.set_context(ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
))
monkeypatch.setattr(mcp_client, "ensure_configured_from_settings", lambda *args, **kwargs: None)
monkeypatch.setattr(mcp_client, "get_manager", lambda: type("_M", (), {"list_tools_for_registry": lambda self: []})())
names = {schema["function"]["name"] for schema in initial_tool_schemas(registry)}
assert "plan_task" in names
assert "task_acceptance_review" in names
assert "commit_reviewed" in names
assert "request_restart" in names
registry.override_handler("task_acceptance_review", lambda ctx=None, **_kwargs: "review-ok")
registry.override_handler("commit_reviewed", lambda ctx=None, **_kwargs: "commit-ok")
assert registry.execute("task_acceptance_review", {}) == "review-ok"
assert registry.execute("commit_reviewed", {"commit_message": "system target"}) == "commit-ok"
def test_workspace_focus_does_not_turn_top_level_cancel_into_child_only(tmp_path, monkeypatch):
from ouroboros.contracts.task_constraint import TaskConstraint
from ouroboros.tools import join_ledger
from ouroboros.tools.registry import ToolContext
system, workspace, data = tmp_path / "system", tmp_path / "workspace", tmp_path / "data"
for path in (system, workspace, data):
path.mkdir()
ctx = ToolContext(
repo_dir=system,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="parent",
)
monkeypatch.setattr(join_ledger, "_is_own_child", lambda *_a, **_k: False)
# NB: no ``write_task_result`` patch — the cancel tool no longer writes a
# status latch at all (phase A: it records a durable cancel INTENT), and the
# symbol is not imported here any more, so patching it raised AttributeError.
monkeypatch.setattr("ouroboros.tools.control._emit_control_event", lambda *_a, **_k: "live")
assert join_ledger._cancel_task(ctx, "foreign-task").startswith("Cancel requested")
ctx.task_constraint = TaskConstraint(mode="local_readonly_subagent", allow_enable=False)
assert "may only cancel its own children" in join_ledger._cancel_task(ctx, "foreign-task")
def test_schedule_subagent_inherits_workspace_executor_ref(tmp_path, monkeypatch):
from ouroboros.contracts.task_contract import build_task_contract
from ouroboros.tools.registry import ToolContext, ToolRegistry
from tests._shared import configure_test_subagent
system_repo = tmp_path / "system"
workspace = tmp_path / "workspace"
data = tmp_path / "data"
for path in (system_repo, workspace, data):
path.mkdir(parents=True)
task_contract = build_task_contract({
"resource_policy": {
"protected_artifacts": [
{
"id": "reference",
"role": "black_box_reference",
"paths": ["/workspace/executable"],
"allow": ["execute"],
}
]
}
})
executor_ref = {
"type": "docker_exec",
"id": "pb-container",
"container_name": "pb-container",
"network": "none",
"workspace_host_path": str(workspace),
"workspace_backend_path": "/workspace",
}
registry = ToolRegistry(repo_dir=system_repo, drive_root=data)
ctx = ToolContext(
repo_dir=system_repo,
drive_root=data,
workspace_root=workspace,
workspace_mode="external",
task_id="parent-task",
task_contract=task_contract,
task_metadata={"task_contract": task_contract},
executor_ref=executor_ref,
)
registry.set_context(ctx)
monkeypatch.setenv("OUROBOROS_MAX_SUBAGENT_DEPTH", "4")
subagent_id = configure_test_subagent(monkeypatch)
result = registry.execute(
"schedule_subagent",
{
"subagent_id": subagent_id,
"objective": "Inspect the workspace contract.",
"expected_output": "A concise report.",
"role": "auditor",
},
)
assert "Subagent request queued" in result
assert ctx.pending_events
event = ctx.pending_events[0]
assert event["executor_ref"] == executor_ref
assert event["metadata"]["executor_ref"] == executor_ref
child_id = event["task_id"]
persisted = json.loads((data / "task_results" / f"{child_id}.json").read_text(encoding="utf-8"))
assert persisted["executor_ref"] == executor_ref
assert persisted["task_contract"]["resource_policy"]["protected_artifacts"][0]["paths"] == ["/workspace/executable"]
def test_capability_omission_manifest_surfaces_extension_discovery_failure(tmp_path, monkeypatch):
from ouroboros import extension_loader
from ouroboros.tools import tool_discovery
from ouroboros.tools.registry import ToolRegistry
class BoomLock:
def __enter__(self):
raise RuntimeError("boom")
def __exit__(self, exc_type, exc, tb):
return False
registry = ToolRegistry(repo_dir=tmp_path / "repo", drive_root=tmp_path / "data")
monkeypatch.setattr(extension_loader, "_lock", BoomLock())
registry.schemas()
tool_discovery.set_registry(registry)
text = tool_discovery._list_available_tools(registry._ctx)
assert "CAPABILITY_OMISSION_MANIFEST" in text
assert "extensions" in text
assert "boom" in text

View file

@ -0,0 +1,148 @@
"""Facade-identity contract for the extracted tool descriptor/context owners.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656) with one identity
continuation to THIS tree's bytes: upstream added the ``alias_for`` field to
``ToolEntry`` after the reference cutoff, so the pinned descriptor contract
carries that row here (tip bytes are the truth of the transplant).
"""
from __future__ import annotations
import dataclasses
import inspect
def test_tool_descriptor_owner_facades_preserve_identity():
"""Extracted owners preserve facade identity and the characterized ABI."""
import ouroboros.tools as tools_package
from ouroboros.tools import registry, tool_catalog, tool_context
assert registry.BrowserState is tool_context.BrowserState
assert registry.ToolContext is tool_context.ToolContext
assert tools_package.ToolContext is tool_context.ToolContext
assert registry.ToolEntry is tool_catalog.ToolEntry
assert tools_package.ToolEntry is tool_catalog.ToolEntry
def field_contract(cls):
contract = []
for item in dataclasses.fields(cls):
if item.default_factory is not dataclasses.MISSING:
default = f"factory:{item.default_factory.__name__}"
elif item.default is dataclasses.MISSING:
default = "required"
elif callable(item.default):
default = f"callable:{item.default.__name__}"
else:
default = item.default
contract.append((item.name, default))
return tuple(contract)
assert field_contract(tool_context.BrowserState) == (
("pw_instance", None),
("browser", None),
("page", None),
("last_screenshot_b64", None),
)
assert field_contract(tool_catalog.ToolEntry) == (
("name", "required"),
("schema", "required"),
("handler", "required"),
("is_code_tool", False),
("timeout_sec", 360),
("mutates_worktree", False),
("alias_for", ""),
)
assert field_contract(tool_context.ToolContext) == (
("repo_dir", "required"),
("drive_root", "required"),
("branch_dev", "ouroboros"),
("system_repo_dir", None),
("workspace_root", None),
("workspace_mode", ""),
("memory_mode", ""),
("budget_drive_root", ""),
("project_id", ""),
("task_metadata", "factory:dict"),
("executor_ref", "factory:dict"),
("pending_events", "factory:list"),
("current_chat_id", None),
("current_task_type", None),
("pending_restart_reason", None),
("last_push_succeeded", False),
("last_reviewed_commit_sha", ""),
("emit_progress_fn", "callable:<lambda>"),
("active_model_override", None),
("active_effort_override", None),
("active_use_local_override", None),
("task_model_override", None),
("task_use_local_override", None),
("active_context_mode", ""),
("browser_state", "factory:BrowserState"),
("event_queue", None),
("task_id", None),
("messages", None),
("task_constraint", None),
("task_contract", "factory:dict"),
("task_depth", 0),
("is_direct_chat", False),
("is_ephemeral_turn", False),
("_review_advisory", "factory:list"),
("_review_iteration_count", 0),
("_review_history", "factory:list"),
)
assert {
name: str(inspect.signature(getattr(tool_context.ToolContext, name)))
for name in (
"active_repo_dir",
"is_workspace_mode",
"repo_path",
"drive_path",
"drive_logs",
"task_drive_root",
"workspace_executor_ref",
)
} == {
"active_repo_dir": "(self) -> 'pathlib.Path'",
"is_workspace_mode": "(self) -> 'bool'",
"repo_path": "(self, rel: 'str') -> 'pathlib.Path'",
"drive_path": "(self, rel: 'str') -> 'pathlib.Path'",
"drive_logs": "(self) -> 'pathlib.Path'",
"task_drive_root": "(self) -> 'pathlib.Path'",
"workspace_executor_ref": "(self) -> 'Dict[str, Any]'",
}
def test_registry_split_leaves_keep_protected_label_parity():
"""The D04 split moved guard/resolution bodies out of the protected,
hot-code registry without moving any of the risk: every leaf carries the
SAME safety-critical and hot-code membership as the parent (the inverse
of the L-C2 parity rule pinned in test_lc2_owner_facades.py)."""
from ouroboros.runtime_mode_policy import SAFETY_CRITICAL_PATHS
from supervisor.update_merge_policy import HOT_CODE_PATHS
parent = "ouroboros/tools/registry.py"
leaves = (
"ouroboros/tools/registry_guard_process.py",
"ouroboros/tools/registry_guards.py",
"ouroboros/tools/tool_catalog.py",
"ouroboros/tools/tool_context.py",
"ouroboros/tools/tool_resolution.py",
)
for inventory in (SAFETY_CRITICAL_PATHS, HOT_CODE_PATHS):
assert parent in inventory
for leaf in leaves:
assert leaf in inventory, leaf
# The tool_access split's parent carries NEITHER label; its leaves must
# not silently acquire one (same parity, other direction).
ta_parent = "ouroboros/tool_access.py"
ta_leaves = (
"ouroboros/tool_access_types.py",
"ouroboros/tool_access_paths.py",
"ouroboros/tool_access_roots.py",
"ouroboros/tool_access_user_files.py",
)
for inventory in (SAFETY_CRITICAL_PATHS, HOT_CODE_PATHS):
assert ta_parent not in inventory
for leaf in ta_leaves:
assert leaf not in inventory, leaf

View file

@ -465,3 +465,56 @@ def test_skill_repair_explicit_root_infers_its_existing_selector(tmp_path, monke
)
assert "repair target" in result
def test_registry_tool_resolution_owner_facades_preserve_identity():
"""The tool_resolution extraction is a semantic no-op: the registry facade
re-exports the exact objects, and the characterized signatures hold.
Carried from the v7 reference (ouroboros_v7_wip @ 9f691656); the reference's
companion test of the TYPED dispatch-path projection
(``_normalize_dispatch_path_args_result``) is deliberately NOT carried —
that machinery is part of the deferred typed-result cutover and this tree
keeps the upstream string-returning body.
"""
import inspect
from ouroboros.tools import registry, tool_resolution
names = (
"_coerce_real_path",
"active_repo_dir_for",
"system_repo_dir_for",
"_PATH_NORMALIZED_TOOLS",
"_normalize_dispatch_path_args",
"_GENERIC_VCS_TARGET_TOOLS",
"_TARGET_BINDING_OPERATIONS",
"_SKILL_LIFECYCLE_TARGET_TOOLS",
"_PROCESS_TARGET_TOOLS",
"_VERIFY_RUN_KINDS",
"_target_binding_operation",
"_build_builtin_target_binding",
"_binding_items",
"_binding_set_targets_system_repo",
"_binding_set_is_light_restricted",
"_binding_state_drive_root",
)
for name in names:
assert getattr(registry, name) is getattr(tool_resolution, name)
callables = {
"_coerce_real_path": "(value: 'Any') -> 'pathlib.Path | None'",
"active_repo_dir_for": "(ctx: 'Any') -> 'pathlib.Path'",
"system_repo_dir_for": "(ctx: 'Any') -> 'pathlib.Path'",
"_normalize_dispatch_path_args": "(ctx: 'Any', name: 'str', args: 'Dict[str, Any]') -> 'str'",
"_target_binding_operation": "(name: 'str', args: 'dict[str, Any]') -> 'str | None'",
"_build_builtin_target_binding": "(ctx: 'Any', name: 'str', args: 'dict[str, Any]') -> 'Any'",
"_binding_items": "(binding: 'Any') -> 'tuple[Any, ...]'",
"_binding_set_targets_system_repo": "(ctx: 'Any', binding: 'Any') -> 'bool'",
"_binding_set_is_light_restricted": "(ctx: 'Any', binding: 'Any') -> 'bool'",
"_binding_state_drive_root": "(ctx: 'Any', binding: 'Any') -> 'pathlib.Path'",
}
assert {
name: str(inspect.signature(getattr(tool_resolution, name)))
for name in callables
} == callables