Security
Hollywood provides ToolPolicy and RestrictedExecutor for validating and restricting tool execution based on custom rules.
ToolPolicy
A ToolPolicy[T] defines validation and transformation rules for tools:
trait ToolPolicy[T <: CallableTool[?]] {
def validate(tool: T): Try[Unit]
def transformArgs(args: Json): Json = args
}
Built-in Policies
// Allow all operations
val permissive = ToolPolicy.allowAll[Calculator]
// Block all operations
val restrictive = ToolPolicy.denyAll[Calculator](
reason = "Calculator operations disabled"
)
// Custom validation
val policy = ToolPolicy.fromValidator[Calculator] { calc =>
if (calc.a < 0 || calc.b < 0) {
Failure(new SecurityException("Negative numbers not allowed"))
} else {
Success(())
}
}
// Custom validation with argument transformation
val sanitizingPolicy = ToolPolicy.custom[Calculator](
validator = calc => Success(()),
transformer = args => {
// Modify args before validation/execution
args
}
)
Use Cases
- Restrict tool operations based on input values
- Prevent access to sensitive resources
- Enforce business rules
- Sanitize or transform inputs before execution
RestrictedExecutor
RestrictedExecutor wraps a ToolExecutor to enforce a policy:
val baseExecutor = ToolExecutor.derived[Calculator]
val policy = ToolPolicy.fromValidator[Calculator] { calc =>
if (calc.a > 1000 || calc.b > 1000) {
Failure(new SecurityException("Numbers too large"))
} else {
Success(())
}
}
val restrictedExecutor = RestrictedExecutor(baseExecutor, policy)
Execution Flow
When executing tools, the RestrictedExecutor:
- Applies the policy's
transformArgsto the JSON input - Decodes the transformed arguments into the tool instance
- Validates the tool against the policy
- If validation passes, executes the tool with the delegate executor
- If validation fails, returns a policy violation error
Example with ToolRegistry
val calculator = ToolExecutor.derived[Calculator]
val policy = ToolPolicy.fromValidator[Calculator] { calc =>
if (calc.a < 0 || calc.b < 0) {
Failure(new SecurityException("Negative numbers not allowed"))
} else {
Success(())
}
}
val restricted = RestrictedExecutor(calculator, policy)
val toolRegistry = ToolRegistry()
.register(ToolSchema.derive[Calculator], restricted)
val agent = OneShotAgent(
systemPrompt = "You are a math assistant.",
toolRegistry = Some(toolRegistry)
)
// This will succeed
agent.chat("What is 5 plus 3?")
// This will fail with policy violation
agent.chat("What is -5 plus 3?")
FileSystem Security Example
When using FileSystemTool, use the provided FileSystemPolicy to restrict access:
import dev.alteration.branch.hollywood.tools.provided.fs.{FileSystemTool, FileSystemPolicy}
import dev.alteration.branch.hollywood.tools.{ToolExecutor, RestrictedExecutor}
import java.nio.file.Paths
// Create a sandboxed filesystem policy
val policy = FileSystemPolicy.strict(Paths.get("/tmp"))
// or FileSystemPolicy.default(Paths.get("/allowed/path"))
// or FileSystemPolicy.permissive(Some(Paths.get("/path")))
val executor = ToolExecutor.derived[FileSystemTool]
val restricted = RestrictedExecutor(executor, policy)
val toolRegistry = ToolRegistry()
.register(ToolSchema.derive[FileSystemTool], restricted)
FileSystemPolicy Features
- Sandboxing: Restrict operations to a specific directory tree
- Read-only mode: Block write operations entirely
- File size limits: Prevent writing excessively large files (default 10MB)
- Blocked patterns: Automatically block sensitive files (
.env,.key,.pem,.ssh, credentials, passwords, etc.)
Preset Policies
FileSystemPolicy.strict(path): Read-only, sandboxed with default blocked patternsFileSystemPolicy.default(path): Sandboxed with write access and default restrictionsFileSystemPolicy.permissive(path): Larger file size limit (100MB), minimal blocked patterns
Complex Policy Example
import java.time.LocalTime
case class TimeRestrictedPolicy[T <: CallableTool[?]](
allowedStart: LocalTime,
allowedEnd: LocalTime
) extends ToolPolicy[T] {
override def validate(tool: T): Try[Unit] = {
val now = LocalTime.now()
if (now.isAfter(allowedStart) && now.isBefore(allowedEnd)) {
Success(())
} else {
Failure(new SecurityException(
s"Tool can only be used between $allowedStart and $allowedEnd"
))
}
}
}
// Only allow calculator during business hours
val businessHoursPolicy = TimeRestrictedPolicy[Calculator](
allowedStart = LocalTime.of(9, 0),
allowedEnd = LocalTime.of(17, 0)
)
val executor = ToolExecutor.derived[Calculator]
val restricted = RestrictedExecutor(executor, businessHoursPolicy)
Rate Limiting Example
import scala.collection.mutable
import java.time.Instant
case class RateLimitPolicy[T <: CallableTool[?]](
maxCalls: Int,
windowSeconds: Int
) extends ToolPolicy[T] {
private val callTimes = mutable.Queue[Instant]()
override def validate(tool: T): Try[Unit] = synchronized {
val now = Instant.now()
val cutoff = now.minusSeconds(windowSeconds)
// Remove old calls outside the window
while (callTimes.nonEmpty && callTimes.head.isBefore(cutoff)) {
callTimes.dequeue()
}
if (callTimes.size >= maxCalls) {
Failure(new SecurityException(
s"Rate limit exceeded: $maxCalls calls per $windowSeconds seconds"
))
} else {
callTimes.enqueue(now)
Success(())
}
}
}
// Limit to 10 calls per minute
val rateLimitPolicy = RateLimitPolicy[WebSearch](
maxCalls = 10,
windowSeconds = 60
)
Best Practices
- Always use policies for sensitive tools: FileSystem, HTTP, and other tools that access external resources
- Start restrictive: Use
strictordefaultpolicies and relax as needed - Combine policies: Layer multiple policies for defense in depth
- Log policy violations: Track and alert on security violations
- Test policies: Ensure policies work as expected before deployment
Next Steps
- Browse Provided tools
- Learn about the Tool system
- Explore Agents