Feat: port the GCS toolsets (GCSToolset / GCSAdminToolset) from adk-python - #528
Open
AmaadMartin wants to merge 4 commits into
Open
Feat: port the GCS toolsets (GCSToolset / GCSAdminToolset) from adk-python#528AmaadMartin wants to merge 4 commits into
AmaadMartin wants to merge 4 commits into
Conversation
added 4 commits
August 2, 2026 08:36
Adds core/src/integrations/gcs with GCSToolset (object operations) and GCSAdminToolset (bucket administration), kept as two classes so an agent can be granted object access without bucket-level privileges. Tools are built on FunctionTool and gated by GCSToolSettings capabilities.
Also replaces GCSCredentialsConfig.credentials (typed from google-auth-library) with storageOptions: the repo's google-auth-library 10.x AuthClient is not assignable to the 9.15.1 AuthClient that the installed @google-cloud/storage expects, and StorageOptions is the type the client actually accepts.
Standardises the acronym on Gcs (matching GcsArtifactService), drops the redundant gcs_ prefix inside integrations/gcs, mirrors each source module with a same-named test, collapses the two toolsets onto a shared GcsToolsetBase, and extracts the duplicated list/paginate and object-handle blocks. The client cache moves from a module-level WeakMap onto the toolset instance, keeping the size cap.
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Closes: #issue_number
Related: #issue_number
Problem: An adk-js agent has no way to talk to Google Cloud Storage as a tool. adk-python ships two
BaseToolsetsubclasses undersrc/google/adk/integrations/gcs/that give an agent GCS access, deliberately split along a privilege boundary. adk-js has no equivalent.Solution: Port the two toolsets to
core/src/integrations/gcs/, built onFunctionTool:GcsToolset— object operations:gcs_get_bucket,gcs_list_objects,gcs_get_object_metadata,gcs_get_object_dataand, withGcsCapability.READ_WRITE,gcs_create_objectandgcs_delete_objects.GcsAdminToolset— bucket administration:gcs_list_bucketsand, withREAD_WRITE,gcs_create_bucket,gcs_update_bucket,gcs_delete_bucket.The two classes stay separate on purpose: that is what lets an operator grant an agent read/write access to objects without also granting it the ability to delete a bucket. They share
GcsToolsetBase(capability gating, client memoisation, tool filtering) and differ only in which tools they may expose, which is the decision the split exists to make. Capability gating (GcsToolSettings) defaults to read-only, sonew GcsToolset({})returns exactly four tools and no mutation is possible. No new dependency:@google-cloud/storageis already acoredependency.Collision check.
gh pr list --repo <fork> --state open --limit 1000filtered forgcs|storage|bucket|blobreturned three PRs;gh pr diff --name-onlyon each shows none of them touchescore/src/integrations/or adds GCS tools. The closest is #387 ("make @google-cloud/storage and the OTel GCP exporters optional peer dependencies"), which is adjacent but disjoint — it changes howgcs_artifact_service.ts/telemetryload the package and does not land this feature. This PR branches frommain; if #387 merges first, the GCS toolsets would need the same lazy-load treatment as a follow-up.Deliberate divergences from adk-python (each verified against the referenced Python file):
Gcs, notGCS. adk-python exportsGCSToolset/GCSAdminToolset/GCSCredentialsConfig. Class names never cross the model boundary, so local convention wins: this repo's other Cloud Storage class isGcsArtifactService, and mixingGcsToolResultwithGCSToolsetinside one feature is worse than either alone. Everything model-facing — tool names, parameter names, response keys, status values — is unchanged.getTools(). adk-python'sBaseToolsethas a finalget_tools_with_prefix()that the agent calls; adk-js'sllm_agent.tscallsgetTools()directly, so the toolset applies the prefix itself, exactly asMCPToolsetdoes. Observable behaviour is identical: the model seesgcs_get_bucket.toolFiltermatches the prefixed name (['gcs_get_bucket']), because adk-js'sBaseToolset.isToolSelectedcomparestool.name. adk-python filters before prefixing. Local convention wins:toolFilteris a construction-time developer API that never crosses the model boundary.delete_objectsformats the name list as[a, b]instead of Python's list repr['a', 'b'], which is a Python-specific rendering.update_bucketpatches throughstorage.bucket(name).setMetadata(...)and skips the request entirely when neither updatable field is supplied. adk-python fetches the bucket first and then callspatch(); dropping that fetch removes a round trip, at the cost of not surfacing a missing bucket when there is nothing to change.delete_objectsissuesPromise.all(names.map(n => bucket.file(n).delete())).@google-cloud/storagehas nodelete_blobsequivalent;deleteFilesdeletes by query, which is a different contract.GoogleTool/ OAuth consent flow. adk-python wraps each function inGoogleTool, which resolves OAuth credentials and injectscredentials=at call time. adk-js has no such class; auth comes fromGcsCredentialsConfigor Application Default Credentials. PortingGoogleToolis out of scope for this change.credentialsConfig, the client is built asnew Storage({userAgent})so ADC applies. This has no adk-python analogue, but every@google-cloud/*client defaults to ADC and a JS user expects it.Where the approved plan was wrong, and what shipped instead:
GCSCredentialsConfig.credentials?: AuthClient | GoogleAuthimported fromgoogle-auth-library^10.3.0. That does not typecheck. The installed@google-cloud/storage@7.17.1resolvesgoogle-auth-library@9.15.1, whoseAuthClientdeclares agaxiosmember that the rootgoogle-auth-library@10.7.0AuthClientdoes not have, so a v10 client is not assignable toStorageOptions['authClient'](andnew OAuth2Client(...)from v10 is not either). Making it compile would have required an unchecked cast. Instead the field isstorageOptions?: StorageOptions— the type the Storage client actually accepts, and already part of this repo's public API throughGcsArtifactService. A pre-built client is passed as{storageOptions: {authClient}}; theclientId/clientSecretpath becomesclientOptions: {clientId, clientSecret}, which hands the credentials to the Storage client's own auth stack rather than to a second copy of the auth library. Noascasts and no suppressions anywhere in the diff.toStorageOptions(), which also keepscredentials.tsfrom importingclient.tswhileclient.tsimports it back.GCS_TOOL_NAME_PREFIXintypes.ts, notDEFAULT_GCS_TOOL_NAME_PREFIXinstorage_toolset.ts: nothing overrides it, so it is not a default, and keeping it intypes.tslets the tool modules build prefixed names without importing their own toolset.core/src/index.tsonly, notcore/src/common.ts:common.tsis re-exported byindex_web.ts(the browser bundle), and the plan forbids touchingindex_web.ts.GcsArtifactService, the other@google-cloud/storageconsumer, is exported the same way.DEFAULT_GCS_TOOL_SETTINGSis replaced byresolveAccess(settings)inhelpers.ts, a single helper both toolsets share instead of an exported mutable default object.client_test.ts,credentials_test.ts,storage_tool_test.ts,admin_tool_test.ts,storage_toolset_test.ts,admin_toolset_test.ts) instead of the plan'sgcs_storage_toolset_test.ts/gcs_toolset_test.tspair, which were two near-identical names for the same two classes.Known limitation (also stated in the
GcsCredentialsConfigdoc comment): an OAuth client id and secret alone carry no access token. adk-python mints one by driving the interactive consent flow insideGoogleTool; adk-js has no equivalent yet, so calls made through that path fail at request time and surface as a normal{status: 'ERROR'}result. The production paths today are a pre-built client viastorageOptions, or ADC.Security notes.
.describe()text:gcs_create_object.source_file_path(reads an arbitrary local file and uploads it) andgcs_get_object_data.destination_file_path(writes an arbitrary local path). They are ported because they exist in adk-python; they were not widened (no globs, no directory recursion) and no opt-out knob was added.Storageclient cache lives on the toolset instance and is capped at 16 entries, dropped wholesale on overflow, because project ids reach it from model-supplied tool arguments.{status: 'ERROR', error_details}; no tool throws.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
npx vitest run --project unit:core core/test/integrations/gcs→ 77 passed in 6 files:storage_tool_test.ts(24) — every object tool, including the 404 →Object X not found in bucket Ymapping, UTF-8 vs base64 payload decoding,generationselection,destination_file_path,source_file_path, "neither data nor source_file_path", pagination arguments, and an error path for a read tool, a write tool and a non-Errorrejection.admin_tool_test.ts(13) — every bucket tool, bothupdate_bucketfields, the no-field case that issues no patch, and an error path per tool.storage_toolset_test.ts(12) andadmin_toolset_test.ts(11) — capability gating (4/6 and 1/4 tools, 0 withcapabilities: []), the privilege-boundary regression (no bucket verb ever reachable fromGcsToolset, no object verb ever reachable fromGcsAdminToolset, under every setting), array and predicatetoolFilter,close(), and a table per toolset pinning the exact snake_case parameter names andrequiredlists the model sees.credentials_test.ts(12) — every validation branch and bothtoStorageOptions()branches.client_test.ts(5) — user agent, config-derived options, memoisation, one client per project, and cache overflow.Coverage of the new code, measured with
npx vitest run --project unit:core core/test/integrations/gcs --coverage --coverage.include='core/src/integrations/gcs/**': 100% statements, 100% branches, 100% functions, 100% lines across all nine new source files.Proof the tests can fail. Each mutation was applied to the source, the targeted suite was run, and the source was reverted:
toolset_base.ts: drop theaccess.writebranchAssertionError: Toolset does not expose a tool named gcs_create_bucket.helpers.ts: decode withbytes.toString('utf8')instead of the fatalTextDecoderexpected { status: 'SUCCESS', …(2) } to deeply equal { status: 'SUCCESS', …(2) }(base64 case)admin_tool.ts: movedelete_bucketintocreateAdminReadToolsexpected [ 'gcs_delete_bucket', …(1) ] to deeply equal [ 'gcs_list_buckets' ]helpers.ts: dropautoPaginate: falsefrom the page optionsexpected "spy" to be called with arguments: [ { maxResults: 1, …(2) } ]helpers.ts: always attachnext_page_tokenexpected { status: 'SUCCESS', …(2) } to strictly equal { status: 'SUCCESS', …(1) }storage_tool.ts: return the generic error instead of the not-found messageexpected { status: 'ERROR', …(1) } to deeply equal { status: 'ERROR', …(1) }storage_tool.ts: rename thepage_sizeparameter topageSizeexpected [ 'bucket_name', 'prefix', …(2) ] to deeply equal [ 'bucket_name', 'prefix', …(2) ]storage_tool.ts: ignoregenerationinobjectHandleexpected "spy" to be called with arguments: [ 'test-object', { generation: 1 } ]admin_tool.ts: always callsetMetadata, even with no fieldsexpected "spy" to not be called at all, but actually been called 1 timestoolset_base.ts: remove the client cache hitexpected "spy" to be called 1 times, but got 2 timescredentials.ts: accept a client id or a secret instead of bothexpected [Function] to throw an errorNo integration test was added. Every operation needs a live bucket and real credentials, so a test under
tests/integration/would perform network I/O. The tool and client tests mock@google-cloud/storagewithvi.hoisted+vi.mock, followingcore/test/artifacts/gcs_artifact_service_test.ts; nothing in this change touches the network.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
gcloud auth application-default loginand pick a project with a Cloud Storage bucket.gcs_delete_buckettool is offered, so it cannot.new GcsAdminToolset({toolSettings: {capabilities: [GcsCapability.READ_WRITE]}})totoolsand confirmgcs_list_bucketsandgcs_delete_bucketnow appear.Local validation on the pushed commit:
npx vitest run --project unit:core core/test/integrations/gcs— 77 passed.npx tsc --noEmit -p core/tsconfig.json— clean.npm run build— clean.npm run lint— clean.npm run format:check— clean.npm run docs:check— clean (all types in public signatures are exported).bash scripts/check_license.sh— clean.npx secretlinton the new files — clean.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.