Permissions & Dry-run
Permission flags, destructive markers, and the dry-run contract.
#Permissions and dry-run
A tool that reads your calendar is very different from a tool that empties it. mAIvn makes that difference explicit: every tool declares what kind of work it does, so the agent host can enforce least privilege, warn a user before something irreversible runs, or refuse to register a destructive tool without operator approval. By default maivn-tools assumes a tool is read-only — anything that mutates provider state has to opt in.
This page covers the two mechanisms behind that contract: permission flags (what a tool is allowed to touch) and dry-run (let a tool describe its changes without making them).
The permission types live in the core SDK (maivn), so a custom toolset does not need to install maivn-tools just to declare permissions on its own methods:
from maivn import PermissionFlag, PermissionSet, require_permissions, toolify, toolsetFor callers that depend on the original layout, maivn_tools.core.permissions re-exports PermissionFlag, PermissionSet, and require_permissions. New code should import the permission types and decorators from maivn directly.
#Permission flags
PermissionFlag is a Flag enum: combine values with |.
PermissionSet wraps a flag bitfield and offers ergonomic helpers:
from maivn import PermissionFlag, PermissionSet, require_permissions
write_only = PermissionSet(PermissionFlag.WRITE)
all_perms = PermissionSet.all()
assert PermissionFlag.WRITE in write_only
assert write_only.to_list() == ["write"]
assert PermissionSet.from_names(["read", "write"]).flags == (
PermissionFlag.READ | PermissionFlag.WRITE
)
assert PermissionSet(PermissionFlag.DELETE).is_destructive()Hosts can run their own gate:
require_permissions(
granted=PermissionSet(PermissionFlag.READ),
required=PermissionSet(PermissionFlag.READ | PermissionFlag.WRITE),
)
# Raises PermissionError: Missing required permissions: ['write']#Declaring tool permissions
Generic adapters expose permission attributes for host inspection. For SDK registration, declare custom-tool metadata with @toolify:
@toolify(
permissions=PermissionSet(PermissionFlag.WRITE | PermissionFlag.DELETE),
destructive=True,
)
def archive_message(message_id: str) -> dict:
return provider_api.archive(message_id)Metadata describes intended behavior; it does not enforce provider authorization. An include_tags=["read"] filter also matches tools tagged with both READ and WRITE. Generic query or workflow tools can perform operations beyond their tag. For read-only use, choose explicit methods and restrict provider credentials.
destructive=True is a coarser hint than permissions. It is the right signal for a host UI to warn users; permissions is the right signal for machine-readable gating.
#Dry run
Write-capable tools should accept a dry_run flag. When it is true the tool must:
- Validate the inputs as if the call were real.
- Compute the change set it would apply.
- Return a
DryRunOutcomedescribing the planned change. - Make no provider mutation.
from maivn_tools import DryRunOutcome, DryRunPlan, dry_run_capable
@dry_run_capable
def archive_message(message_id: str, *, dry_run: bool = False) -> DryRunOutcome | dict:
if dry_run:
return DryRunOutcome(
tool="archive_message",
plans=(
DryRunPlan(
operation="archive",
target=f"messages/{message_id}",
before={"folder": "inbox"},
after={"folder": "archive"},
),
),
)
return provider_api.archive(message_id)@dry_run_capable is a marker decorator. It does not change behavior but attaches __maivn_dry_run_capable__ = True to the callable so hosts and tests can discover which tools honor the contract without executing them.
#Recommended defaults
- Declare permissions for each tool; do not rely on a default READ marker to establish safety.
- Write tools opt in to
WRITEonly. - Delete, export, import, and admin tools opt in explicitly to those flags in addition to
destructive=True. - Tools that act on behalf of another user set
IMPERSONATEand document the impersonation contract.