NAME

AnyEvent::MCP::Server::Permissions - action authorization and sandbox grants

SYNOPSIS

my $permissions = AnyEvent::MCP::Server::Permissions->new (
   name           => "example",
   actions        => { read => "Read file contents", write => "Modify files" },
   sandbox_grants => {
      "workdir-read"  => [],
      "workdir-write" => ["workdir-read"],
   },
   sandbox_grant_descriptions => {
      "workdir-read" => "Read the configured workdir",
   },
   sandbox_implementation => "bubblewrap",
);

my $grant = $req->require_permission ({
   tool           => $req->{tool},
   action         => "read",
   subject        => $path,
   subjects       => [{ label => "this file", subject => $path }],
   sandbox_grants => ["workdir-read"],
   reason         => "read $path",
});

DESCRIPTION

This module lets an MCP server check whether a tool operation is allowed before doing the work. It applies ordered permission rules and asks the user when a decision is needed. Users can allow or deny one operation, or save a rule for future requests.

Attach a Permissions object to your server, then have each tool call $req->require_permission before starting its operation. The tool describes what it wants to do and which worker privileges it needs. An allowed request receives the granted sandbox privileges; a denied request must not proceed.

By default, questions go to a local graphical or terminal helper. You can instead configure the server to ask through an MCP client that supports form elicitation. Saved rules can answer later requests without another prompt; see "RULE FILES".

For HTTP, the transport must authenticate callers before dispatching MCP requests. This module then authorizes operations for the accepted caller. The tool must enforce the returned grants, for example by constructing a restricted sandbox. The permission engine does not construct that sandbox or enforce worker isolation itself.

The three separate questions

Authentication establishes who may call the MCP endpoint. The permission engine answers whether a requested action on a subject may proceed and which sandbox grants the user approved. The application's sandbox or other enforcement then determines what the operation can actually do. None of the three substitutes for the others.

For example, a user may approve write on a project but only with workdir-write authority. This does not authorize an arbitrary command with network access, and an approval to write a buffer does not imply approval to save it. Ask before side effects: permission prompts can restart the tool callback. See "Permission checks and retries" in AnyEvent::MCP::Server::Toolset and AnyEvent::MCP::Intro for examples from actual servers.

PERMISSION REQUESTS

A request names an action (a server-defined class of authority such as inspect, read, write, manage or exec), an optional subject, and the sandbox_grants required by its worker. These are independent: authorization to write does not imply authorization to read, although sandbox-level workdir-write may imply workdir-read. tool is optional human-readable context identifying the actual MCP tool; rules do not match it.

An action may have a short human-readable description declared in actions. sandbox_grant_descriptions similarly describes worker privileges, and sandbox_implementation identifies the enforcement environment (for example, bubblewrap or NONE - UNSANDBOXED). These labels are explanatory, not authorization identifiers, and the trusted server supplies them.

details optionally maps plain display labels to Unicode text, for example { "View patch" => $diff }. These details are display-only and never participate in rule matching or persistence. Supply complete text; the UI chooses presentation, escaping and preview limits. Labels carry no UI punctuation such as an ellipsis. The local YAD helper opens a text viewer; terminal and native MCP forms include bounded previews in the prompt.

RULE FILES

Rules are evaluated in order, with the first match deciding. decision is allow, deny or ask; omitted decisions are invalid. action is the server-defined action class, not the decision. A matcher beginning with ^ is interpreted as a Perl regular expression; otherwise it matches exactly. An omitted subject matches any subject. For example:

[rule]
decision=allow
action=read
subject=^/home/example/documents/
sandbox-grants=workdir-read

[rule]
decision=deny
action=^

An allow rule requires sandbox-grants, which may be empty for actions that need no worker grants. It matches only requests whose required grants (including implied grants) are contained in the allowed closure. Omitted sandbox-grants on deny and ask rules matches any requested grants. An explicit ask rule takes precedence over any later allow rules. Deny rules may also specify an exact path matched against any entry in the request's paths array and a human-readable message included in subsequent denial errors. Neither field is permitted on allow rules. Exact-path denials apply even when an operation requires additional sandbox grants for other files. File paths are lexical strings, not a sandbox or inode identity. With no matching rule the user is asked.

An optional expires field is an unsigned integer Unix timestamp. Rules with expires at or before the current time are removed from the returned list on every read, before matching. Omit it for indefinite rules. Reads do not rewrite the file; the next ordinary save removes expired entries from disk. Permission prompts offer indefinite (the default), 900 seconds or 16 hours. Expiry does not interrupt operations already authorized.

Decisions received after asynchronous prompting are rechecked against the current policy. Persistent answers are appended. Rule files are private, updates use locking and atomic rename, and failed persistence is reported as a failed grant rather than a successful permanent authorization.

METHODS

$permissions = AnyEvent::MCP::Server::Permissions->new (%config)

Creates a permission engine. It does not enforce worker isolation. Options are:

actions => { $action => $description, ... }

Maps action names to descriptions, defaulting to an empty hash. When nonempty, unknown actions are rejected.

sandbox_grants => { $grant => [$implied_grant...], ... }

Defines grant names and their implications, defaulting to an empty hash. Every requested or implied grant must be declared.

sandbox_grant_descriptions => { $grant => $description, ... }
sandbox_grant_descriptions => $callback->($request)

Optional display descriptions. The callback returns a description hash reference. These labels come from the server; request-supplied overrides are ignored.

sandbox_implementation => $description

Optional display label, such as bubblewrap. It does not select or construct an enforcement mechanism.

rules => [$rule...]

Ordered in-memory rule hashes, defaulting to an empty array. See "RULE FILES" for their fields.

file => $file

UTF-8 rule file. When supplied, rules are read from this file on permission checks. A missing file supplies an empty rule list.

name => $name

Optional name used for display and, when file is omitted, the default filename $XDG_CONFIG_HOME/anyevent-mcp/rules/NAME.ini, or $HOME/.config/anyevent-mcp/rules/NAME.ini. Without a name or file, policy stays in memory. Applications can supply a different file; mcp-fs uses anyevent-mcp/rules/fs-NAME.ini.

ask => $method

helper, tty, yad, auto or mcp. When omitted, uses PERL_ANYEVENT_MCP_PERMISSION_ASK, defaulting to helper. helper prompts locally using the configured helper. tty and yad run the built-in permission helper with only that interface, ignoring custom helper settings and without interface fallback. auto uses modern MCP form elicitation when advertised for the request, otherwise the helper. mcp requires modern form elicitation and fails when unavailable. A declined prompt never falls back to a different approval mechanism.

ask => $callback->($prepared, $done->($success, $decision))

Local asynchronous asking callback. $prepared is the prepared request; $done receives a decision hash on success or an error on failure. Callback success means a decision was obtained, not that access was granted.

helper => $executable
helper => [$executable, $arg...]

External helper, not a shell command. Defaults to PERL_ANYEVENT_MCP_PERMISSION_HELPER or anyevent-mcp-askpermission.

timeout => $seconds

External helper timeout, defaulting to 300 seconds. Also determines the lifetime of each modern permission continuation. Use a positive value; zero makes continuations expire immediately, while the helper treats zero as its default timeout.

auto_rules => $rules

Ordered process-local permission rules, each [DECISION, SPEC] where DECISION is allow or deny and SPEC is TOOL[,ACTION[,CAPABILITY]]. Each field is an exact name or * (any); omitted trailing fields mean *. Tool names treat underscores and hyphens alike. A named capability must be known to this server. Allow matches when all requested grants are implied by the specified capability; deny matches when the requested grants imply that capability. * matches any capability set in either direction. First match wins, before saved rules and prompting. Only tools which call the permission engine are affected. Automatic allowance grants exactly the requested sandbox grants; tool validation and sandbox enforcement still apply.

$grant = $permissions->check ($request, $tool)

$request contains action, reason, and sandbox_grants (an array reference; it may be empty). tool in the request is optional display context. The separate optional $tool argument is the trusted dispatched MCP tool name for automatic overrides; Server supplies it from the call. subject is optional; when present, subjects offers persistent scope choices as { label => $label, subject => $matcher } hashes, each matching the requested subject. workdir is an optional exact-match identity separate from subject. Rules with workdir match only that configured workdir spelling, and rules without it may match any workdir when the sandbox grants also match. The application decides when a selected workdir matters to the requested sandbox grants. The persistent-rule dialog defaults to the selected workdir but can explicitly offer any current or future workdir by omitting the matcher; this does not change the sandbox grants available to a single operation. The multibroker alias, when supplied by the child's environment, is display-only and does not change rule matching or policy file selection. paths may contain a list of affected filenames for display in permission prompts and for optional deny-only exact-file rules. Such matching does not restrict sandbox authority and is not a substitute for sandboxing. Absent a path matcher, the permission rule is not limited by paths.

Returns {decision => "allow", sandbox_grants => [...]}, {decision => "deny", sandbox_grants => []}, or {decision => "ask", request => $prepared}. Denials can also include a saved message. check does not prompt or execute the operation.

$grant = $permissions->resolve ($request, $approval)

$request is the original permission request. $approval is { request => $prepared, decision => $decision }, where $prepared is the request returned by check and $decision is the helper's decision hash. Resolves the answer to that prepared request. Approval decisions are allow-once, allow-always, deny-once, and deny-always. Allow-once uses exactly the requested sandbox grants. Allow-always may save a broader or narrower set of grants for future invocations; the present invocation still receives no more than requested. Permanent decisions contain a validated rule restricted to the offered action and subject scope. Denial decisions may include an optional human message; allowed tool results are never modified by permission prompting.

This is a trusted application interface. Do not pass unauthenticated client metadata directly to it.

$grant = $permissions->require_permission ($req, { %request })

Synchronous interface used by $req->require_permission. Returns the granted sandbox-grant array, dies on denial, or throws a permission-needed exception for the server to handle. The worker must enforce the returned grants; never treat merely requested grants as sufficient.

$permissions->require ($req, $request, $done->($success, $grant))

Asynchronous interface. $grant is the allowed sandbox-grant array or an error. Check before starting side effects.

With a local prompt, $done runs when permission is resolved. Modern MCP prompting instead completes $req with input_required without calling $done; the client repeats the call with input. Original callbacks are not retained. Continuations are short-lived and bound to the method, parameters, and request, and are consumed once. Approval retries retain earlier one-time grants within the same call. Always decisions and Back use additional protected input_required rounds, capped at 32, without executing the original tool. Messages in persistent deny rules are returned to subsequent callers, including later sessions. Client-side elicitation is only appropriate when the connected client is the trusted approval authority.

$closure = $permissions->closure ([$grant...])

Returns a hash reference whose keys contain the supplied sandbox grants and all grants they imply. Unknown grants or malformed implication lists die. This expands authority names; it does not grant permission.

$permissions->auto_rules ($rules)

Replace the ordered process-local rule chain. Parsing and validation occur once, at installation. An empty list disables automatic rules.

$rules = $permissions->rules

Returns the ordered, uncompiled in-memory or file-backed rule array reference. File-backed rules are reread, so changes apply to subsequent checks.

$permissions->save ($rules)

Validates and replaces the ordered rules, using atomic file replacement if backed by a file.