feat: add tasks:manage permission level to deny delete without blocking other writes
The consolidated manage_task tool bundles create/update/delete/move into a single tool, making it impossible to deny just the delete action via tool tiers or scope-based filtering. This adds: - A `manage` permission level for tasks (between readonly and full) - A SERVICE_DENIED_ACTIONS registry mapping (service, level) to denied actions - An is_action_denied() helper that tools call before executing actions - Guards in manage_task and manage_task_list that reject denied actions Usage: --permissions tasks:manage Allows create, update, move. Denies delete. tasks:full remains unchanged (all actions allowed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,12 @@ Usage:
|
||||
--permissions gmail:organize drive:readonly
|
||||
|
||||
Gmail levels: readonly, organize, drafts, send, full
|
||||
Tasks levels: readonly, manage, full
|
||||
Other services: readonly, full (extensible by adding entries to SERVICE_PERMISSION_LEVELS)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, FrozenSet, List, Optional, Set, Tuple
|
||||
|
||||
from auth.scopes import (
|
||||
GMAIL_READONLY_SCOPE,
|
||||
@@ -97,7 +98,8 @@ SERVICE_PERMISSION_LEVELS: Dict[str, List[Tuple[str, List[str]]]] = {
|
||||
],
|
||||
"tasks": [
|
||||
("readonly", [TASKS_READONLY_SCOPE]),
|
||||
("full", [TASKS_SCOPE]),
|
||||
("manage", [TASKS_SCOPE]),
|
||||
("full", []),
|
||||
],
|
||||
"contacts": [
|
||||
("readonly", [CONTACTS_READONLY_SCOPE]),
|
||||
@@ -131,16 +133,43 @@ SERVICE_PERMISSION_LEVELS: Dict[str, List[Tuple[str, List[str]]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
# Actions denied at specific permission levels.
|
||||
# Maps service -> level -> frozenset of denied action names.
|
||||
# Levels not listed here (or services without entries) deny nothing.
|
||||
SERVICE_DENIED_ACTIONS: Dict[str, Dict[str, FrozenSet[str]]] = {
|
||||
"tasks": {
|
||||
"manage": frozenset({"delete"}),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def is_action_denied(service: str, action: str) -> bool:
|
||||
"""Check whether *action* is denied for *service* under current permissions.
|
||||
|
||||
Returns ``False`` when granular permissions mode is not active, when the
|
||||
service has no permission entry, or when the configured level does not
|
||||
deny the action.
|
||||
"""
|
||||
if _PERMISSIONS is None:
|
||||
return False
|
||||
level = _PERMISSIONS.get(service)
|
||||
if level is None:
|
||||
return False
|
||||
denied = SERVICE_DENIED_ACTIONS.get(service, {}).get(level, frozenset())
|
||||
return action in denied
|
||||
|
||||
|
||||
# Module-level state: parsed --permissions config
|
||||
# Dict mapping service_name -> level_name, e.g. {"gmail": "organize"}
|
||||
_PERMISSIONS: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
def set_permissions(permissions: Dict[str, str]) -> None:
|
||||
def set_permissions(permissions: Optional[Dict[str, str]]) -> None:
|
||||
"""Set granular permissions from parsed --permissions argument."""
|
||||
global _PERMISSIONS
|
||||
_PERMISSIONS = permissions
|
||||
logger.info("Granular permissions set: %s", permissions)
|
||||
if permissions is not None:
|
||||
logger.info("Granular permissions set: %s", permissions)
|
||||
|
||||
|
||||
def get_permissions() -> Optional[Dict[str, str]]:
|
||||
|
||||
Reference in New Issue
Block a user