Files
cleveragents-core/robot/helper_sandbox_boundary.py
T
freemo 5935940276
CI / lint (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 31s
CI / security (pull_request) Successful in 29s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 1m21s
CI / quality (pull_request) Successful in 2m33s
CI / unit_tests (pull_request) Successful in 3m10s
CI / integration_tests (pull_request) Successful in 5m17s
CI / coverage (pull_request) Successful in 4m25s
CI / docker (pull_request) Successful in 40s
CI / lint (push) Successful in 12s
CI / typecheck (push) Successful in 31s
CI / quality (push) Successful in 15s
CI / security (push) Successful in 32s
CI / build (push) Successful in 21s
CI / integration_tests (push) Successful in 3m6s
CI / unit_tests (push) Successful in 3m30s
CI / benchmark-regression (push) Has been skipped
CI / docker (push) Successful in 1m43s
CI / coverage (push) Successful in 5m21s
CI / benchmark-regression (pull_request) Successful in 31m11s
CI / benchmark-publish (push) Successful in 17m24s
feat(sandbox): implement sandbox boundary algebra and domain computation
Implement sandbox_boundary(r) function that walks up containment edges
in the resource DAG to the nearest sandboxable ancestor, enabling
resources sharing a boundary to share one sandbox instance.

Changes:
- Add boundary.py: is_sandbox_boundary(), sandbox_boundary(),
  compute_sandbox_domains(), BoundaryCache (thread-safe, per-execution)
- Update SandboxManager: resolve_sandbox_key() and
  get_or_create_sandbox_for_resource() key by (plan_id, boundary_id)
  instead of (plan_id, resource_id); boundary cache lifecycle methods
- Define "sandboxable" via ResourceCapabilities.sandboxable + non-none
  sandbox_strategy as per specification section 24659-24674
- Export new symbols from sandbox __init__.py
- Add vulture whitelist entries for new public API

Tests:
- 26 Behave BDD scenarios (features/sandbox_boundary_algebra.feature)
- 5 Robot Framework integration tests (robot/sandbox_boundary_algebra.robot)
- ASV benchmarks for boundary walk, domain grouping, and cache performance

ISSUES CLOSED: #548
2026-03-04 15:59:15 +00:00

282 lines
8.5 KiB
Python

"""Helper utilities for sandbox boundary algebra Robot integration tests.
Covers the integration between:
- sandbox_boundary() function
- compute_sandbox_domains() grouping
- BoundaryCache caching
- SandboxManager boundary-aware keying
"""
from __future__ import annotations
import sys
from cleveragents.domain.models.core.resource import (
PhysVirt,
Resource,
ResourceCapabilities,
SandboxStrategy,
)
from cleveragents.infrastructure.sandbox.boundary import (
BoundaryCache,
NoSandboxBoundaryError,
compute_sandbox_domains,
is_sandbox_boundary,
sandbox_boundary,
)
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
from cleveragents.infrastructure.sandbox.manager import SandboxManager
_COUNTER = 0
def _ulid(name: str) -> str:
"""Generate a deterministic ULID for test resources."""
global _COUNTER
_COUNTER += 1
base = abs(hash(name)) % (10**20)
raw = f"{_COUNTER:06d}{base:020d}"
charset = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
result = ""
for ch in raw:
result += charset[int(ch)]
return (result + "0" * 26)[:26]
def _resource(
name: str,
sandboxable: bool = False,
strategy: str = "none",
parents: list[str] | None = None,
location: str | None = None,
) -> Resource:
return Resource(
resource_id=_ulid(name),
name=name,
resource_type_name="git-checkout" if sandboxable else "fs-file",
classification=PhysVirt.PHYSICAL,
sandbox_strategy=SandboxStrategy(strategy),
capabilities=ResourceCapabilities(
readable=True,
writable=True,
sandboxable=sandboxable,
checkpointable=False,
),
parents=parents or [],
location=location,
)
def _test_boundary_walk() -> None:
"""Integration: boundary walks up to nearest sandboxable ancestor."""
checkout = _resource("checkout", sandboxable=True, strategy="git_worktree")
directory = _resource("dir", sandboxable=False, parents=[checkout.resource_id])
file_res = _resource("file", sandboxable=False, parents=[directory.resource_id])
registry = {
checkout.resource_id: checkout,
directory.resource_id: directory,
file_res.resource_id: file_res,
}
assert is_sandbox_boundary(checkout)
assert not is_sandbox_boundary(directory)
assert not is_sandbox_boundary(file_res)
boundary = sandbox_boundary(file_res, registry)
assert boundary is not None
assert boundary.resource_id == checkout.resource_id
# Self-boundary
self_boundary = sandbox_boundary(checkout, registry)
assert self_boundary is not None
assert self_boundary.resource_id == checkout.resource_id
print("boundary-walk-ok")
def _test_domain_grouping() -> None:
"""Integration: domain grouping partitions resources by boundary."""
co1 = _resource("co1", sandboxable=True, strategy="git_worktree")
co2 = _resource("co2", sandboxable=True, strategy="copy_on_write")
f1 = _resource("f1", parents=[co1.resource_id])
f2 = _resource("f2", parents=[co1.resource_id])
f3 = _resource("f3", parents=[co2.resource_id])
orphan = _resource("orphan")
registry = {r.resource_id: r for r in [co1, co2, f1, f2, f3, orphan]}
domains = compute_sandbox_domains([f1, f2, f3, orphan], registry)
assert co1.resource_id in domains
assert len(domains[co1.resource_id]) == 2
assert co2.resource_id in domains
assert len(domains[co2.resource_id]) == 1
assert "__unsandboxable__" in domains
assert len(domains["__unsandboxable__"]) == 1
print("domain-grouping-ok")
def _test_boundary_cache() -> None:
"""Integration: boundary cache stores and retrieves results."""
checkout = _resource("co-cache", sandboxable=True, strategy="git_worktree")
file_res = _resource("f-cache", parents=[checkout.resource_id])
registry = {
checkout.resource_id: checkout,
file_res.resource_id: file_res,
}
cache = BoundaryCache()
assert cache.size == 0
result = cache.get_boundary(file_res, registry)
assert result is not None
assert result.resource_id == checkout.resource_id
assert cache.size == 1
# Cache hit
result2 = cache.get_boundary(file_res, registry)
assert result2 is not None
assert result2.resource_id == checkout.resource_id
assert cache.size == 1
cache.clear()
assert cache.size == 0
print("boundary-cache-ok")
def _test_manager_boundary_keying() -> None:
"""Integration: SandboxManager resolves keys via boundary algebra.
Uses ``resolve_sandbox_key`` (no real sandbox creation) and then
``get_or_create_sandbox`` with the resolved boundary to demonstrate
that two child resources share the same sandbox.
"""
checkout = _resource(
"co-mgr",
sandboxable=True,
strategy="git_worktree",
location="/tmp",
)
f1 = _resource("f1-mgr", parents=[checkout.resource_id])
f2 = _resource("f2-mgr", parents=[checkout.resource_id])
registry = {
checkout.resource_id: checkout,
f1.resource_id: f1,
f2.resource_id: f2,
}
factory = SandboxFactory()
manager = SandboxManager(factory=factory, cleanup_on_exit=False)
key1 = manager.resolve_sandbox_key(f1, registry)
key2 = manager.resolve_sandbox_key(f2, registry)
assert key1 == checkout.resource_id
assert key2 == checkout.resource_id
# Both files resolve to the same key — verify they would share a sandbox
# by using get_or_create_sandbox with the resolved key and "none" strategy
# (to avoid needing a real git repo).
sb1 = manager.get_or_create_sandbox(
plan_id="plan-1",
resource_id=key1,
original_path="/tmp",
sandbox_strategy="none",
)
sb2 = manager.get_or_create_sandbox(
plan_id="plan-1",
resource_id=key2,
original_path="/tmp",
sandbox_strategy="none",
)
assert sb1 is sb2
# Orphan raises NoSandboxBoundaryError.
orphan = _resource("orphan-mgr")
registry[orphan.resource_id] = orphan
try:
manager.resolve_sandbox_key(orphan, registry)
print("FAIL: expected NoSandboxBoundaryError")
return
except NoSandboxBoundaryError:
pass
manager.cleanup_all("plan-1")
print("manager-boundary-keying-ok")
def _test_multi_resource_plan() -> None:
"""Integration: multi-resource plan with separate sandbox domains.
Uses resolve_sandbox_key to verify two different boundaries produce
different keys, then creates sandboxes using the keys directly.
"""
co_git = _resource(
"co-git",
sandboxable=True,
strategy="git_worktree",
location="/tmp",
)
co_fs = _resource(
"co-fs",
sandboxable=True,
strategy="copy_on_write",
location="/tmp",
)
f_git = _resource("f-git", parents=[co_git.resource_id])
f_fs = _resource("f-fs", parents=[co_fs.resource_id])
registry = {r.resource_id: r for r in [co_git, co_fs, f_git, f_fs]}
factory = SandboxFactory()
manager = SandboxManager(factory=factory, cleanup_on_exit=False)
key_git = manager.resolve_sandbox_key(f_git, registry)
key_fs = manager.resolve_sandbox_key(f_fs, registry)
assert key_git != key_fs
assert key_git == co_git.resource_id
assert key_fs == co_fs.resource_id
# Create sandboxes via resolved keys using "none" strategy (test env).
sb_git = manager.get_or_create_sandbox(
plan_id="plan-2",
resource_id=key_git,
original_path="/tmp",
sandbox_strategy="none",
)
sb_fs = manager.get_or_create_sandbox(
plan_id="plan-2",
resource_id=key_fs,
original_path="/tmp",
sandbox_strategy="none",
)
# Different boundaries → different sandboxes.
assert sb_git is not sb_fs
sandboxes = manager.list_sandboxes("plan-2")
assert len(sandboxes) == 2
manager.cleanup_all("plan-2")
print("multi-resource-plan-ok")
_COMMANDS = {
"boundary-walk": _test_boundary_walk,
"domain-grouping": _test_domain_grouping,
"boundary-cache": _test_boundary_cache,
"manager-boundary-keying": _test_manager_boundary_keying,
"multi-resource-plan": _test_multi_resource_plan,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <command>")
print(f"Commands: {', '.join(_COMMANDS)}")
sys.exit(1)
cmd = sys.argv[1]
fn = _COMMANDS.get(cmd)
if fn is None:
print(f"Unknown command: {cmd}")
sys.exit(1)
fn()