UNPKG

@toolbox-sdk/core

Version:
221 lines 11.4 kB
// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ToolboxTool } from './tool.js'; import axios from 'axios'; import { ZodManifestSchema, createZodSchemaFromParams, } from './protocol.js'; import { logApiError } from './errorUtils.js'; import { ZodError } from 'zod'; import { identifyAuthRequirements, resolveValue } from './utils.js'; /** * An asynchronous client for interacting with a Toolbox service. */ class ToolboxClient { #baseUrl; #session; #clientHeaders; /** * Initializes the ToolboxClient. * @param {string} url - The base URL for the Toolbox service API (e.g., "http://localhost:5000"). * @param {AxiosInstance} [session] - Optional Axios instance for making HTTP * requests. If not provided, a new one will be created. * @param {ClientHeadersConfig} [clientHeaders] - Optional initial headers to * be included in each request. */ constructor(url, session, clientHeaders) { this.#baseUrl = url; this.#session = session || axios.create({ baseURL: this.#baseUrl }); this.#clientHeaders = clientHeaders || {}; } /** * Resolves client headers from their provider functions. * @returns {Promise<Record<string, string>>} A promise that resolves to the resolved headers. */ async #resolveClientHeaders() { const resolvedEntries = await Promise.all(Object.entries(this.#clientHeaders).map(async ([key, value]) => { const resolved = await resolveValue(value); return [key, String(resolved)]; })); return Object.fromEntries(resolvedEntries); } /** * Fetches and parses the manifest from a given API path. * @param {string} apiPath - The API path to fetch the manifest from (e.g., "/api/tool/mytool"). * @returns {Promise<Manifest>} A promise that resolves to the parsed manifest. * @throws {Error} If there's an error fetching data or if the manifest structure is invalid. */ async #fetchAndParseManifest(apiPath) { const url = `${this.#baseUrl}${apiPath}`; try { const headers = await this.#resolveClientHeaders(); const config = { headers }; const response = await this.#session.get(url, config); const responseData = response.data; try { const manifest = ZodManifestSchema.parse(responseData); return manifest; } catch (validationError) { let detailedMessage = `Invalid manifest structure received from ${url}: `; if (validationError instanceof ZodError) { const issueDetails = validationError.issues; detailedMessage += JSON.stringify(issueDetails, null, 2); } else if (validationError instanceof Error) { detailedMessage += validationError.message; } else { detailedMessage += 'Unknown validation error.'; } throw new Error(detailedMessage); } } catch (error) { if (error instanceof Error && error.message.startsWith('Invalid manifest structure received from')) { throw error; } logApiError(`Error fetching data from ${url}:`, error); throw error; } } /** * Creates a ToolboxTool instance from its schema. * @param {string} toolName - The name of the tool. * @param {ToolSchemaFromManifest} toolSchema - The schema definition of the tool from the manifest. * @param {BoundParams} [boundParams] - A map of all candidate parameters to bind. * @returns {ReturnType<typeof ToolboxTool>} A ToolboxTool function. */ #createToolInstance(toolName, toolSchema, authTokenGetters = {}, boundParams = {}) { const params = []; const authParams = {}; const currBoundParams = {}; for (const p of toolSchema.parameters) { if (p.authSources && p.authSources.length > 0) { authParams[p.name] = p.authSources; } else if (boundParams && p.name in boundParams) { currBoundParams[p.name] = boundParams[p.name]; } else { params.push(p); } } const [remainingAuthnParams, remainingAuthzTokens, usedAuthKeys] = identifyAuthRequirements(authParams, toolSchema.authRequired || [], authTokenGetters ? Object.keys(authTokenGetters) : []); const paramZodSchema = createZodSchemaFromParams(params); const tool = ToolboxTool(this.#session, this.#baseUrl, toolName, toolSchema.description, paramZodSchema, authTokenGetters, remainingAuthnParams, remainingAuthzTokens, currBoundParams, this.#clientHeaders); const usedBoundKeys = new Set(Object.keys(currBoundParams)); return { tool, usedAuthKeys, usedBoundKeys }; } /** * Asynchronously loads a tool from the server. * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * * @param {string} name - The unique name or identifier of the tool to load. * @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters. * @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tool. * @returns {Promise<ReturnType<typeof ToolboxTool>>} A promise that resolves * to a ToolboxTool function, ready for execution. * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, * or if there's an error fetching data from the API. */ async loadTool(name, authTokenGetters = {}, boundParams = {}) { const apiPath = `/api/tool/${name}`; const manifest = await this.#fetchAndParseManifest(apiPath); if (manifest.tools && Object.prototype.hasOwnProperty.call(manifest.tools, name)) { const specificToolSchema = manifest.tools[name]; const { tool, usedAuthKeys, usedBoundKeys } = this.#createToolInstance(name, specificToolSchema, authTokenGetters || undefined, boundParams || {}); const providedAuthKeys = new Set(authTokenGetters ? Object.keys(authTokenGetters) : []); const providedBoundKeys = new Set(boundParams ? Object.keys(boundParams) : []); const unusedAuth = [...providedAuthKeys].filter(key => !usedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !usedBoundKeys.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for tool '${name}': ${errorMessages.join('; ')}.`); } return tool; } else { throw new Error(`Tool "${name}" not found in manifest from ${apiPath}.`); } } /** * Asynchronously fetches a toolset and loads all tools defined within it. * * @param {string | null} [name] - Name of the toolset to load. If null or undefined, loads the default toolset. * @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters. * @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tools in the toolset. * @param {boolean} [strict=false] - If true, throws an error if any provided auth token or bound param is not used by at least one tool. * @returns {Promise<Array<ReturnType<typeof ToolboxTool>>>} A promise that resolves * to a list of ToolboxTool functions, ready for execution. * @throws {Error} If the manifest structure is invalid or if there's an error fetching data from the API. */ async loadToolset(name, authTokenGetters = {}, boundParams = {}, strict = false) { const toolsetName = name || ''; const apiPath = `/api/toolset/${toolsetName}`; const manifest = await this.#fetchAndParseManifest(apiPath); const tools = []; const overallUsedAuthKeys = new Set(); const overallUsedBoundParams = new Set(); const providedAuthKeys = new Set(authTokenGetters ? Object.keys(authTokenGetters) : []); const providedBoundKeys = new Set(boundParams ? Object.keys(boundParams) : []); for (const [toolName, toolSchema] of Object.entries(manifest.tools)) { const { tool, usedAuthKeys, usedBoundKeys } = this.#createToolInstance(toolName, toolSchema, authTokenGetters || {}, boundParams || {}); tools.push(tool); if (strict) { const unusedAuth = [...providedAuthKeys].filter(key => !usedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !usedBoundKeys.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for tool '${toolName}': ${errorMessages.join('; ')}.`); } } else { usedAuthKeys.forEach(key => overallUsedAuthKeys.add(key)); usedBoundKeys.forEach(key => overallUsedBoundParams.add(key)); } } if (!strict) { const unusedAuth = [...providedAuthKeys].filter(key => !overallUsedAuthKeys.has(key)); const unusedBound = [...providedBoundKeys].filter(key => !overallUsedBoundParams.has(key)); const errorMessages = []; if (unusedAuth.length > 0) { errorMessages.push(`unused auth tokens could not be applied to any tool: ${unusedAuth.join(', ')}`); } if (unusedBound.length > 0) { errorMessages.push(`unused bound parameters could not be applied to any tool: ${unusedBound.join(', ')}`); } if (errorMessages.length > 0) { throw new Error(`Validation failed for toolset '${name || 'default'}': ${errorMessages.join('; ')}.`); } } return tools; } } export { ToolboxClient }; //# sourceMappingURL=client.js.map