mirror of
https://github.com/razzant/ouroboros.git
synced 2026-08-04 16:19:50 +00:00
Structural, additive, default-on fixes derived from a forensic analysis of three benchmark runs (SWE-bench-Pro, SWE-Pro, Terminal-bench). The coding quality was already high; the score drag was a handful of real-usage defects. Each fix is a meta-solution per BIBLE (P2 meta-over-patch, P5 LLM-first, P7 minimalism), not a per-bug prompt crutch. T1 — honest task-acceptance review: - `outcome_tier` + `completion_coach` are now REQUIRED reviewer JSON keys (the best_effort completion-coach branch was dead because models ignored the prose). - EVIDENCE-INDEPENDENCE and ENVIRONMENT-vs-DELIVERABLE reviewer directives (pure LLM judgment, no regex on test paths). - Host-forced `required` review is LABEL-ONLY: it records the verdict/tier on the objective axis but no longer injects the review into the transcript or forces an extra model round (that re-loop made the agent rewrite its deliverable into a meta-essay and tanked metrics). `auto` stays truly auto. - Aggregation splits parse_degraded vs actor_error so one bad slot can't drop a clean quorum; the objective axis aggregates outcome_tier from only the CONTRIBUTING PASS actors on a PASS run. - The reviewer is fed a host-collected turn diff (tracked + untracked new files; secrets redacted; any agent-supplied diff kept only under a labeled non-independent key so EVIDENCE-INDEPENDENCE can't be steered). T2 — path normalization for any external workspace: - One shared `normalize_root_relative(root, path)` helper (relative string only, never widens access) applied ONCE at the dispatch boundary so the handler and every guard (protected-path, protected-artifact, shrink guard) resolve the same target — closing a normalize-vs-guard desync class. Covers search_code/list_files/query_code/read_file/write_file/edit_text. T3 — per-call shell timeout: - run_command/run_script accept `timeout_sec` (alias `timeout`), clamped by the deadline and `OUROBOROS_PER_CALL_TIMEOUT_CEILING_SEC`; the outer tool-execution timeout rises to match so long approved commands aren't cut at the static cap. T4 — outcome-axis honesty: - A one-shot `run_command`/`run_script` non-zero exit/shell error becomes a non-degrading `execution.cosmetic_tool_errors` record (timeout stays blocking); the execution axis means harness/capability health, while "did it work?" lives on the objective/review axis. A structural `objective.warning = residual_tool_errors_without_review` is surfaced (UI severity → warn) when cosmetic errors exist and no review judged the objective. T7 — core workspace.patch hygiene: - Untracked build binaries, >5 MiB blobs, and junk artifacts are excluded and recorded in the manifest (never silently lost); `_PATCH_EXCLUDE_RULES_VERSION` bumped to 2. Verification: targeted + broad isolated pytest green; Ouroboros's own triad (gpt-5.5 + gemini-3.5-flash + opus-4.8) and scope (gpt-5.5) review converged clean after fixing 6 review-surfaced edge cases; claudexor codex (gpt-5.5) final review. All version carriers synced to 6.35.0.
173 lines
8.7 KiB
Python
173 lines
8.7 KiB
Python
"""T2 (v6.35.0): workspace-relative path normalization for structural/read tools.
|
|
|
|
An agent working in an external workspace naturally writes absolute paths
|
|
(``/app/foo``) or a redundant root-basename prefix (``app/foo``). Before this
|
|
change ``search_code``/``list_files``/``query_code`` rejected those
|
|
(``SEARCH_ERROR: path not found: active_workspace:app`` / ``path escapes root``).
|
|
``normalize_root_relative`` maps such paths to the root-relative form WITHOUT
|
|
widening access: genuine escapes (outside-root, ``..``-traversal, symlink) are
|
|
still rejected by the unchanged ``safe_relpath`` + ``relative_to`` confinement.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ouroboros.tool_access import normalize_root_relative
|
|
from ouroboros.tools.registry import ToolContext, ToolRegistry
|
|
|
|
|
|
def test_normalize_root_relative_table(tmp_path):
|
|
root = tmp_path / "app"
|
|
(root / "foo").mkdir(parents=True)
|
|
(root / "foo" / "bar.txt").write_text("x", encoding="utf-8")
|
|
base = root.name # "app"
|
|
|
|
assert normalize_root_relative(root, str(root / "foo")) == "foo" # abs inside -> rel
|
|
assert normalize_root_relative(root, str(root)) == "." # abs == root -> .
|
|
assert normalize_root_relative(root, f"{base}/foo") == "foo" # redundant prefix -> stripped
|
|
assert normalize_root_relative(root, "foo/bar.txt") == "foo/bar.txt" # relative unchanged
|
|
assert normalize_root_relative(root, "/etc/passwd") == "/etc/passwd" # abs outside -> unchanged
|
|
assert normalize_root_relative(root, "foo") == "foo" # plain relative unchanged
|
|
assert normalize_root_relative(root, ".") == "."
|
|
# redundant prefix for a NEW (not-yet-existing) target still normalizes, so
|
|
# write_file/edit_text create-paths work, since there's no real <base>/ subdir.
|
|
assert normalize_root_relative(root, f"{base}/new.py") == "new.py"
|
|
|
|
# A REAL same-named subdir makes '<base>/x' genuinely ambiguous -> kept.
|
|
(root / base).mkdir()
|
|
assert normalize_root_relative(root, f"{base}/inner.py") == f"{base}/inner.py"
|
|
|
|
|
|
def _ext_registry(tmp_path):
|
|
system = tmp_path / "system"
|
|
workspace = tmp_path / "workspace"
|
|
data = tmp_path / "data"
|
|
for p in (system, workspace, data):
|
|
p.mkdir()
|
|
(data / "settings.json").write_text("{}", encoding="utf-8")
|
|
(workspace / "pkg").mkdir()
|
|
(workspace / "pkg" / "mod.py").write_text("def hello_world():\n return 1\n", encoding="utf-8")
|
|
reg = ToolRegistry(repo_dir=system, drive_root=data)
|
|
reg.set_context(ToolContext(repo_dir=system, drive_root=data, workspace_root=workspace, workspace_mode="external"))
|
|
return reg, workspace
|
|
|
|
|
|
def test_search_code_accepts_absolute_and_redundant_paths(tmp_path):
|
|
reg, ws = _ext_registry(tmp_path)
|
|
base = ws.name
|
|
# absolute within root
|
|
out_abs = reg.execute("search_code", {"query": "hello_world", "path": str(ws / "pkg"), "root": "active_workspace"})
|
|
assert "SEARCH_ERROR" not in out_abs and "hello_world" in out_abs
|
|
# redundant root-basename prefix
|
|
out_red = reg.execute("search_code", {"query": "hello_world", "path": f"{base}/pkg", "root": "active_workspace"})
|
|
assert "SEARCH_ERROR" not in out_red and "hello_world" in out_red
|
|
# outside-root absolute path is still rejected (no leak)
|
|
out_bad = reg.execute("search_code", {"query": "root", "path": "/etc", "root": "active_workspace"})
|
|
assert "SEARCH_ERROR" in out_bad
|
|
|
|
|
|
def test_list_files_accepts_absolute_dir(tmp_path):
|
|
reg, ws = _ext_registry(tmp_path)
|
|
out = reg.execute("list_files", {"path": str(ws / "pkg"), "root": "active_workspace"})
|
|
assert "Directory not found" not in out and "Not a directory" not in out
|
|
assert "mod.py" in out
|
|
|
|
|
|
def test_read_file_accepts_absolute_and_redundant(tmp_path):
|
|
reg, ws = _ext_registry(tmp_path)
|
|
base = ws.name
|
|
out_abs = reg.execute("read_file", {"path": str(ws / "pkg" / "mod.py"), "root": "active_workspace"})
|
|
assert "hello_world" in out_abs
|
|
out_red = reg.execute("read_file", {"path": f"{base}/pkg/mod.py", "root": "active_workspace"})
|
|
assert "hello_world" in out_red
|
|
|
|
|
|
def test_resolve_resource_path_normalizes_like_repo_path(tmp_path):
|
|
"""v6.35.0 security: resolve_resource_path (used by the protected-artifact
|
|
guards) must normalize absolute/redundant-root-prefix paths the SAME way the
|
|
write path (repo_path) does, so a guard resolving through it sees the same
|
|
target the handler touches — no desync bypass for a protected_artifacts path."""
|
|
from ouroboros.tool_access import resolve_resource_path
|
|
|
|
base = tmp_path / "repo"
|
|
base.mkdir()
|
|
ctx = ToolContext(repo_dir=base, drive_root=tmp_path / "d")
|
|
(tmp_path / "d").mkdir()
|
|
# redundant root-basename prefix -> real target (matches repo_path resolution)
|
|
assert resolve_resource_path(ctx, root="active_workspace", path="repo/secret.bin") == (base / "secret.bin").resolve()
|
|
# absolute-inside-root -> real target
|
|
assert resolve_resource_path(ctx, root="active_workspace", path=str(base / "secret.bin")) == (base / "secret.bin").resolve()
|
|
# plain relative unchanged
|
|
assert resolve_resource_path(ctx, root="active_workspace", path="sub/x.txt") == (base / "sub" / "x.txt").resolve()
|
|
|
|
|
|
def test_resolve_resource_path_keeps_non_repo_roots_raw(tmp_path):
|
|
"""v6.35.0 security: resolve_resource_path must NOT strip a redundant
|
|
drive-basename prefix for NON-repo roots (runtime_data, ...). Their operations
|
|
(_data_read via _normalize_data_read_path) resolve the RAW path, so normalizing
|
|
only the guard side here would desync the guard from the operation."""
|
|
from ouroboros.tool_access import resolve_resource_path
|
|
|
|
data = tmp_path / "data"
|
|
system = tmp_path / "system"
|
|
for p in (data, system):
|
|
p.mkdir()
|
|
ctx = ToolContext(repo_dir=system, drive_root=data)
|
|
# runtime_data root resolves to drive_root; a redundant 'data/' is kept RAW.
|
|
assert resolve_resource_path(ctx, root="runtime_data", path="data/x") == (data / "data" / "x").resolve()
|
|
# repo roots still normalize (consistent with the dispatch boundary).
|
|
assert resolve_resource_path(ctx, root="system_repo", path=f"{system.name}/y") == (system / "y").resolve()
|
|
|
|
|
|
def test_search_code_runtime_data_redundant_prefix_cannot_reach_project_store(tmp_path):
|
|
"""v6.35.0 security: a redundant drive-basename prefix must NOT let a
|
|
runtime_data search slip the project-store guard (which matches the RAW path)
|
|
and then search the normalized 'projects/...' store — the desync class codex
|
|
flagged. Normalization is restricted to the repo roots."""
|
|
data = tmp_path / "data"
|
|
system = tmp_path / "system"
|
|
for p in (data, system):
|
|
p.mkdir()
|
|
(data / "settings.json").write_text("{}", encoding="utf-8")
|
|
reg = ToolRegistry(repo_dir=system, drive_root=data)
|
|
# runtime_data `search` is an operator_control (direct-chat) capability.
|
|
ctx = ToolContext(repo_dir=system, drive_root=data)
|
|
ctx.is_direct_chat = True
|
|
reg.set_context(ctx)
|
|
|
|
# Positive control: runtime_data search works for an ordinary subdir.
|
|
logs = data / "logs"
|
|
logs.mkdir()
|
|
(logs / "notes.txt").write_text("RUNTIME_MARK_OK\n", encoding="utf-8")
|
|
ok = reg.execute("search_code", {"query": "RUNTIME_MARK_OK", "path": "logs", "root": "runtime_data"})
|
|
assert "RUNTIME_MARK_OK" in ok
|
|
|
|
# The protected per-project store.
|
|
store = data / "projects" / "proj1"
|
|
store.mkdir(parents=True)
|
|
(store / "secret.txt").write_text("PROJSTORE_SECRET_ZZZ\n", encoding="utf-8")
|
|
|
|
# Direct store path is blocked by the project-store guard.
|
|
direct = reg.execute("search_code", {"query": "PROJSTORE_SECRET_ZZZ", "path": "projects/proj1", "root": "runtime_data"})
|
|
assert "PROJSTORE_SECRET_ZZZ" not in direct
|
|
# Redundant drive-basename prefix must NOT slip the guard and reach the store.
|
|
bypass = reg.execute("search_code", {"query": "PROJSTORE_SECRET_ZZZ", "path": "data/projects/proj1", "root": "runtime_data"})
|
|
assert "PROJSTORE_SECRET_ZZZ" not in bypass
|
|
|
|
|
|
def test_write_file_normalizes_redundant_prefix_for_new_file(tmp_path):
|
|
reg, ws = _ext_registry(tmp_path)
|
|
base = ws.name
|
|
out = reg.execute("write_file", {"root": "active_workspace", "path": f"{base}/created.txt", "content": "hi\n"})
|
|
assert "ERROR" not in out
|
|
# The redundant root-basename prefix is stripped: file lands at the root,
|
|
# NOT double-nested under <root>/<base>/.
|
|
assert (ws / "created.txt").is_file()
|
|
assert not (ws / base / "created.txt").exists()
|
|
|
|
|
|
def test_query_code_path_does_not_escape_on_redundant_prefix(tmp_path):
|
|
reg, ws = _ext_registry(tmp_path)
|
|
base = ws.name
|
|
out = reg.execute("query_code", {"op": "digest", "path": f"{base}/pkg/mod.py", "root": "active_workspace"})
|
|
# The redundant prefix must no longer produce a path-escape / arg error.
|
|
assert "path escapes root" not in out and "TOOL_ARG_ERROR" not in out
|