Skip to content
Open
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
4 changes: 4 additions & 0 deletions core/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,11 @@ export * from './artifacts/base_artifact_service.js';
export * from './features/feature_registry.js';
export * from './memory/base_memory_service.js';
export * from './sessions/base_session_service.js';
export {ApplicationIntegrationToolset} from './tools/application_integration_tool/application_integration_toolset.js';
export type {ApplicationIntegrationToolsetOptions} from './tools/application_integration_tool/application_integration_toolset.js';
export type {EntityOperations} from './tools/application_integration_tool/clients/integration_client.js';
export {IntegrationConnectorTool} from './tools/application_integration_tool/integration_connector_tool.js';
export type {IntegrationConnectorToolOptions} from './tools/application_integration_tool/integration_connector_tool.js';
export * from './tools/base_tool.js';
export {OpenApiSpecParser} from './tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.js';
export type {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {OpenAPIV3} from 'openapi-types';
import {ReadonlyContext} from '../../agents/readonly_context.js';
import {
AuthCredential,
AuthCredentialTypes,
} from '../../auth/auth_credential.js';
import {AuthScheme} from '../../auth/auth_schemes.js';
import {experimental} from '../../utils/experimental.js';
import {logger} from '../../utils/logger.js';
import {parseServiceAccountCredential} from '../../utils/service_account_utils.js';
import {BaseTool} from '../base_tool.js';
import {BaseToolset, ToolPredicate} from '../base_toolset.js';
import {OpenApiSpecParser} from '../openapi_tool/openapi_spec_parser/openapi_spec_parser.js';
import {OpenAPIToolset} from '../openapi_tool/openapi_toolset.js';
import {createRestApiTool} from '../openapi_tool/rest_api_tool.js';
import {
ConnectionDetails,
ConnectionsClient,
} from './clients/connections_client.js';
import {ConnectorOperationExtensions} from './clients/connector_spec_builders.js';
import {
EntityOperations,
IntegrationClient,
} from './clients/integration_client.js';
import {IntegrationConnectorTool} from './integration_connector_tool.js';

const CLOUD_PLATFORM_SCOPES = [
'https://www.googleapis.com/auth/cloud-platform',
];

const INVALID_REQUEST_MESSAGE =
'Invalid request, Either integration or (connection and (entity_operations' +
' or actions)) should be provided.';

/** Constructor options for {@link ApplicationIntegrationToolset}. */
export interface ApplicationIntegrationToolsetOptions {
/** The Google Cloud project ID. */
project: string;
/** The Google Cloud location, e.g. `us-central1`. */
location: string;
/** Overrides the default `ExecuteConnection` integration name. */
connectionTemplateOverride?: string;
/** The integration name. */
integration?: string;
/** The trigger IDs to publish from the integration. */
triggers?: string[];
/** The connection name. */
connection?: string;
/** The entity operations to publish from the connection. */
entityOperations?: EntityOperations;
/** The actions to publish from the connection. */
actions?: string[];
/** Prepended to every generated tool name. */
toolNamePrefix?: string;
/** Appended to every generated tool description. */
toolInstructions?: string;
/**
* A service account key file. Required when Application Default Credentials
* are not available or should not be used.
*/
serviceAccountJson?: string;
authScheme?: AuthScheme;
authCredential?: AuthCredential;
/** Tool predicate, or the names of the tools to expose. */
toolFilter?: ToolPredicate | string[];
credentialKey?: string;
}

/**
* Generates tools from an Application Integration or Integration Connector
* resource.
*
* Unlike adk-python, whose constructor performs the HTTP calls, the network I/O
* runs on the first `getTools()` call because a TypeScript constructor cannot
* await. The constructor still validates the requested mode and throws
* synchronously.
*
* @example
* ```ts
* // Publish an integration's API triggers as tools.
* const toolset = new ApplicationIntegrationToolset({
* project: 'test-project',
* location: 'us-central1',
* integration: 'test-integration',
* triggers: ['api_trigger/test_trigger'],
* });
*
* // Publish a connection's entity operations and actions as tools. See
* // https://cloud.google.com/integration-connectors/docs/reference/rest/v1/projects.locations.connections.connectionSchemaMetadata
* // for the operations and actions a connection supports.
* const connectorToolset = new ApplicationIntegrationToolset({
* project: 'test-project',
* location: 'us-central1',
* connection: 'test-connection',
* entityOperations: {Issues: ['LIST', 'GET'], Projects: []},
* actions: ['ExecuteCustomQuery'],
* });
* ```
*/
@experimental
export class ApplicationIntegrationToolset extends BaseToolset {
readonly project: string;
readonly location: string;

private readonly options: ApplicationIntegrationToolsetOptions;
private tools: IntegrationConnectorTool[] = [];
private openapiToolset?: OpenAPIToolset;
private initPromise?: Promise<void>;

constructor(options: ApplicationIntegrationToolsetOptions) {
super(options.toolFilter || []);
if (!isValidMode(options)) {
throw new Error(INVALID_REQUEST_MESSAGE);
}

this.options = options;
this.project = options.project;
this.location = options.location;
}

@experimental
override async getTools(context?: ReadonlyContext): Promise<BaseTool[]> {
await this.initialize();

if (this.openapiToolset) {
return this.openapiToolset.getTools(context);
}

return this.tools.filter((tool) => {
if (Array.isArray(this.toolFilter) && this.toolFilter.length > 0) {
return this.toolFilter.includes(tool.name);
}
if (context) {
return this.isToolSelected(tool, context);
}
return true;
});
}

@experimental
override async close(): Promise<void> {
await this.openapiToolset?.close();
}

/**
* Fetches the spec and builds the tools once, no matter how many callers race
* on the first `getTools()`. A failed attempt is not memoised, so a transient
* network failure does not leave the toolset permanently empty.
*/
private initialize(): Promise<void> {
this.initPromise ??= this.fetchAndBuildTools().catch((err: unknown) => {
this.initPromise = undefined;
throw err;
});
return this.initPromise;
}

private async fetchAndBuildTools(): Promise<void> {
const {
project,
location,
connection = '',
serviceAccountJson,
} = this.options;
const integrationClient = new IntegrationClient({
project,
location,
connectionTemplateOverride: this.options.connectionTemplateOverride,
integration: this.options.integration,
triggers: this.options.triggers,
connection: this.options.connection,
entityOperations: this.options.entityOperations,
actions: this.options.actions,
serviceAccountJson,
});
const {authScheme, authCredential} =
buildSpecCredentials(serviceAccountJson);

if (this.options.integration) {
this.openapiToolset = new OpenAPIToolset({
specDict: await integrationClient.getOpenApiSpecForIntegration(),
authScheme,
authCredential,
credentialKey: this.options.credentialKey,
toolFilter: this.toolFilter,
});
return;
}

const connectionDetails = await new ConnectionsClient({
project,
location,
connection,
serviceAccountJson,
}).getConnectionDetails();
const spec = await integrationClient.getOpenApiSpecForConnection(
this.options.toolNamePrefix ?? '',
this.options.toolInstructions ?? '',
);

const connectorAuth = this.connectorAuth(connectionDetails);
const tools: IntegrationConnectorTool[] = [];

for (const parsed of new OpenApiSpecParser().parse(spec)) {
const operation: OpenAPIV3.OperationObject<ConnectorOperationExtensions> =
parsed.operation;
const restApiTool = createRestApiTool(parsed);
restApiTool.configureAuthScheme(authScheme);
restApiTool.configureAuthCredential(authCredential);

tools.push(
new IntegrationConnectorTool({
name: restApiTool.name,
description: restApiTool.description,
connectionName: connectionDetails.name,
connectionHost: connectionDetails.host,
connectionServiceName: connectionDetails.serviceName,
entity: operation['x-entity'] ?? '',
action: operation['x-entity'] ? '' : (operation['x-action'] ?? ''),
operation: operation['x-operation'] ?? '',
restApiTool,
...connectorAuth,
credentialKey: this.options.credentialKey,
}),
);
}

// Published only once every tool is built, so a retry after a partial
// failure cannot leave duplicates behind.
this.tools = tools;
}

/**
* Caller-supplied auth only reaches the connector when the connection allows
* it to be overridden.
*/
private connectorAuth(connectionDetails: ConnectionDetails): {
authScheme?: AuthScheme;
authCredential?: AuthCredential;
} {
const {authScheme, authCredential} = this.options;
if (
authScheme &&
authCredential &&
!connectionDetails.authOverrideEnabled
) {
logger.warn(
'Authentication schema and credentials are not used because' +
' authOverrideEnabled is not enabled in the connection.',
);
return {};
}
return {authScheme, authCredential};
}
}

function isValidMode(options: ApplicationIntegrationToolsetOptions): boolean {
if (options.integration) {
return true;
}
const hasEntityOperations =
Object.keys(options.entityOperations ?? {}).length > 0;
return Boolean(
options.connection && (hasEntityOperations || options.actions?.length),
);
}

/**
* Credentials used to fetch the generated spec's endpoints: an explicit service
* account when one is configured, Application Default Credentials otherwise.
*/
function buildSpecCredentials(serviceAccountJson?: string): {
authScheme: OpenAPIV3.HttpSecurityScheme;
authCredential: AuthCredential;
} {
const authScheme: OpenAPIV3.HttpSecurityScheme = {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
};
const serviceAccount = serviceAccountJson
? {
serviceAccountCredential:
parseServiceAccountCredential(serviceAccountJson),
scopes: CLOUD_PLATFORM_SCOPES,
}
: {useDefaultCredential: true, scopes: CLOUD_PLATFORM_SCOPES};

return {
authScheme,
authCredential: {
authType: AuthCredentialTypes.SERVICE_ACCOUNT,
serviceAccount,
},
};
}
Loading