5.3 KiB
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:
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:
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:
- Resource-level override (
resource.sandbox_strategy) — per-resource - Type default (
ResourceTypeSpec.sandbox_strategy) — per-type - Fallback (
none) — no sandboxing
ResourceHandlerService
The orchestration service that chains the resolution pipeline:
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:
- Create a module under
cleveragents/resource/handlers/. - Implement a class satisfying
ResourceHandler. - Register it on the
ResourceTypeSpecvia thehandlerfield:"cleveragents.resource.handlers.my_handler:MyHandler".
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,
)