Files
cleveragents-core/docs/reference/resource_handlers.md
T

173 lines
5.3 KiB
Markdown

# Resource Handlers
Resource handlers bridge resource types to sandbox provisioning. When a plan
executes, each tool's resource slot is resolved to a physical sandbox path
through the handler pipeline.
## Architecture
```
BindingResolutionService ResourceHandlerService
resolve(tool, project) resolve_binding(binding, plan_id)
│ │
▼ ▼
BindingResult(resource_id) ┌── Resource (location)
│ ResourceTypeSpec (handler, strategy)
│ │
│ ▼
│ resolve_handler("module:Class")
│ │
│ ▼
│ handler.resolve(resource, sandbox_manager)
│ │
│ ▼
└── BoundResource(sandbox_path="/tmp/sandbox/...")
```
## Handler Protocol
All handlers implement the `ResourceHandler` protocol:
```python
class ResourceHandler(Protocol):
def resolve(
self,
*,
resource: Resource,
plan_id: str,
slot_name: str,
sandbox_manager: SandboxManager,
access: str = "read_only",
) -> BoundResource: ...
```
## Built-in Handlers
### GitCheckoutHandler
Resolves `git-checkout` resources using the `git_worktree` sandbox strategy.
| Property | Value |
|----------|-------|
| Module | `cleveragents.resource.handlers.git_checkout` |
| Class | `GitCheckoutHandler` |
| Default strategy | `git_worktree` |
| Fallback strategy | `copy_on_write` |
| Required fields | `resource.location` (path to git repo root) |
### FsDirectoryHandler
Resolves `fs-directory` resources using the `copy_on_write` sandbox strategy.
| Property | Value |
|----------|-------|
| Module | `cleveragents.resource.handlers.fs_directory` |
| Class | `FsDirectoryHandler` |
| Default strategy | `copy_on_write` |
| Required fields | `resource.location` (path to directory) |
## Handler Resolution
Handler strings use the `module.path:ClassName` format and are stored on
`ResourceTypeSpec.handler`. Resolution is dynamic via `importlib`:
```python
from cleveragents.resource.handlers import resolve_handler
handler = resolve_handler(
"cleveragents.resource.handlers.git_checkout:GitCheckoutHandler"
)
```
Resolved handlers are cached for the process lifetime. Call
`clear_handler_cache()` to reset.
### Fallback Behavior
If no handler string is set (or resolution fails), the
`ResourceHandlerService` uses a default handler that delegates directly
to `SandboxManager` using the type's `sandbox_strategy`.
## Strategy Precedence
The sandbox strategy is determined in this order:
1. **Resource-level override** (`resource.sandbox_strategy`) — per-resource
2. **Type default** (`ResourceTypeSpec.sandbox_strategy`) — per-type
3. **Fallback** (`none`) — no sandboxing
## ResourceHandlerService
The orchestration service that chains the resolution pipeline:
```python
from cleveragents.application.services.resource_handler_service import (
ResourceHandlerService,
)
service = ResourceHandlerService(
sandbox_manager=sandbox_manager,
resource_lookup=registry_service.show_resource,
type_lookup=registry_service.show_type,
)
# Resolve a single binding
bound = service.resolve_binding(binding, plan_id="01ARZ3...")
# Resolve all bindings for a tool
bindings_map = service.resolve_bindings(bindings, plan_id="01ARZ3...")
# -> {"repo": BoundResource(sandbox_path="/tmp/sandbox/...")}
```
## Sandbox Outputs
After resolution, each `BoundResource` carries:
| Field | Description |
|-------|-------------|
| `slot_name` | Tool slot this binding fills |
| `resource_id` | ULID of the resolved resource |
| `resource_type` | Type name (e.g. `git-checkout`) |
| `sandbox_path` | Root path of the provisioned sandbox |
| `access` | `read_only` or `read_write` |
The `sandbox_path` points to an isolated directory managed by
`SandboxManager`. Changes are committed or rolled back via
`SandboxManager.commit_all()` / `rollback_all()`.
## Writing Custom Handlers
To add a handler for a new resource type:
1. Create a module under `cleveragents/resource/handlers/`.
2. Implement a class satisfying `ResourceHandler`.
3. Register it on the `ResourceTypeSpec` via the `handler` field:
`"cleveragents.resource.handlers.my_handler:MyHandler"`.
```python
class MyHandler:
def resolve(
self,
*,
resource: Resource,
plan_id: str,
slot_name: str,
sandbox_manager: SandboxManager,
access: str = "read_only",
) -> BoundResource:
# Custom validation / setup
sandbox = sandbox_manager.get_or_create_sandbox(
plan_id=plan_id,
resource_id=resource.resource_id,
original_path=resource.location,
sandbox_strategy="copy_on_write",
)
return BoundResource(
slot_name=slot_name,
resource_id=resource.resource_id,
resource_type=resource.resource_type_name,
sandbox_path=sandbox.context.sandbox_path if sandbox.context else "",
access=access,
)
```