Grants
NAME
MCP::Client::Policy::Grants - the session grant book a fleet of agents share
SYNOPSIS
use MCP::Client::Policy;
use MCP::Client::Policy::Grants;
# One store for the whole session, seeded from what the last session remembered.
my $grants = MCP::Client::Policy::Grants.new(grants => $session.grants);
my $parent = MCP::Client::Policy.new(:$provider, :&on-ask, grants-store => $grants);
my $child = MCP::Client::Policy.new(:$provider, :&on-ask, grants-store => $grants);
# The human says "always allow" on the parent's question ...
$grants.list.elems; # 1
# ... and the child never asks that question at all.
DESCRIPTION
A session grant ā the rule an always-allow or always-deny answer leaves
behind ā belongs to the human, not to the object that happened to ask the
question. A single agent cannot tell the difference, because there is only one
policy. A fleet can: ten children, each with its own
MCP::Client::Policy, will ask the same
question ten times if each of them remembers the answer privately.
This is the shared answer to that: a small, lock-protected list of rules that any number of policies consult and any of them can add to. Give the same store to every policy in a session and the doctrine becomes one session, one grant set ā the human says "always" once, and every agent alive at that moment, plus every agent started afterwards, is bound by it.
What it does not change
A store is a place to keep grants, not a new kind of permission. Everything about how a grant is used stays where it was, in the policy:
grants are consulted only after the static rules have said
ask, so a grant can never overrule adenyrule, and never overrules the danger floor;among the rules that do get consulted, the same precedence applies ā deny > ask > allow ā so a deny grant beats an allow grant whichever policy made either of them;
a rule is validated before it is kept, by exactly the validator the policy's own rules go through.
Live, not snapshotted
Policies read the store on every decision rather than copying it at construction, so a grant made a millisecond ago through one policy is in force for the next call through another. This is the whole point: a fleet unblocks the moment the human answers, not at the next agent's next start-up.
Thread safety
One lock, held only across the list. Everything that comes out is a deep plain-data copy, so a caller may keep and edit a snapshot ā persist it, render it, diff it ā without the store moving underneath it or the caller's edits reaching back in.
EXAMPLES
Seeding a resumed session, then persisting whatever it learns:
my $grants = MCP::Client::Policy::Grants.new(grants => from-json(slurp 'grants.json'));
my $policy = MCP::Client::Policy.new(
:$provider, :&on-ask,
grants-store => $grants,
on-grant => -> @all { spurt 'grants.json', to-json(@all) },
);
Adding a grant nobody was asked about ā a preset that starts a session already
trusting a directory, say. add takes a list, validates every rule in it, and
appends them in order:
$grants.add([
{ tool => 'fs_write', decision => 'allow', under => '/srv/scratch' },
{ tool => 'fs_edit', decision => 'allow', under => '/srv/scratch' },
]);
A rule that will not validate throws, and nothing is added ā a half-applied list of grants would leave a session trusting something the caller never described:
$grants.add([{ tool => 'fs_write', decision => 'allow', under => '/a/../b' }]);
# X::MCP::Client: invalid under '/a/../b' in a session grant for 'fs_write' ...
say $grants.list.elems; # unchanged
SEE ALSO
MCP::Client::Policy ā the grants-store
option, and what a grant means once it is in the book.