Skip to content

Commit 6611291

Browse files
feat(toolbox-core): Add auth token getters (#38)
* feat: add client headers * lint * lint * resolve comments * disable any lint warning on a line * unclean working code * fix client tests * add utils test * cleanup * fix * fix tests * lint * fix any type issues * fix docstrings * remove redundant check * merge fix * Only intercept request if it is sent to the toolbox server * utility functions + e2e tests * Add to client and tool * lint * Only run e2e tests * fix e2e test * fix test * try fix * revert change * rename variable * fix param handling * lint * feat(toolbox-core): Add helper methods for retrieving Google ID Tokens (#32) * working code * fix tests * lint * Revert "feat(toolbox-core): Add helper methods for retrieving Google ID Token…" (#39) This reverts commit f747e60. * fix e2e test * fix e2e test * fix e2e tests * fix e2e tests * fix tests * fix utils tests * lint * add unit tests * remove addHeaders method * fix tests. Still need to update to use # for private vars * fix tests * minor cleanup * lint * do not use interceptors * Update client.ts * lint * Update integration.cloudbuild.yaml * fixed tests * fix null issues * Update packages/toolbox-core/src/toolbox_core/client.ts Co-authored-by: Anubhav Dhawan <[email protected]> * lint * fix tests * let tool auth tokens override client headers --------- Co-authored-by: Anubhav Dhawan <[email protected]>
1 parent edb347c commit 6611291

File tree

11 files changed

+1323
-273
lines changed

11 files changed

+1323
-273
lines changed

packages/toolbox-core/src/toolbox_core/client.ts

Lines changed: 131 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ import {
1919
type AxiosRequestConfig,
2020
type AxiosResponse,
2121
} from 'axios';
22-
import {ZodManifestSchema, createZodSchemaFromParams} from './protocol.js';
22+
import {
23+
ZodManifestSchema,
24+
createZodSchemaFromParams,
25+
ParameterSchema,
26+
} from './protocol.js';
2327
import {logApiError} from './errorUtils.js';
2428
import {ZodError} from 'zod';
25-
import {BoundParams, BoundValue, resolveValue} from './utils.js';
29+
import {BoundParams, identifyAuthRequirements, resolveValue} from './utils.js';
30+
import {AuthTokenGetters, RequiredAuthnParams} from './tool.js';
2631

2732
type Manifest = import('zod').infer<typeof ZodManifestSchema>;
2833
type ToolSchemaFromManifest = Manifest['tools'][string];
@@ -123,33 +128,52 @@ class ToolboxClient {
123128
#createToolInstance(
124129
toolName: string,
125130
toolSchema: ToolSchemaFromManifest,
131+
authTokenGetters: AuthTokenGetters = {},
126132
boundParams: BoundParams = {}
127133
): {
128134
tool: ReturnType<typeof ToolboxTool>;
135+
usedAuthKeys: Set<string>;
129136
usedBoundKeys: Set<string>;
130137
} {
131-
const toolParamNames = new Set(toolSchema.parameters.map(p => p.name));
132-
const applicableBoundParams: Record<string, BoundValue> = {};
133-
const usedBoundKeys = new Set<string>();
134-
135-
for (const key in boundParams) {
136-
if (toolParamNames.has(key)) {
137-
applicableBoundParams[key] = boundParams[key];
138-
usedBoundKeys.add(key);
138+
const params: ParameterSchema[] = [];
139+
const authParams: RequiredAuthnParams = {};
140+
const currBoundParams: BoundParams = {};
141+
142+
for (const p of toolSchema.parameters) {
143+
if (p.authSources && p.authSources.length > 0) {
144+
authParams[p.name] = p.authSources;
145+
} else if (boundParams && p.name in boundParams) {
146+
currBoundParams[p.name] = boundParams[p.name];
147+
} else {
148+
params.push(p);
139149
}
140150
}
141151

142-
const paramZodSchema = createZodSchemaFromParams(toolSchema.parameters);
152+
const [remainingAuthnParams, remainingAuthzTokens, usedAuthKeys] =
153+
identifyAuthRequirements(
154+
authParams,
155+
toolSchema.authRequired || [],
156+
authTokenGetters ? Object.keys(authTokenGetters) : []
157+
);
158+
159+
const paramZodSchema = createZodSchemaFromParams(params);
160+
143161
const tool = ToolboxTool(
144162
this.#session,
145163
this.#baseUrl,
146164
toolName,
147165
toolSchema.description,
148166
paramZodSchema,
149-
applicableBoundParams,
167+
authTokenGetters,
168+
remainingAuthnParams,
169+
remainingAuthzTokens,
170+
currBoundParams,
150171
this.#clientHeaders
151172
);
152-
return {tool, usedBoundKeys};
173+
174+
const usedBoundKeys = new Set(Object.keys(currBoundParams));
175+
176+
return {tool, usedAuthKeys, usedBoundKeys};
153177
}
154178

155179
/**
@@ -159,40 +183,59 @@ class ToolboxClient {
159183
* tool remotely.
160184
*
161185
* @param {string} name - The unique name or identifier of the tool to load.
162-
* @param {BoundParams} [boundParams] - Optional parameters to pre-bind to the tool.
186+
* @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters.
187+
* @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tool.
163188
* @returns {Promise<ReturnType<typeof ToolboxTool>>} A promise that resolves
164189
* to a ToolboxTool function, ready for execution.
165190
* @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid,
166191
* or if there's an error fetching data from the API.
167192
*/
168193
async loadTool(
169194
name: string,
170-
boundParams: BoundParams = {}
195+
authTokenGetters: AuthTokenGetters | null = {},
196+
boundParams: BoundParams | null = {}
171197
): Promise<ReturnType<typeof ToolboxTool>> {
172198
const apiPath = `/api/tool/${name}`;
173199
const manifest = await this.#fetchAndParseManifest(apiPath);
174200

175201
if (
176-
manifest.tools && // Zod ensures manifest.tools exists if schema requires it
202+
manifest.tools &&
177203
Object.prototype.hasOwnProperty.call(manifest.tools, name)
178204
) {
179205
const specificToolSchema = manifest.tools[name];
180-
const {tool, usedBoundKeys} = this.#createToolInstance(
206+
const {tool, usedAuthKeys, usedBoundKeys} = this.#createToolInstance(
181207
name,
182208
specificToolSchema,
183-
boundParams
209+
authTokenGetters || undefined,
210+
boundParams || {}
184211
);
185212

186-
const providedBoundKeys = Object.keys(boundParams);
187-
const unusedBound = providedBoundKeys.filter(
213+
const providedAuthKeys = new Set(
214+
authTokenGetters ? Object.keys(authTokenGetters) : []
215+
);
216+
const providedBoundKeys = new Set(
217+
boundParams ? Object.keys(boundParams) : []
218+
);
219+
const unusedAuth = [...providedAuthKeys].filter(
220+
key => !usedAuthKeys.has(key)
221+
);
222+
const unusedBound = [...providedBoundKeys].filter(
188223
key => !usedBoundKeys.has(key)
189224
);
190225

226+
const errorMessages: string[] = [];
227+
if (unusedAuth.length > 0) {
228+
errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`);
229+
}
191230
if (unusedBound.length > 0) {
231+
errorMessages.push(
232+
`unused bound parameters: ${unusedBound.join(', ')}`
233+
);
234+
}
235+
236+
if (errorMessages.length > 0) {
192237
throw new Error(
193-
`Validation failed for tool '${name}': unused bound parameters: ${unusedBound.join(
194-
', '
195-
)}.`
238+
`Validation failed for tool '${name}': ${errorMessages.join('; ')}.`
196239
);
197240
}
198241
return tool;
@@ -205,46 +248,95 @@ class ToolboxClient {
205248
* Asynchronously fetches a toolset and loads all tools defined within it.
206249
*
207250
* @param {string | null} [name] - Name of the toolset to load. If null or undefined, loads the default toolset.
208-
* @param {BoundParams} [boundParams] - Optional parameters to pre-bind to the tools in the toolset.
251+
* @param {AuthTokenGetters | null} [authTokenGetters] - Optional map of auth service names to token getters.
252+
* @param {BoundParams | null} [boundParams] - Optional parameters to pre-bind to the tools in the toolset.
253+
* @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.
209254
* @returns {Promise<Array<ReturnType<typeof ToolboxTool>>>} A promise that resolves
210255
* to a list of ToolboxTool functions, ready for execution.
211256
* @throws {Error} If the manifest structure is invalid or if there's an error fetching data from the API.
212257
*/
213258
async loadToolset(
214259
name?: string,
215-
boundParams: BoundParams = {}
260+
authTokenGetters: AuthTokenGetters | null = {},
261+
boundParams: BoundParams | null = {},
262+
strict = false
216263
): Promise<Array<ReturnType<typeof ToolboxTool>>> {
217264
const toolsetName = name || '';
218265
const apiPath = `/api/toolset/${toolsetName}`;
219266

220267
const manifest = await this.#fetchAndParseManifest(apiPath);
221268
const tools: Array<ReturnType<typeof ToolboxTool>> = [];
222269

223-
const providedBoundKeys = new Set(Object.keys(boundParams));
270+
const overallUsedAuthKeys: Set<string> = new Set();
224271
const overallUsedBoundParams: Set<string> = new Set();
272+
const providedAuthKeys = new Set(
273+
authTokenGetters ? Object.keys(authTokenGetters) : []
274+
);
275+
const providedBoundKeys = new Set(
276+
boundParams ? Object.keys(boundParams) : []
277+
);
225278

226279
for (const [toolName, toolSchema] of Object.entries(manifest.tools)) {
227-
const {tool, usedBoundKeys} = this.#createToolInstance(
280+
const {tool, usedAuthKeys, usedBoundKeys} = this.#createToolInstance(
228281
toolName,
229282
toolSchema,
230-
boundParams
283+
authTokenGetters || {},
284+
boundParams || {}
231285
);
232286
tools.push(tool);
233-
usedBoundKeys.forEach((key: string) => overallUsedBoundParams.add(key));
287+
288+
if (strict) {
289+
const unusedAuth = [...providedAuthKeys].filter(
290+
key => !usedAuthKeys.has(key)
291+
);
292+
const unusedBound = [...providedBoundKeys].filter(
293+
key => !usedBoundKeys.has(key)
294+
);
295+
const errorMessages: string[] = [];
296+
if (unusedAuth.length > 0) {
297+
errorMessages.push(`unused auth tokens: ${unusedAuth.join(', ')}`);
298+
}
299+
if (unusedBound.length > 0) {
300+
errorMessages.push(
301+
`unused bound parameters: ${unusedBound.join(', ')}`
302+
);
303+
}
304+
if (errorMessages.length > 0) {
305+
throw new Error(
306+
`Validation failed for tool '${toolName}': ${errorMessages.join('; ')}.`
307+
);
308+
}
309+
} else {
310+
usedAuthKeys.forEach(key => overallUsedAuthKeys.add(key));
311+
usedBoundKeys.forEach(key => overallUsedBoundParams.add(key));
312+
}
234313
}
235314

236-
const unusedBound = [...providedBoundKeys].filter(
237-
k => !overallUsedBoundParams.has(k)
238-
);
239-
if (unusedBound.length > 0) {
240-
throw new Error(
241-
`Validation failed for toolset '${
242-
name || 'default'
243-
}': unused bound parameters could not be applied to any tool: ${unusedBound.join(
244-
', '
245-
)}.`
315+
if (!strict) {
316+
const unusedAuth = [...providedAuthKeys].filter(
317+
key => !overallUsedAuthKeys.has(key)
318+
);
319+
const unusedBound = [...providedBoundKeys].filter(
320+
key => !overallUsedBoundParams.has(key)
246321
);
322+
const errorMessages: string[] = [];
323+
if (unusedAuth.length > 0) {
324+
errorMessages.push(
325+
`unused auth tokens could not be applied to any tool: ${unusedAuth.join(', ')}`
326+
);
327+
}
328+
if (unusedBound.length > 0) {
329+
errorMessages.push(
330+
`unused bound parameters could not be applied to any tool: ${unusedBound.join(', ')}`
331+
);
332+
}
333+
if (errorMessages.length > 0) {
334+
throw new Error(
335+
`Validation failed for toolset '${name || 'default'}': ${errorMessages.join('; ')}.`
336+
);
337+
}
247338
}
339+
248340
return tools;
249341
}
250342
}

0 commit comments

Comments
 (0)