PermissionMiddleware

org.llm4s.agent.graph.middleware.PermissionMiddleware
See thePermissionMiddleware companion object
@Experimental
final class PermissionMiddleware(rules: Seq[PermissionRule], val id: MiddlewareId) extends AgentMiddleware

Ordered permission rules over each tool call's resources (design ยง5.6), checked after the call's arguments were validated and decoded, on ToolCallRequest.resources.

For each resource the first rule that matches it decides, and a resource no rule matches is denied. A call runs only if every resource is allowed: a denied one gives ToolOutcome.Error("Denied: <operation> <target>") (the first, in resource order); otherwise one to ask about gives NeedsApproval, unless the call is already approved. Approval is one flag per call: once a reviewer approves it - whichever middleware or tool asked - every Ask passes. An edited call is checked again on its new arguments, so a deny rule still refuses it.

Register it first, so it wraps every other middleware and a denied call is refused before anything else asks for approval of it. An Execute rule limits which commands the agent may ask to run, not what a command does: a sandbox constrains that itself.

Attributes

Companion
object
Graph
Supertypes
class Object
trait Matchable
class Any

Members list

Value members

Concrete methods

The decision for resource: for a concrete target, the first matching rule's, or Deny when none matches. A target is read as a Glob, so one written as a pattern - an envelope such as /out/&#42;&#42;, declared by a tool whose answer picks the path - is decided for every target in it: the rules are read in order, and the envelope is denied as soon as a Deny rule reaches any of its targets, asked about once an Ask rule reaches some, and allowed only when an Allow rule covers all that remain; if no rule covers the rest, some target is unmatched, so it is denied. An Allow that covers only part does not settle it, so a later Deny reaching the rest still denies (conservatively, even when the earlier allow already decided the targets they share). For a concrete target, reaching and covering are both matching, so this is first match wins.

The decision for resource: for a concrete target, the first matching rule's, or Deny when none matches. A target is read as a Glob, so one written as a pattern - an envelope such as /out/&#42;&#42;, declared by a tool whose answer picks the path - is decided for every target in it: the rules are read in order, and the envelope is denied as soon as a Deny rule reaches any of its targets, asked about once an Ask rule reaches some, and allowed only when an Allow rule covers all that remain; if no rule covers the rest, some target is unmatched, so it is denied. An Allow that covers only part does not settle it, so a later Deny reaching the rest still denies (conservatively, even when the earlier allow already decided the targets they share). For a concrete target, reaching and covering are both matching, so this is first match wins.

Attributes

override def wrapToolCall(request: ToolCallRequest, context: ToolContext)(next: () => ToolOutcome): ToolOutcome

Wraps one tool call, after its arguments were validated. A wrapper denies with ToolOutcome.Error("Denied: ..."), asks for approval with NeedsApproval(reason) (unless context.approved), fails the run with Fatal, and short-circuits by not calling next. It may call next more than once (retry) and transform the outcome next returns.

Wraps one tool call, after its arguments were validated. A wrapper denies with ToolOutcome.Error("Denied: ..."), asks for approval with NeedsApproval(reason) (unless context.approved), fails the run with Fatal, and short-circuits by not calling next. It may call next more than once (retry) and transform the outcome next returns.

Attributes

Definition Classes

Inherited methods

def afterAgent(answer: String, context: ModelContext): Result[BoundaryResult]

Sees the run's final answer: returns it, possibly changed, with an update to this middleware's writes, or Left to block the run.

Sees the run's final answer: returns it, possibly changed, with an update to this middleware's writes, or Left to block the run.

Attributes

Inherited from:
AgentMiddleware
def beforeAgent(input: String, context: ModelContext): Result[BoundaryResult]

Sees the run's input before anything else runs: returns it, possibly changed, with an update to this middleware's writes that the turn's first model call sees, or Left to block the run.

Sees the run's input before anything else runs: returns it, possibly changed, with an update to this middleware's writes that the turn's first model call sees, or Left to block the run.

Attributes

Inherited from:
AgentMiddleware

Content for the system prompt of each model request: collected before every model step from the committed state, in stack order, and never stored. Each middleware bounds its own; the agent bounds them all together (maxPromptSectionTokens). Left fails the run.

Content for the system prompt of each model request: collected before every model step from the committed state, in stack order, and never stored. Each middleware bounds its own; the agent bounds them all together (maxPromptSectionTokens). Left fails the run.

Attributes

Inherited from:
AgentMiddleware

Middleware this one must run inside of (be wrapped by); each must be registered in the same stack.

Middleware this one must run inside of (be wrapped by); each must be registered in the same stack.

Attributes

Inherited from:
AgentMiddleware

Middleware this one must run outside of (wrap); each must be registered in the same stack.

Middleware this one must run outside of (wrap); each must be registered in the same stack.

Attributes

Inherited from:
AgentMiddleware
def tools: Vector[AgentTool[_]]

Tools this middleware contributes; they join the loop's tool set and are validated like any other.

Tools this middleware contributes; they join the loop's tool set and are validated like any other.

Attributes

Inherited from:
AgentMiddleware

Wraps one model call. A wrapper may rewrite the request (inject a note, filter tools), call next more than once (retry, fallback), transform its response, add an update to its writes (ModelResponse.addUpdate), or return Left, which fails the run. Only an AgentMiddleware.Asking wrapper suspends, by asking its question. Every call next makes is counted in the thread's usage, whichever response the wrapper returns.

Wraps one model call. A wrapper may rewrite the request (inject a note, filter tools), call next more than once (retry, fallback), transform its response, add an update to its writes (ModelResponse.addUpdate), or return Left, which fails the run. Only an AgentMiddleware.Asking wrapper suspends, by asking its question. Every call next makes is counted in the thread's usage, whichever response the wrapper returns.

Filtering ModelRequest.tools shapes what the model is offered and is not a permission control: a tool the model calls anyway still runs through wrapToolCall, where denial belongs.

Attributes

Inherited from:
AgentMiddleware
def writes: Set[StateKey[_, _]]

The state keys this middleware's hooks may write: an update from beforeAgent, wrapModelCall or afterAgent, or one wrapToolCall adds to a Success. A hook's write to any other key fails the run.

The state keys this middleware's hooks may write: an update from beforeAgent, wrapModelCall or afterAgent, or one wrapToolCall adds to a Success. A hook's write to any other key fails the run.

Attributes

Inherited from:
AgentMiddleware

Concrete fields

This middleware's identifier, unique in its stack; must match [a-zA-Z0-9_-]{1,64}.

This middleware's identifier, unique in its stack; must match [a-zA-Z0-9_-]{1,64}.

Attributes