forked from HAL9000/cleveragents-core
2688c85769
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
151 lines
4.0 KiB
Markdown
151 lines
4.0 KiB
Markdown
# 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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
@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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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.
|