feat: implement --read-only mode with tool filtering

- Adds --read-only CLI flag to restrict OAuth scopes to read-only permissions
- Implements dynamic tool filtering to disable tools requiring write permissions when in read-only mode
- Updates auth/scopes.py to manage read-only scope mappings
- Enhances @require_google_service and handle_http_errors decorators to propagate scope metadata
- Updates documentation in README.md
This commit is contained in:
Dmytro Dziuma
2025-12-24 00:19:28 +00:00
parent a446b72104
commit 0d4394ae27
6 changed files with 113 additions and 25 deletions

View File

@@ -113,6 +113,20 @@ TOOL_SCOPES_MAP = {
"search": CUSTOM_SEARCH_SCOPES,
}
# Tool-to-read-only-scopes mapping
TOOL_READONLY_SCOPES_MAP = {
"gmail": [GMAIL_READONLY_SCOPE],
"drive": [DRIVE_READONLY_SCOPE],
"calendar": [CALENDAR_READONLY_SCOPE],
"docs": [DOCS_READONLY_SCOPE],
"sheets": [SHEETS_READONLY_SCOPE],
"chat": [CHAT_READONLY_SCOPE],
"forms": [FORMS_BODY_READONLY_SCOPE, FORMS_RESPONSES_READONLY_SCOPE],
"slides": [SLIDES_READONLY_SCOPE],
"tasks": [TASKS_READONLY_SCOPE],
"search": CUSTOM_SEARCH_SCOPES,
}
def set_enabled_tools(enabled_tools):
"""
@@ -126,6 +140,35 @@ def set_enabled_tools(enabled_tools):
logger.info(f"Enabled tools set for scope management: {enabled_tools}")
# Global variable to store read-only mode (set by main.py)
_READ_ONLY_MODE = False
def set_read_only(enabled: bool):
"""
Set the global read-only mode.
Args:
enabled: Boolean indicating if read-only mode should be enabled.
"""
global _READ_ONLY_MODE
_READ_ONLY_MODE = enabled
logger.info(f"Read-only mode set to: {enabled}")
def is_read_only_mode() -> bool:
"""Check if read-only mode is enabled."""
return _READ_ONLY_MODE
def get_all_read_only_scopes() -> list[str]:
"""Get all possible read-only scopes across all tools."""
all_scopes = set(BASE_SCOPES)
for scopes in TOOL_READONLY_SCOPES_MAP.values():
all_scopes.update(scopes)
return list(all_scopes)
def get_current_scopes():
"""
Returns scopes for currently enabled tools.
@@ -134,24 +177,7 @@ def get_current_scopes():
Returns:
List of unique scopes for the enabled tools plus base scopes.
"""
enabled_tools = _ENABLED_TOOLS
if enabled_tools is None:
# Default behavior - return all scopes
enabled_tools = TOOL_SCOPES_MAP.keys()
# Start with base scopes (always required)
scopes = BASE_SCOPES.copy()
# Add scopes for each enabled tool
for tool in enabled_tools:
if tool in TOOL_SCOPES_MAP:
scopes.extend(TOOL_SCOPES_MAP[tool])
logger.debug(
f"Generated scopes for tools {list(enabled_tools)}: {len(set(scopes))} unique scopes"
)
# Return unique scopes
return list(set(scopes))
return get_scopes_for_tools(_ENABLED_TOOLS)
def get_scopes_for_tools(enabled_tools=None):
@@ -171,11 +197,18 @@ def get_scopes_for_tools(enabled_tools=None):
# Start with base scopes (always required)
scopes = BASE_SCOPES.copy()
# Determine which map to use based on read-only mode
scope_map = TOOL_READONLY_SCOPES_MAP if _READ_ONLY_MODE else TOOL_SCOPES_MAP
mode_str = "read-only" if _READ_ONLY_MODE else "full"
# Add scopes for each enabled tool
for tool in enabled_tools:
if tool in TOOL_SCOPES_MAP:
scopes.extend(TOOL_SCOPES_MAP[tool])
if tool in scope_map:
scopes.extend(scope_map[tool])
logger.debug(
f"Generated {mode_str} scopes for tools {list(enabled_tools)}: {len(set(scopes))} unique scopes"
)
# Return unique scopes
return list(set(scopes))

View File

@@ -636,6 +636,9 @@ def require_google_service(
if func.__doc__:
wrapper.__doc__ = _remove_user_email_arg_from_docstring(func.__doc__)
# Attach required scopes to the wrapper for tool filtering
wrapper._required_google_scopes = _resolve_scopes(scopes)
return wrapper
return decorator
@@ -774,6 +777,12 @@ def require_multiple_services(service_configs: List[Dict[str, Any]]):
if func.__doc__:
wrapper.__doc__ = _remove_user_email_arg_from_docstring(func.__doc__)
# Attach all required scopes to the wrapper for tool filtering
all_scopes = []
for config in service_configs:
all_scopes.extend(_resolve_scopes(config["scopes"]))
wrapper._required_google_scopes = all_scopes
return wrapper
return decorator