Implement SandboxStrategyProtocol, a 9-method @runtime_checkable Protocol enabling third-party sandbox strategy registration. Includes: - SandboxStrategyProtocol with create/read/write/diff/commit/rollback/checkpoint/ restore_checkpoint/cleanup methods - SandboxRef (frozen dataclass) and DiffView/DiffEntry (Pydantic models) - SandboxStrategyRegistry with config-driven registration, Protocol validation, thread safety, and clear/list/has operations - BuiltInSandboxStrategyAdapter wrapping existing Sandbox implementations to conform to the new Protocol - CustomStrategyConfig for YAML/dict-based strategy registration - SandboxFactory integration with custom_registry parameter, has_custom_strategy() and get_custom_strategy_class() - 25 Behave BDD scenarios (85 steps) covering protocol, registry, adapter, config, and factory integration - 8 Robot Framework integration tests with real filesystem operations - ASV benchmarks for registry and adapter operations - Developer documentation ISSUES CLOSED: #586
4.0 KiB
Custom Sandbox Strategy Registration
Overview
CleverAgents supports custom sandbox strategies via the SandboxStrategyProtocol.
Custom strategies enable domain-specific isolation for specialized resource types
that are not covered by the built-in strategies (none, git_worktree, copy_on_write,
transaction_rollback).
SandboxStrategy Protocol
All custom sandbox strategies must implement the 9-method SandboxStrategyProtocol:
from cleveragents.domain.models.core.sandbox_strategy import (
SandboxStrategyProtocol,
SandboxRef,
DiffEntry,
DiffView,
)
from cleveragents.domain.models.core.resource import Resource
class MyCustomSandbox:
def create(self, plan_id: str, resource: Resource) -> SandboxRef: ...
def read(self, ref: SandboxRef, path: str) -> bytes: ...
def write(self, ref: SandboxRef, path: str, content: bytes) -> DiffEntry: ...
def diff(self, ref: SandboxRef) -> DiffView: ...
def commit(self, ref: SandboxRef) -> None: ...
def rollback(self, ref: SandboxRef) -> None: ...
def checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None: ...
def restore_checkpoint(self, ref: SandboxRef, checkpoint_id: str) -> None: ...
def cleanup(self, ref: SandboxRef) -> None: ...
Supporting Models
SandboxRef
Opaque reference returned by create() and passed to all other methods:
@dataclass(frozen=True)
class SandboxRef:
sandbox_id: str
plan_id: str
resource_id: str
created_at: datetime
metadata: dict[str, object] = field(default_factory=dict)
DiffEntry / DiffView
DiffEntry describes a single file change; DiffView aggregates entries:
class DiffEntry(BaseModel):
path: str # relative file path
operation: str # "added", "modified", or "deleted"
before: bytes | None
after: bytes | None
class DiffView(BaseModel):
sandbox_id: str
entries: list[DiffEntry]
summary: str
Config-Driven Registration
Custom strategies are registered via the SandboxStrategyRegistry:
from cleveragents.infrastructure.sandbox.strategy_registry import (
SandboxStrategyRegistry,
)
registry = SandboxStrategyRegistry(
allowed_prefixes=("cleveragents.", "mycompany."),
)
# Single registration
registry.register(
"my_strategy",
"mycompany.sandbox.custom",
"MyCustomSandbox",
)
# Batch config-driven registration
configs = {
"redis_sandbox": {
"module": "mycompany.sandbox.redis",
"class": "RedisSandbox",
},
"s3_sandbox": {
"module": "mycompany.sandbox.s3",
"class": "S3Sandbox",
},
}
registry.register_all_from_config(configs)
Factory Integration
The SandboxFactory can be initialized with a custom registry:
from cleveragents.infrastructure.sandbox.factory import SandboxFactory
factory = SandboxFactory(custom_registry=registry)
# Check if custom strategy is available
if factory.has_custom_strategy("redis_sandbox"):
cls = factory.get_custom_strategy_class("redis_sandbox")
Built-In Strategy Adapter
The BuiltInSandboxStrategyAdapter wraps existing Sandbox implementations
to conform to the SandboxStrategyProtocol:
from cleveragents.infrastructure.sandbox.strategy_adapter import (
BuiltInSandboxStrategyAdapter,
)
adapter = BuiltInSandboxStrategyAdapter(strategy_name="copy_on_write")
ref = adapter.create("plan-001", resource)
adapter.write(ref, "file.txt", b"content")
data = adapter.read(ref, "file.txt")
adapter.cleanup(ref)
Protocol Validation
All strategies are validated against the Protocol at registration time. The registry checks that all 9 required methods are present as callable attributes:
# This will raise ProtocolMismatchError if the class
# doesn't implement all required methods:
registry.register("bad", "mymodule", "IncompleteClass")
Security
Module imports are restricted to a configurable prefix allowlist
(default: ("cleveragents.",)). Only modules whose fully-qualified
name starts with an allowed prefix may be dynamically imported.