Skip to content

Commit d910705

Browse files
Milofaxclaude
andcommitted
fix(auth): add cross-service Drive scopes for docs and sheets tools
Several docs tools (search_docs, get_doc_content, list_docs_in_folder, export_doc_to_pdf) and sheets tools (list_spreadsheets) internally use the Google Drive API but only receive docs/sheets-specific OAuth scopes when configured with `--tools docs sheets` (without `drive`). This adds the minimal required Drive scopes as cross-service dependencies: - docs: drive.readonly (metadata queries) + drive.file (PDF export) - sheets: drive.readonly (spreadsheet listing) This follows the existing pattern where appscript already includes DRIVE_FILE_SCOPE for its Drive API dependency. The alternative workaround of adding `--tools drive` exposes 14 full-access Drive tools which is undesirable from a security perspective. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a0a0a96 commit d910705

2 files changed

Lines changed: 99 additions & 4 deletions

File tree

auth/scopes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
BASE_SCOPES = [USERINFO_EMAIL_SCOPE, USERINFO_PROFILE_SCOPE, OPENID_SCOPE]
8585

8686
# Service-specific scope groups
87-
DOCS_SCOPES = [DOCS_READONLY_SCOPE, DOCS_WRITE_SCOPE]
87+
DOCS_SCOPES = [DOCS_READONLY_SCOPE, DOCS_WRITE_SCOPE, DRIVE_READONLY_SCOPE, DRIVE_FILE_SCOPE]
8888

8989
CALENDAR_SCOPES = [CALENDAR_SCOPE, CALENDAR_READONLY_SCOPE, CALENDAR_EVENTS_SCOPE]
9090

@@ -101,7 +101,7 @@
101101

102102
CHAT_SCOPES = [CHAT_READONLY_SCOPE, CHAT_WRITE_SCOPE, CHAT_SPACES_SCOPE]
103103

104-
SHEETS_SCOPES = [SHEETS_READONLY_SCOPE, SHEETS_WRITE_SCOPE]
104+
SHEETS_SCOPES = [SHEETS_READONLY_SCOPE, SHEETS_WRITE_SCOPE, DRIVE_READONLY_SCOPE]
105105

106106
FORMS_SCOPES = [
107107
FORMS_BODY_SCOPE,
@@ -148,8 +148,8 @@
148148
"gmail": [GMAIL_READONLY_SCOPE],
149149
"drive": [DRIVE_READONLY_SCOPE],
150150
"calendar": [CALENDAR_READONLY_SCOPE],
151-
"docs": [DOCS_READONLY_SCOPE],
152-
"sheets": [SHEETS_READONLY_SCOPE],
151+
"docs": [DOCS_READONLY_SCOPE, DRIVE_READONLY_SCOPE],
152+
"sheets": [SHEETS_READONLY_SCOPE, DRIVE_READONLY_SCOPE],
153153
"chat": [CHAT_READONLY_SCOPE],
154154
"forms": [FORMS_BODY_READONLY_SCOPE, FORMS_RESPONSES_READONLY_SCOPE],
155155
"slides": [SLIDES_READONLY_SCOPE],

tests/test_scopes.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Unit tests for cross-service scope generation.
3+
4+
Verifies that docs and sheets tools automatically include the Drive scopes
5+
they need for operations like search_docs, list_docs_in_folder,
6+
export_doc_to_pdf, and list_spreadsheets — without requiring --tools drive.
7+
"""
8+
9+
import sys
10+
import os
11+
12+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
13+
14+
from auth.scopes import (
15+
DRIVE_FILE_SCOPE,
16+
DRIVE_READONLY_SCOPE,
17+
DRIVE_SCOPE,
18+
get_scopes_for_tools,
19+
set_read_only,
20+
)
21+
22+
23+
class TestDocsScopes:
24+
"""Tests for docs tool scope generation."""
25+
26+
def test_docs_includes_drive_readonly(self):
27+
"""search_docs, get_doc_content, list_docs_in_folder need drive.readonly."""
28+
scopes = get_scopes_for_tools(["docs"])
29+
assert DRIVE_READONLY_SCOPE in scopes
30+
31+
def test_docs_includes_drive_file(self):
32+
"""export_doc_to_pdf needs drive.file to create the PDF."""
33+
scopes = get_scopes_for_tools(["docs"])
34+
assert DRIVE_FILE_SCOPE in scopes
35+
36+
def test_docs_does_not_include_full_drive(self):
37+
"""docs should NOT request full drive access."""
38+
scopes = get_scopes_for_tools(["docs"])
39+
assert DRIVE_SCOPE not in scopes
40+
41+
42+
class TestSheetsScopes:
43+
"""Tests for sheets tool scope generation."""
44+
45+
def test_sheets_includes_drive_readonly(self):
46+
"""list_spreadsheets needs drive.readonly."""
47+
scopes = get_scopes_for_tools(["sheets"])
48+
assert DRIVE_READONLY_SCOPE in scopes
49+
50+
def test_sheets_does_not_include_full_drive(self):
51+
"""sheets should NOT request full drive access."""
52+
scopes = get_scopes_for_tools(["sheets"])
53+
assert DRIVE_SCOPE not in scopes
54+
55+
56+
class TestCombinedScopes:
57+
"""Tests for combined tool scope generation."""
58+
59+
def test_docs_sheets_no_duplicate_drive_readonly(self):
60+
"""Combined docs+sheets should deduplicate drive.readonly."""
61+
scopes = get_scopes_for_tools(["docs", "sheets"])
62+
assert scopes.count(DRIVE_READONLY_SCOPE) <= 1
63+
64+
def test_docs_sheets_returns_unique_scopes(self):
65+
"""All returned scopes should be unique."""
66+
scopes = get_scopes_for_tools(["docs", "sheets"])
67+
assert len(scopes) == len(set(scopes))
68+
69+
70+
class TestReadOnlyScopes:
71+
"""Tests for read-only mode scope generation."""
72+
73+
def setup_method(self):
74+
set_read_only(False)
75+
76+
def teardown_method(self):
77+
set_read_only(False)
78+
79+
def test_docs_readonly_includes_drive_readonly(self):
80+
"""Even in read-only mode, docs needs drive.readonly for search/list."""
81+
set_read_only(True)
82+
scopes = get_scopes_for_tools(["docs"])
83+
assert DRIVE_READONLY_SCOPE in scopes
84+
85+
def test_docs_readonly_excludes_drive_file(self):
86+
"""In read-only mode, docs should NOT request drive.file."""
87+
set_read_only(True)
88+
scopes = get_scopes_for_tools(["docs"])
89+
assert DRIVE_FILE_SCOPE not in scopes
90+
91+
def test_sheets_readonly_includes_drive_readonly(self):
92+
"""Even in read-only mode, sheets needs drive.readonly for list."""
93+
set_read_only(True)
94+
scopes = get_scopes_for_tools(["sheets"])
95+
assert DRIVE_READONLY_SCOPE in scopes

0 commit comments

Comments
 (0)