Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 17 additions & 28 deletions anton/utils/datasources.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,18 @@ def build_datasource_context(vault: DataVault, active_only: str | None = None) -
)
# Google Drive's drive.file OAuth scope only covers files the app created
# itself, plus files explicitly granted via the Google Picker (persisted
# as a `_picked_files` vault field) — these must be named explicitly here
# since a plain files.list()/files.search() call won't return them.
# as a `_picked_files` vault field).
#
# Those granted files are deliberately NOT listed here (ENG-2071). Listing
# them needs the *current project* to scope by, and `projects` is a cowork
# concept this function has no access to — so the list rendered here was
# unscoped, and named files from other projects. cowork-server renders the
# scoped list itself, via ConnectionsService.picked_files_by_project() ->
# the harness prompt suffix, which is the single renderer. All we track
# here is *whether* any exist, so the availability paragraph below still
# fires for a Picker-only connection exactly as it always has.
google_drive_oauth_connected = False
google_drive_picked_files: dict[str, list[dict]] = {}
google_drive_has_picked_files = False
# Hoisted out of the loop below: this function is rebuilt on every chat
# turn, and DatasourceRegistry() parses the full built-in + user
# datasources.md on every construction (no caching) — with N
Expand Down Expand Up @@ -323,36 +331,17 @@ def build_datasource_context(vault: DataVault, active_only: str | None = None) -
if c["engine"] == "google_drive":
if fields.get("auth_type") == "oauth":
google_drive_oauth_connected = True
picked = _parse_picked_files(fields.get("_picked_files"))
if picked:
google_drive_picked_files[c["name"]] = picked
if google_drive_oauth_connected or google_drive_picked_files:
# Parsed, not merely truthy-checked: a `_picked_files` holding only
# malformed entries must read as "none", the same way it did when
# this drove the (now removed) listing.
if _parse_picked_files(fields.get("_picked_files")):
google_drive_has_picked_files = True
if google_drive_oauth_connected or google_drive_has_picked_files:
lines.append(
"\nConnected Google Drive accounts are available through Google OAuth credentials "
"in the injected `DS_GOOGLE_DRIVE_<CONNECTION>__...` environment variables. "
"Only claim Google Drive access if you can actually use those credentials successfully."
)
if google_drive_picked_files:
picked_lines = [
f"- {f.get('name', 'untitled')} (id: {f.get('id')}, connection: {conn_name})"
for conn_name, files in google_drive_picked_files.items()
for f in files
]
# Imperative and structurally separate from the paragraph above — a
# softer prose mention got silently dropped by the agent when
# reporting files.list() results verbatim.
lines.append(
"\nIMPORTANT — additional Drive files the user has explicitly granted access to "
"via the Google Picker, which a plain files.list() or files.search() call will NOT "
"return (the google_drive scope only covers files this app created itself, plus "
"these specifically granted ones):\n"
+ "\n".join(picked_lines)
+ "\nWhenever you list, search, or enumerate Drive files for the user, you MUST "
"include every file above IN ADDITION to whatever files.list()/files.search() "
"returns — do not report only the API call's results. To read one of these files' "
"content, call files.get(fileId=...) directly with its id above; do not expect it "
"to appear in a files.list() response first."
)
return "\n".join(lines)


Expand Down
14 changes: 9 additions & 5 deletions tests/test_build_chat_session_google_drive.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""End-to-end: build_chat_session() -> ChatSession._build_system_prompt() actually
surfaces Google Drive Picker guidance in the real assembled system prompt.
"""End-to-end: build_chat_session() -> ChatSession._build_system_prompt() and what
Google Drive guidance really reaches the assembled system prompt.

ENG-687 review (PR #241): the picked-files/OAuth guidance moved out of
build_chat_session (anton/core/runtime.py) into build_datasource_context
Expand Down Expand Up @@ -38,7 +38,7 @@ def workspace_path(tmp_path):
return p


async def test_picked_files_reach_the_real_system_prompt(workspace_path):
async def test_drive_availability_reaches_prompt_without_naming_picked_files(workspace_path):
from anton.core.datasources.data_vault import LocalDataVault
from anton.core.runtime import build_chat_session

Expand All @@ -53,8 +53,12 @@ async def test_picked_files_reach_the_real_system_prompt(workspace_path):
prompt = await session._build_system_prompt()

assert "Connected Google Drive accounts are available" in prompt
assert "IMPORTANT — additional Drive files" in prompt
assert "Roadmap.gdoc" in prompt
# ENG-2071: the availability paragraph reaches the real prompt, but the
# Picker-granted files themselves must not — this function has no project
# to scope them by, so it named files from every project. cowork-server
# renders the scoped list instead (ConnectionsService.picked_files_by_project).
assert "IMPORTANT — additional Drive files" not in prompt
assert "Roadmap.gdoc" not in prompt


async def test_no_connections_and_no_suffix_does_not_crash(workspace_path):
Expand Down
63 changes: 44 additions & 19 deletions tests/test_datasource_context_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,11 @@ def test_empty_or_none_returns_empty(self):


class TestGoogleDrivePickerContext:
"""ENG-687: google_drive's drive.file OAuth scope only covers files the
app created itself, plus files explicitly granted via the Google
Picker — the agent needs those named by id or a plain files.list()/
files.search() call won't surface them at all."""
"""ENG-687 gave google_drive's Picker-granted files their own prompt block
here. ENG-2071 removed the *listing* again: it had no project to scope by,
so it named files the user granted in other projects, re-opening the leak
ConnectionsService.picked_files_by_project() closes on the cowork-server
side. What stays here is the availability paragraph — no file ids."""

def test_oauth_connection_without_picked_files_shows_availability_only(self, tmp_path):
v = LocalDataVault(tmp_path)
Expand All @@ -179,34 +180,56 @@ def test_oauth_connection_without_picked_files_shows_availability_only(self, tmp
assert "Connected Google Drive accounts are available" in ctx
assert "IMPORTANT" not in ctx

def test_picked_files_surfaced_with_id_and_connection(self, tmp_path):
def test_picked_files_are_never_named_here(self, tmp_path):
"""ENG-2071: the availability paragraph still fires, but no file
identity reaches the prompt from this function."""
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {
"auth_type": "oauth",
"_picked_files": json.dumps([{"id": "f1", "name": "Roadmap.gdoc"}]),
})
ctx = build_datasource_context(v)
assert "IMPORTANT — additional Drive files" in ctx
assert "Roadmap.gdoc" in ctx
assert "id: f1" in ctx
assert "connection: work" in ctx
assert "Connected Google Drive accounts are available" in ctx
assert "Roadmap.gdoc" not in ctx
assert "f1" not in ctx
assert "IMPORTANT — additional Drive files" not in ctx

def test_file_granted_in_another_project_is_not_named(self, tmp_path):
"""The leak itself. A file tagged to Project A must not surface at all
from here — this function cannot tell which project is active, which is
exactly why it no longer lists any of them."""
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {
"auth_type": "oauth",
"_picked_files": json.dumps([
{"id": "secret-a", "name": "ProjectA-Salaries.gsheet", "projects": ["Project A"]},
]),
})
ctx = build_datasource_context(v)
assert "ProjectA-Salaries.gsheet" not in ctx
assert "secret-a" not in ctx

def test_resource_key_not_required_but_included_when_present(self, tmp_path):
def test_picker_only_connection_still_shows_availability(self, tmp_path):
"""A Picker-granted connection with no `auth_type` still triggers the
paragraph — unchanged from before ENG-2071, which is why presence is
still tracked even though the list is gone."""
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {
"_picked_files": json.dumps([{"id": "f1", "name": "Shared.gdoc", "resourceKey": "rk123"}]),
"_picked_files": json.dumps([{"id": "f1", "name": "Roadmap.gdoc"}]),
})
ctx = build_datasource_context(v)
assert "Roadmap.gdoc" not in ctx # sanity: not leaking the other test's fixture
assert "Shared.gdoc" in ctx
assert "Connected Google Drive accounts are available" in ctx
assert "Roadmap.gdoc" not in ctx

def test_malformed_picked_file_entries_are_dropped_not_crashed(self, tmp_path):
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {
"_picked_files": json.dumps(["not-a-dict", {"name": "missing-id"}]),
})
ctx = build_datasource_context(v) # must not raise
assert "IMPORTANT" not in ctx # nothing well-formed survived, so no block at all
# Nothing well-formed survived and there is no oauth marker, so the
# connection reads as "no Picker grant" and no paragraph appears.
assert "Connected Google Drive accounts are available" not in ctx

def test_no_google_drive_connection_no_guidance(self, tmp_path):
v = LocalDataVault(tmp_path)
Expand All @@ -215,13 +238,14 @@ def test_no_google_drive_connection_no_guidance(self, tmp_path):
assert "Google Drive" not in ctx
assert "IMPORTANT" not in ctx

def test_multiple_google_drive_connections_each_listed_separately(self, tmp_path):
def test_multiple_google_drive_connections_name_no_files(self, tmp_path):
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {"_picked_files": json.dumps([{"id": "1", "name": "Work.gdoc"}])})
v.save("google_drive", "personal", {"_picked_files": json.dumps([{"id": "2", "name": "Personal.gdoc"}])})
ctx = build_datasource_context(v)
assert "Work.gdoc" in ctx and "connection: work" in ctx
assert "Personal.gdoc" in ctx and "connection: personal" in ctx
assert "Connected Google Drive accounts are available" in ctx
assert "Work.gdoc" not in ctx
assert "Personal.gdoc" not in ctx

def test_active_only_suppresses_other_connections_guidance(self, tmp_path):
v = LocalDataVault(tmp_path)
Expand All @@ -235,10 +259,11 @@ def test_active_only_suppresses_other_connections_guidance(self, tmp_path):
assert "Google Drive" not in ctx
assert "Roadmap.gdoc" not in ctx

def test_active_only_on_google_drive_still_shows_its_guidance(self, tmp_path):
def test_active_only_on_google_drive_still_shows_availability(self, tmp_path):
v = LocalDataVault(tmp_path)
v.save("google_drive", "work", {
"_picked_files": json.dumps([{"id": "1", "name": "Roadmap.gdoc"}]),
})
ctx = build_datasource_context(v, active_only="google_drive-work")
assert "Roadmap.gdoc" in ctx
assert "Connected Google Drive accounts are available" in ctx
assert "Roadmap.gdoc" not in ctx
Loading