Feat: port ApplicationIntegrationToolset and IntegrationConnectorTool (Part 2/2) - #579
Open
AmaadMartin wants to merge 5 commits into
Conversation
AmaadMartin
force-pushed
the
feat/application-integration-toolset-part2
branch
2 times, most recently
from
August 3, 2026 17:20
2be3a73 to
043fcb1
Compare
added 5 commits
August 3, 2026 10:39
Turns an Application Integration trigger, or an Integration Connector connection's entity operations and actions, into ADK tools. The connector arguments (connection name, service name, host, entity, operation, action) are stripped from the model-facing declaration and re-applied after the model's arguments are read, so a model cannot redirect a call. Caller-supplied auth only reaches the connector when the connection enables auth overrides. Because a TypeScript constructor cannot await, the network I/O adk-python performs in __init__ runs on the first getTools() call, memoised so concurrent callers share one initialization.
connectorAuth() only depends on the connection details, so calling it inside the per-operation loop re-evaluated it for every tool and repeated the override warning once per generated operation.
…hat it drops others
…ejection A transient failure on the first getTools() left the memoised promise rejected forever, so the toolset could never produce a tool again. The memo is now cleared on failure, and the connector tools are published only once all of them are built so a retry cannot append duplicates. Also corrects the connector chain test, which asserted the corrupted triggerId the fragment bug produced.
ApplicationIntegrationToolset exposed a public authConfig whose documented workflow - set exchangedAuthCredential on it before getTools() - had no caller anywhere: in adk-python the flow reaches the config through BaseToolset.get_auth_config(), and adk-js has no such hook. At runtime the exchanged credential is already resolved per call by ToolAuthHandler.prepareAuthCredentials, which reads it from tool-context state. Removing the field also removes the clone-on-read branch in getTools and cloneWithAuthCredential, and lets the DEFAULT_CREDENTIAL_KEY export in tool_auth_handler.ts - added solely to feed it - revert entirely, so this stack no longer touches that file. The three tests that drove the field by hand go with it; the remaining auth tests still pin that caller-supplied auth reaches the connector tools only when the connection enables overrides. The hook and this path will land together in the queued follow-up. Also folds the toolset's one-use connection copy into a local and imports the x-* extension type from the module that produces it.
AmaadMartin
force-pushed
the
feat/application-integration-toolset-part2
branch
from
August 3, 2026 17:42
043fcb1 to
b8430c1
Compare
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: adk-python ships a first-class Google Cloud Application Integration / Integration Connectors toolset; adk-js has nothing equivalent. Part 1 of this stack added the API clients that generate the OpenAPI spec. On their own they produce a spec but no tools.
Solution: Part 2 of 2 — add the toolset and the tool that turn that spec into ADK tools. Stacked on
feat/application-integration-toolset-part1; review that PR first.This part adds:
core/src/tools/application_integration_tool/application_integration_toolset.ts—ApplicationIntegrationToolset. Validates the mode, fetches the spec, and produces eitherRestApiTools (viaOpenAPIToolset) orIntegrationConnectorTools.core/src/tools/application_integration_tool/integration_connector_tool.ts—IntegrationConnectorToolplus the exported pure functionfilterConnectorParameters.Part 1 additionally fixes
prepareRequestParamsso a generated path's#<operation>_<entity>fragment is dropped rather than percent-encoded into the query string; without it every connector call here would targettriggerId=api_trigger/ExecuteConnection%23list_Issues, a trigger that does not exist. The chain test in this PR asserts the correctedtriggerId, that the request URL has no fragment, and that it contains no%23.Collision check.
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000(477 open PRs) plusgh pr diff --name-onlyon every plausibly adjacent PR (#462, #463, #464, #465, #528, #529) found no open PR touchingcore/src/tools/application_integration_tool/. #463 also editscore/src/tools/openapi_tool/..., but nottool_auth_handler.ts. The only other overlap is the append-only export block incore/src/common.ts.Security-relevant behaviour
EXCLUDE_FIELDS(connection_name,service_name,host,entity,operation,action,dynamic_auth_config) are removed from the model-facing declaration, andrunAsyncwrites them after spreading the model's arguments. A test asserts that a model-suppliedconnection_name: 'attacker-conn'/operation: 'DELETE_ENTITY'is overwritten with the instance values.logger.info('Running tool: %s with args: %s', ...), and by that pointargscontains the OAuth access token indynamic_auth_config. This port logs the tool name and the argument keys atdebug, never the values.authSchemeandauthCredentialare both supplied but the connection'sauthOverrideEnabledis falsy, the connector tools get neither, and a warning is logged.Parity notes (adk-python is the reference)
Deliberate divergences:
__init__; a TypeScript constructor cannot await. The constructor still validates synchronously and throws the byte-identicalInvalid request, Either integration or (connection and (entity_operations or actions)) should be provided.; the network I/O runs in a privateinitialize()memoised behind one promise, so concurrentgetTools()callers share a single initialization (a test asserts one spec fetch for two concurrent calls). This is documented on the class.authConfig. The approved plan called for a publicauthConfigfield that a caller setsexchangedAuthCredentialon beforegetTools(), with the toolset then returning clones carrying that credential. It is not included, because in adk-js nothing would ever call it: adk-python reaches the config throughBaseToolset.get_auth_config()and adk-js has no such hook, so the field could only be driven by hand from a test. At runtime the exchanged credential is already resolved per call byToolAuthHandler.prepareAuthCredentials, which reads it out of tool-context state. The hook and this path belong together and are queued as one follow-up; shipping the half with no consumer would be dead public API. This also means the stack no longer needs to touchtool_auth_handler.tsat all.FeatureName.JSON_SCHEMA_FOR_FUNC_DECLbetweenparameters_json_schemaandparameters=_to_gemini_schema(...). adk-js has no such feature (core/src/features/feature_registry.tsdeclares onlyPROGRESSIVE_SSE_STREAMING) and the siblingRestApiTool._getDeclaration()already returns the raw JSON schema inparametersunconditionally. One code path, matching the sibling.auth_scheme/auth_credentialare not| str. Python types themOptional[Union[AuthScheme, str]], but thestrarm is never constructed anywhere in adk-python. Typed here asAuthScheme | undefined/AuthCredential | undefined.entityOperationsisRecord<string, string[]>. Python'sApplicationIntegrationToolset.__init__annotates itOptional[str]whileIntegrationClientannotates itOptional[dict[str, list[str]]]and iterates it with.items(). The dict is the real contract; the stalestrannotation is not reproduced.entity/actiondefault to''rather thanNone. They are typedstring, so an absentx-entity/x-actionextension yields''. Not observable on the wire: neither name is a parameter of the operation it is absent from, soprepareRequestParamsdrops it either way.toolFilteris only applied when aReadonlyContextis passed. Python calls the predicate withNonewhen there is no context; adk-js'sToolPredicaterequires one, so with no context every tool is returned. This mirrorsOpenAPIToolset.getToolsexactly rather than inventing a different rule for this toolset. The string-array filter has no such caveat.&or#would corrupt the request.OPTIONAL_FIELDSkeeps the odd'sortByColumns'entry (its siblings are snake_case). Copied verbatim fromintegration_connector_tool.py:68; parity wins for field names. Worth knowing that it is inert in both languages — both OpenAPI parsers snake_case the parameter tosort_by_columns, so the entry never matches. Not "fixed" here.Pre-existing behaviour this port surfaces but does not change: adk-js's
OperationParser.getParamNameinserts an underscore before every capital without collapsing an existing one, so the generatedjira_list_Issuesoperation ID becomes the tool namejira_list__issues(adk-python's_to_snake_caseyieldsjira_list_issues). That is a difference in the shared OpenAPI parser affecting everyopenapi_toolconsumer, not something this port introduces, so it is left alone and the test asserts the real adk-js name. Filed as separate follow-up work.No
any, no@ts-expect-error, noeslint-disable, no coverage suppression was added.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/tools/application_integration_tool core/test/utils/service_account_utils_test.ts→ 147 passed (the whole stack), with 100% statements / branches / functions / lines on every file the stack adds.The tests avoid mocking what can be run for real:
integration_connector_tool_test.tsdrives a realRestApiTool, a realToolAuthHandlerand a realContext(built from a realInvocationContext/LlmAgent/session), so the pending-auth path and the credential-exchange path are exercised rather than stubbed.application_integration_toolset_integration_test.tswires the whole chain — realIntegrationClient→ConnectionsClient→OpenApiSpecParser→RestApiTool→IntegrationConnectorTool— with onlyglobalThis.fetchandgoogle-auth-librarystubbed, and asserts that invoking the tool POSTs to/v2/projects/.../integrations/ExecuteConnection:executewithtriggerId=api_trigger/ExecuteConnection#list_Issuesand a body carryingconnectionName,serviceName,host,entityandoperation. That is the assertion that catches spec-builder drift end to end.CI does not run on this PR. The
validationworkflow ispull_request: branches: [main], and this PR targetsfeat/application-integration-toolset-part1, so no test job is triggered. It was therefore validated locally on the exact pushed commitb8430c10:npx vitest run --project unit:core core/test/tools/application_integration_tool core/test/utils/service_account_utils_test.tsnpm run buildnpx tsc --noEmit -p core/tsconfig.jsonnpx eslint "core/**/*.ts"npx prettier --check "core/**/*.ts"npm run docs:checkbash scripts/check_license.shPart 1 (#578) carries the client half of this stack through the real matrix CI on
main, and is fully green on the reviewed commit:run-testspasses on ubuntu-latest, macos-latest and windows-latest, alongside the cross-language job andcheck-license.Initialization is retryable. A transient failure on the first
getTools()used to leave the memoised promise permanently rejected, so the toolset could never produce a tool again. The memo is now cleared on failure, and the connector tools are published only once all of them are built, so a retry after a partial failure cannot append duplicates. Both behaviours are tested.Proof the tests can fail. Each mutation was applied to the source, the targeted test file re-run, then the mutation reverted:
'dynamic_auth_config'fromEXCLUDE_FIELDSexpected [ 'user_id', …(3) ] to deeply equal [ 'user_id', 'page_size', 'filter' ]action: operation['x-action'] ?? '')expected newConstructor{…} to match object { entity: 'Issues', action: '' }authOverrideEnabledguardexpected newConstructor{…} to match object { authScheme: {…}, …(2) }filterConnectorParametersfilters in place instead of copyingexpected { properties: { user_id: {} }, …(1) } to deeply equal {…}catchthat clearsinitPromise)retries after a failed initialization instead of staying empty,does not duplicate tools across a retryoverwrites connector arguments supplied by the modelManual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Not runnable in CI — it needs a real GCP project with Application Integration provisioned and a live connector connection. With ADC pointed at such a project:
ExecuteConnectionwith triggerapi_trigger/ExecuteConnectionin the same region as the connection.connectorOutputPayload.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.