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
3 changes: 2 additions & 1 deletion core/src/tools/openapi_tool/auth/auth_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {OpenAPIV3} from 'openapi-types';
import {AuthCredential} from '../../../auth/auth_credential.js';
import {AuthScheme} from '../../../auth/auth_schemes.js';

/**
* Applies the given credential to the request headers and URL.
Expand All @@ -20,7 +21,7 @@ export function applyCredential(
url: string,
headers: Record<string, string>,
credential?: AuthCredential,
authScheme?: OpenAPIV3.SecuritySchemeObject,
authScheme?: AuthScheme,
): string {
if (!credential) return url;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {OpenAPIV3} from 'openapi-types';
import {AuthScheme} from '../../../auth/auth_schemes.js';
import {experimental} from '../../../utils/experimental.js';
import {ApiParameter, OperationParser} from './operation_parser.js';

Expand All @@ -31,7 +32,7 @@ export interface ParsedOperation {
operation: OpenAPIV3.OperationObject;
parameters: ApiParameter[];
returnValue?: ApiParameter;
authScheme?: OpenAPIV3.SecuritySchemeObject;
authScheme?: AuthScheme;
}

@experimental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {OpenAPIV3} from 'openapi-types';
import {Context} from '../../../agents/context.js';
import {AuthCredential} from '../../../auth/auth_credential.js';
import {AuthScheme} from '../../../auth/auth_schemes.js';
import {AuthConfig} from '../../../auth/auth_tool.js';
import {experimental} from '../../../utils/experimental.js';
import {AutoAuthCredentialExchanger} from '../auth/credential_exchangers/auto_auth_credential_exchanger.js';
Expand All @@ -19,14 +19,12 @@ export interface AuthPreparationResult {
class ToolContextCredentialStore {
constructor(private readonly context: Context) {}

getCredentialKey(authScheme?: OpenAPIV3.SecuritySchemeObject): string {
getCredentialKey(authScheme?: AuthScheme): string {
const schemeName = authScheme?.type || 'default';
return `${schemeName}_existing_exchanged_credential`;
}

getCredential(
authScheme?: OpenAPIV3.SecuritySchemeObject,
): AuthCredential | undefined {
getCredential(authScheme?: AuthScheme): AuthCredential | undefined {
const key = this.getCredentialKey(authScheme);
// Read through the State API so we see values persisted from previous
// tool calls. `context.state` is a `State` instance, not a plain object;
Expand All @@ -47,15 +45,15 @@ class ToolContextCredentialStore {
export class ToolAuthHandler {
constructor(
private readonly context: Context,
private readonly authScheme?: OpenAPIV3.SecuritySchemeObject,
private readonly authScheme?: AuthScheme,
private readonly authCredential?: AuthCredential,
private readonly credentialKey?: string,
) {}

@experimental
public static fromToolContext(
context: Context,
authScheme?: OpenAPIV3.SecuritySchemeObject,
authScheme?: AuthScheme,
authCredential?: AuthCredential,
options: {credentialKey?: string} = {},
): ToolAuthHandler {
Expand Down
3 changes: 2 additions & 1 deletion core/src/tools/openapi_tool/openapi_toolset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import yaml from 'js-yaml';
import {OpenAPIV3} from 'openapi-types';
import {ReadonlyContext} from '../../agents/readonly_context.js';
import {AuthCredential} from '../../auth/auth_credential.js';
import {AuthScheme} from '../../auth/auth_schemes.js';
import {experimental} from '../../utils/experimental.js';
import {BaseTool} from '../base_tool.js';
import {BaseToolset, ToolPredicate} from '../base_toolset.js';
Expand All @@ -26,7 +27,7 @@ export class OpenAPIToolset extends BaseToolset {
toolFilter?: ToolPredicate | string[];
prefix?: string;
preservePropertyNames?: boolean;
authScheme?: OpenAPIV3.SecuritySchemeObject;
authScheme?: AuthScheme;
authCredential?: AuthCredential;
credentialKey?: string;
headerProvider?: (context: ReadonlyContext) => Record<string, string>;
Expand Down
7 changes: 4 additions & 3 deletions core/src/tools/openapi_tool/rest_api_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {OpenAPIV3} from 'openapi-types';
import {Context} from '../../agents/context.js';
import {ReadonlyContext} from '../../agents/readonly_context.js';
import {AuthCredential} from '../../auth/auth_credential.js';
import {AuthScheme} from '../../auth/auth_schemes.js';
import {experimental} from '../../utils/experimental.js';
import {BaseTool, RunAsyncToolRequest} from '../base_tool.js';
import {applyCredential} from './auth/auth_helpers.js';
Expand All @@ -32,7 +33,7 @@ export class RestApiTool extends BaseTool {
description: string,
private readonly endpoint: OperationEndpoint,
private readonly operation: OpenAPIV3.OperationObject,
private authScheme?: OpenAPIV3.SecuritySchemeObject,
private authScheme?: AuthScheme,
private authCredential?: AuthCredential,
options: {
preservePropertyNames?: boolean;
Expand All @@ -49,7 +50,7 @@ export class RestApiTool extends BaseTool {
}

@experimental
public configureAuthScheme(authScheme: OpenAPIV3.SecuritySchemeObject) {
public configureAuthScheme(authScheme: AuthScheme) {
this.authScheme = authScheme;
}

Expand Down Expand Up @@ -282,7 +283,7 @@ export function createRestApiTool(
description: string;
endpoint: OperationEndpoint;
operation: OpenAPIV3.OperationObject;
authScheme?: OpenAPIV3.SecuritySchemeObject;
authScheme?: AuthScheme;
},
options: {
preservePropertyNames?: boolean;
Expand Down
27 changes: 26 additions & 1 deletion core/test/tools/openapi_tool/auth_helpers_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

import {OpenAPIV3} from 'openapi-types';
import {describe, expect, it} from 'vitest';
import {AuthCredential} from '../../../src/auth/auth_credential.js';
import {
AuthCredential,
AuthCredentialTypes,
} from '../../../src/auth/auth_credential.js';
import {
applyCredential,
createApiKeyScheme,
Expand Down Expand Up @@ -97,6 +100,28 @@ describe('auth_helpers', () => {
expect(result).toBe(url);
expect(headers['Authorization']).toBe('Bearer my_token');
});

it('applies an API key credential when given an OpenID Connect scheme with endpoint config', () => {
const url = 'http://example.com';
const headers: Record<string, string> = {};
const credential: AuthCredential = {
authType: AuthCredentialTypes.API_KEY,
apiKey: 'secret_key',
};

const result = applyCredential(url, headers, credential, {
type: 'openIdConnect',
openIdConnectUrl:
'https://issuer.example.com/.well-known/openid-configuration',
authorizationEndpoint: 'https://issuer.example.com/authorize',
tokenEndpoint: 'https://issuer.example.com/token',
});

// A non-apiKey scheme names no location, so the default Authorization
// header branch runs.
expect(result).toBe(url);
expect(headers['Authorization']).toBe('secret_key');
});
});

describe('createApiKeyScheme', () => {
Expand Down
40 changes: 38 additions & 2 deletions core/test/tools/openapi_tool/openapi_toolset_integration_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {Context, OpenAPIToolset} from '@google/adk';
import {Context, OpenAPIToolset, RestApiTool} from '@google/adk';
import * as fs from 'fs';
import * as path from 'path';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';

describe('OpenAPIToolset Integration', () => {
let truanonSpec: string;
Expand All @@ -20,6 +20,10 @@ describe('OpenAPIToolset Integration', () => {
globalThis.fetch = vi.fn();
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should parse truanon spec and create tools', async () => {
const toolset = new OpenAPIToolset({
specStr: truanonSpec,
Expand All @@ -32,6 +36,38 @@ describe('OpenAPIToolset Integration', () => {
expect(toolNames).toContain('get_token');
});

it('builds tools from a spec string with an OpenID Connect scheme carrying endpoint config', async () => {
const configure = vi.spyOn(RestApiTool.prototype, 'configureAuthScheme');

const toolset = new OpenAPIToolset({
specStr: truanonSpec,
specType: 'yaml',
authScheme: {
type: 'openIdConnect',
openIdConnectUrl:
'https://issuer.example.com/.well-known/openid-configuration',
authorizationEndpoint: 'https://issuer.example.com/authorize',
tokenEndpoint: 'https://issuer.example.com/token',
grantTypesSupported: ['authorization_code'],
},
});
const tools = await toolset.getTools();

const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain('get_profile');
expect(toolNames).toContain('get_token');

// The override reaches every tool with its endpoint config intact.
expect(configure).toHaveBeenCalledTimes(tools.length);
for (const call of configure.mock.calls) {
expect(call[0]).toMatchObject({
type: 'openIdConnect',
tokenEndpoint: 'https://issuer.example.com/token',
grantTypesSupported: ['authorization_code'],
});
}
});

it('should execute a tool with mocked fetch', async () => {
const toolset = new OpenAPIToolset({
specStr: truanonSpec,
Expand Down
42 changes: 40 additions & 2 deletions core/test/tools/openapi_tool/openapi_toolset_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {OpenApiSpecParser, OpenAPIToolset, ReadonlyContext} from '@google/adk';
import {
OpenApiSpecParser,
OpenAPIToolset,
ReadonlyContext,
RestApiTool,
} from '@google/adk';
import {OpenAPIV3} from 'openapi-types';
import {describe, expect, it} from 'vitest';
import {afterEach, describe, expect, it, vi} from 'vitest';

describe('OpenAPIToolset', () => {
afterEach(() => {
vi.restoreAllMocks();
});

const mockSpec: OpenAPIV3.Document = {
openapi: '3.0.0',
info: {
Expand Down Expand Up @@ -125,6 +134,35 @@ describe('OpenAPIToolset', () => {
).toEqual({api_key: 'my-key'});
});

it('applies an OpenID Connect scheme with endpoint config as a global auth override', async () => {
const configure = vi.spyOn(RestApiTool.prototype, 'configureAuthScheme');

const toolset = new OpenAPIToolset({
specDict: mockSpec,
authScheme: {
type: 'openIdConnect',
openIdConnectUrl:
'https://issuer.example.com/.well-known/openid-configuration',
authorizationEndpoint: 'https://issuer.example.com/authorize',
tokenEndpoint: 'https://issuer.example.com/token',
},
});
const tools = await toolset.getTools();

expect(tools.length).toBe(2);
// The override reaches every tool with its endpoint config intact.
expect(configure).toHaveBeenCalledTimes(2);
for (const call of configure.mock.calls) {
expect(call[0]).toEqual({
type: 'openIdConnect',
openIdConnectUrl:
'https://issuer.example.com/.well-known/openid-configuration',
authorizationEndpoint: 'https://issuer.example.com/authorize',
tokenEndpoint: 'https://issuer.example.com/token',
});
}
});

it('should return all tools when no toolFilter is set and a context is provided', async () => {
const toolset = new OpenAPIToolset({specDict: mockSpec});
const tools = await toolset.getTools({} as unknown as ReadonlyContext);
Expand Down
Loading
Loading