Skip to content

Add Google Drive metadata search - #292

Open
ndisidore wants to merge 6 commits into
mainfrom
feat/gk-google-drive-search
Open

Add Google Drive metadata search#292
ndisidore wants to merge 6 commits into
mainfrom
feat/gk-google-drive-search

Conversation

@ndisidore

Copy link
Copy Markdown
Member

This adds read-only Google Drive metadata search for a connected account, one organization-owned shared drive, or one file. Agents can find files and folders, inspect their metadata, and page through results without receiving file contents or changing anything in Drive. Each binding stays within its selected scope, and shared workspaces recheck access to every file they have seen. The change also adds resource pickers, OAuth scope mapping, the public TypeScript API, and coverage for queries, pagination, scopes, and sharing.

@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Aug 21, 2026
@ndisidore
ndisidore force-pushed the feat/gk-google-drive-search branch from a876b09 to c33aa40 Compare August 21, 2026 22:46
@github-actions github-actions Bot added the delivery Changes to CI or release delivery label Aug 21, 2026
A code review of the Drive expansion found one blocker class and several
correctness gaps. This is the remediation pass. Nothing here changes what
the feature does; it changes what it is allowed to reach and what it
admits, and it removes a duplicated admission path.

Grants are now recorded rather than inferred. `grantedResourcesFromScopes`
reported a resource as granted when every one of its OAuth scopes was
held, so any account that had ever connected a Doc or a Sheet reported the
whole-account Drive pattern as already granted: `ensureResources` would
then skip consent entirely and `hasDriveResourceGrant` would pass. The
account now persists the resource set the user actually consented to, and
the scope-derived list is frozen to the resources that predate recording.

Drive's batch `files.get` no longer reads a stale token as a denial. The
batch POST returns 200 when a subrequest 401s, so `fetchWithAuthRetry`'s
one-shot refresh never saw it and every file came back inaccessible,
permanently. The batch now forces the same cache invalidation the helper
uses and replays once. Parts are placed by their echoed Content-ID rather
than by arrival order, since these booleans gate observer admission and a
swapped pair admits the wrong collaborator. `corpora`/`driveId` became one
`DriveCorpus` value: a `driveId` without `corpora: "drive"` silently falls
back to the user corpus, and a shared-drive binding's whole boundary is
those two travelling together.

Observer verification is capped again. `maxTrackedSets: null` was
justified as safe for bulk verifiers because their per-open RPC count
stays bounded, but the work behind that RPC is not: a bulk Drive check
issues ceil(N/100) sequential subrequests, per observer, on every open.
The staged-observer rollback also never fired, because it compared a
deserialized KV value against the in-memory stub by reference; it now
turns on a nonce.

Drive observer admission is one path for all three scopes. A file binding
forked in four places and hand-rolled its own verify, which duplicated the
tracker's semantics and did not get the rollback fix above. A shared-drive
or single-file binding is now seeded with the set its scope already names,
so the tracker handles every scope and `addObserver`/`removeObserver` are
one-liners. The factory takes `verifyBatch` rather than a verifier type,
which keeps `drive-session.ts` independent of the worker entrypoint and
makes the seeding invariant testable for the first time.

`getScope()` refuses a provider id that disagrees with the binding, rather
than labelling the binding with another drive's or file's name. `search()`
refuses a single-file binding outright: Drive `q` has no `id =` clause, so
it would have scanned the whole account and post-filtered.

Documentation is matched to the code on the surfaces that describe
authority. Account scope is not limited to My Drive, and reads by ID are
not scope-checked, so the README and `DriveScope` say so. The model-facing
type now records that `fullTextContains` reaches body text, description
and OCR - the README already said it, but the agent never reads the
README. A parity test pins all six `*-types.txt` files to their `.d.ts`,
following the `mcp-shared/base-types` precedent; only `drive-types` had
been checked, and by nothing.

Verified: `pnpm build`, `pnpm lint:check` (0 errors), `pnpm test` across
the workspace, and the release manifest golden test.
@ndisidore
ndisidore marked this pull request as ready for review August 24, 2026 18:03
@Maximo-Guk

Copy link
Copy Markdown
Member

Few GPT findings:

Findings
1. High: OAuth flows can record resources from a different consent attempt. packages/gatekeeper-google/src/google.ts:464-510  
While acceptAuthCode() awaits token exchange, prepareReconnect() can overwrite requestedResources. The earlier callback then records and deletes the newer flow’s resources, potentially granting Drive access never presented in that completed flow. Bind scopes/resources to the OAuth nonce.
2. High: Bulk observer admission has a disclosure race. packages/gatekeeper-google/src/observers.ts:261-276  
A concurrent observation can add an inaccessible file after listTracked() but before admission completes. Although the staged observer is excluded, the overseer ignores that ID until it is persisted at overseer.ts:7945-7947; admission then succeeds without checking the new file. Re-list newly tracked sets before finalizing admission.
3. High: Failed re-verification removes an existing observer. packages/gatekeeper-google/src/observers.ts:261-280  
Reopening overwrites the existing verifier before validation, then deletes it on failure. Previously opened collaborators remain authorized in the sharing graph but disappear from future excludeObservers checks. Preserve and restore the previous observer record on failure.
4. Medium: Shared-drive bindings expose a cross-scope access oracle. packages/gatekeeper-google/src/drive-session.ts:241-244, packages/gatekeeper-google/src/drive-session.ts:300-303  
A readable foreign file produces “outside this Drive binding,” while an inaccessible ID returns Drive’s 403/404 error. This lets a scoped gadget probe the owner’s access to known files outside its capability. Normalize both outcomes.
5. Medium: Empty Drive searches bypass observation authorization. packages/gatekeeper-google/src/drive-session.ts:251-266, packages/gatekeeper-google/src/cursor.ts:109-115  
Terminal empty pages return null without authorizeObservation(). Negative filename/full-text probes therefore bypass auditing and observer enforcement.
6. Medium: User-facing copy understates the account binding’s authority. packages/gatekeeper-google/src/configurator/drive-account-configurator-ui.tsx:13-19, packages/gatekeeper-google/src/google.ts:3003-3008  
It promises “My Drive and Shared with me” metadata only, but getEntry() reaches any accessible shared-drive ID and fullTextContains probes body/OCR text. The consent surface should describe both.
7. Low: File-scoped list() can return trashed files contrary to its contract. packages/gatekeeper-google/src/drive-session.ts:217-220, packages/gatekeeper-google/src/drive-session.ts:269-276  
The exact-file path uses files.get without requesting/checking trashed, while DriveEntry documents that list() never returns trashed entries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

delivery Changes to CI or release delivery gatekeeper Changes to a gatekeeper integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants