@slashfi/agents-sdk 0.77.2 → 0.77.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/adk-tools.ts CHANGED
@@ -17,252 +17,10 @@
17
17
  * ```
18
18
  */
19
19
 
20
- import { z } from "zod";
21
- import { AdkError } from "./adk-error.js";
22
- import { zodToOpenAiJsonSchema } from "./call-agent-schema.js";
23
20
  import type { Adk } from "./config-store.js";
24
21
  import type { RefEntry, RegistryEntry } from "./define-config.js";
25
22
  import { defineTool } from "./define.js";
26
- import type { JsonSchema, ToolContext, ToolDefinition } from "./types.js";
27
-
28
- const objectRecordSchema = z.record(z.unknown());
29
- const sourceRegistrySchema = z
30
- .object({
31
- url: z.string().min(1).describe("Registry MCP URL."),
32
- agentPath: z.string().optional().describe("Agent path on that registry."),
33
- })
34
- .passthrough();
35
- const refScopeSchema = z
36
- .string()
37
- .optional()
38
- .describe("Config scope to operate on.");
39
- const refNameSchema = z.string().min(1).describe("Local connection name.");
40
-
41
- const refAddOperationSchema = z
42
- .object({
43
- operation: z.literal("add"),
44
- scope: refScopeSchema,
45
- ref: z
46
- .string()
47
- .min(1)
48
- .optional()
49
- .describe(
50
- "Canonical agent path, e.g. 'google-calendar'. Defaults to name when omitted.",
51
- ),
52
- name: refNameSchema
53
- .optional()
54
- .describe("Local connection name. Defaults to ref when omitted."),
55
- scheme: z
56
- .enum(["registry", "mcp", "https"])
57
- .optional()
58
- .describe(
59
- "Connection type. Usually inferred from sourceRegistry or url.",
60
- ),
61
- url: z
62
- .string()
63
- .min(1)
64
- .optional()
65
- .describe("Direct MCP/HTTPS URL. Required for direct mcp/https refs."),
66
- sourceRegistry: sourceRegistrySchema
67
- .optional()
68
- .describe(
69
- "Registry that serves this agent. Required for registry-backed refs.",
70
- ),
71
- config: objectRecordSchema
72
- .optional()
73
- .describe("Optional per-instance config."),
74
- })
75
- .passthrough()
76
- .superRefine((input, ctx) => {
77
- if (!input.ref && !input.name) {
78
- ctx.addIssue({
79
- code: z.ZodIssueCode.custom,
80
- path: ["ref"],
81
- message: "Either ref or name is required.",
82
- });
83
- }
84
- if (input.scheme === "registry" && !input.sourceRegistry?.url) {
85
- ctx.addIssue({
86
- code: z.ZodIssueCode.custom,
87
- path: ["sourceRegistry", "url"],
88
- message: "scheme=registry requires sourceRegistry.url.",
89
- });
90
- }
91
- if ((input.scheme === "mcp" || input.scheme === "https") && !input.url) {
92
- ctx.addIssue({
93
- code: z.ZodIssueCode.custom,
94
- path: ["url"],
95
- message: `scheme=${input.scheme} requires url.`,
96
- });
97
- }
98
- if (!input.url && !input.sourceRegistry?.url) {
99
- ctx.addIssue({
100
- code: z.ZodIssueCode.custom,
101
- path: ["sourceRegistry"],
102
- message:
103
- "Connection target is required: provide sourceRegistry.url for a registry ref, or url for a direct mcp/https ref.",
104
- });
105
- }
106
- });
107
-
108
- const refOperationSchemas = {
109
- add: refAddOperationSchema,
110
- remove: z
111
- .object({
112
- operation: z.literal("remove"),
113
- scope: refScopeSchema,
114
- name: refNameSchema,
115
- })
116
- .passthrough(),
117
- list: z
118
- .object({ operation: z.literal("list"), scope: refScopeSchema })
119
- .passthrough(),
120
- update: z
121
- .object({
122
- operation: z.literal("update"),
123
- scope: refScopeSchema,
124
- name: refNameSchema,
125
- ref: z.string().optional(),
126
- scheme: z.enum(["registry", "mcp", "https"]).optional(),
127
- url: z.string().optional(),
128
- sourceRegistry: sourceRegistrySchema.optional(),
129
- config: objectRecordSchema.optional(),
130
- })
131
- .passthrough(),
132
- inspect: z
133
- .object({
134
- operation: z.literal("inspect"),
135
- scope: refScopeSchema,
136
- name: refNameSchema,
137
- full: z.boolean().optional(),
138
- })
139
- .passthrough(),
140
- call: z
141
- .object({
142
- operation: z.literal("call"),
143
- scope: refScopeSchema,
144
- name: refNameSchema,
145
- tool: z.string().min(1),
146
- params: objectRecordSchema.optional(),
147
- })
148
- .passthrough(),
149
- auth: z
150
- .object({
151
- operation: z.literal("auth"),
152
- scope: refScopeSchema,
153
- name: refNameSchema,
154
- ref: z.string().optional(),
155
- apiKey: z.string().optional(),
156
- credentials: z.record(z.string()).optional(),
157
- sourceRegistry: sourceRegistrySchema.optional(),
158
- })
159
- .passthrough(),
160
- "auth-status": z
161
- .object({
162
- operation: z.literal("auth-status"),
163
- scope: refScopeSchema,
164
- name: refNameSchema,
165
- })
166
- .passthrough(),
167
- "refresh-token": z
168
- .object({
169
- operation: z.literal("refresh-token"),
170
- scope: refScopeSchema,
171
- name: refNameSchema,
172
- })
173
- .passthrough(),
174
- resources: z
175
- .object({
176
- operation: z.literal("resources"),
177
- scope: refScopeSchema,
178
- name: refNameSchema,
179
- })
180
- .passthrough(),
181
- read: z
182
- .object({
183
- operation: z.literal("read"),
184
- scope: refScopeSchema,
185
- name: refNameSchema,
186
- uris: z.array(z.string()),
187
- })
188
- .passthrough(),
189
- } as const;
190
-
191
- const refToolInputSchema = z.union([
192
- refOperationSchemas.add,
193
- refOperationSchemas.remove,
194
- refOperationSchemas.list,
195
- refOperationSchemas.update,
196
- refOperationSchemas.inspect,
197
- refOperationSchemas.call,
198
- refOperationSchemas.auth,
199
- refOperationSchemas["auth-status"],
200
- refOperationSchemas["refresh-token"],
201
- refOperationSchemas.resources,
202
- refOperationSchemas.read,
203
- ]);
204
- const refToolInputJsonSchema = zodToOpenAiJsonSchema(
205
- refToolInputSchema,
206
- ) as JsonSchema;
207
-
208
- function parseRefToolInput(
209
- input: Record<string, unknown>,
210
- ): Record<string, unknown> {
211
- const op = typeof input.operation === "string" ? input.operation : undefined;
212
- const schema =
213
- op && op in refOperationSchemas
214
- ? refOperationSchemas[op as keyof typeof refOperationSchemas]
215
- : refToolInputSchema;
216
- const result = schema.safeParse(input);
217
- if (result.success) return result.data as Record<string, unknown>;
218
-
219
- const operation = op ? `ref.${op}` : "ref";
220
- throw new AdkError({
221
- code: "TOOL_INPUT_INVALID",
222
- message: `Invalid ${operation} input`,
223
- hint: "The expected input schema is serialized in details.schema; operation-specific schema is in details.operationSchema.",
224
- details: {
225
- operation,
226
- issues: result.error.issues.map((issue) => ({
227
- path: issue.path.join("."),
228
- message: issue.message,
229
- })),
230
- received: input,
231
- schema: refToolInputJsonSchema,
232
- ...(op &&
233
- op in refOperationSchemas && {
234
- operationSchema: zodToOpenAiJsonSchema(
235
- refOperationSchemas[op as keyof typeof refOperationSchemas],
236
- ),
237
- }),
238
- },
239
- });
240
- }
241
-
242
- function withScopeSchema(
243
- schema: JsonSchema,
244
- scopeSchema: JsonSchema,
245
- ): JsonSchema {
246
- const clone = JSON.parse(JSON.stringify(schema)) as JsonSchema;
247
- const visit = (value: unknown) => {
248
- if (!value || typeof value !== "object") return;
249
- if (Array.isArray(value)) {
250
- for (const item of value) visit(item);
251
- return;
252
- }
253
-
254
- const record = value as Record<string, unknown>;
255
- const properties = record.properties as Record<string, unknown> | undefined;
256
- if (properties?.scope) {
257
- properties.scope = scopeSchema;
258
- }
259
- for (const child of Object.values(record)) {
260
- visit(child);
261
- }
262
- };
263
- visit(clone);
264
- return clone;
265
- }
23
+ import type { ToolContext, ToolDefinition } from "./types.js";
266
24
 
267
25
  export interface AdkToolsHooks<TCtx extends ToolContext = ToolContext> {
268
26
  /**
@@ -310,14 +68,97 @@ export function createAdkTools<TCtx extends ToolContext = ToolContext>(
310
68
  name: "ref",
311
69
  description:
312
70
  "Manage agent refs. Operations: add, remove, list, update, inspect, call, auth, auth-status, refresh-token, resources, read. For `add`, supply `ref` (canonical agent path, e.g. 'notion') and `name` (local identifier). If `name` is omitted on add, it defaults to `ref`. For every other operation, pass `name`.",
313
- inputSchema: withScopeSchema(refToolInputJsonSchema, scopeSchema),
71
+ inputSchema: {
72
+ type: "object" as const,
73
+ properties: {
74
+ operation: {
75
+ type: "string",
76
+ enum: [
77
+ "add",
78
+ "remove",
79
+ "list",
80
+ "update",
81
+ "inspect",
82
+ "call",
83
+ "auth",
84
+ "auth-status",
85
+ "refresh-token",
86
+ "resources",
87
+ "read",
88
+ ],
89
+ },
90
+ scope: scopeSchema,
91
+ ref: {
92
+ type: "string",
93
+ description:
94
+ "Canonical agent path on the remote registry (e.g. 'notion', 'linear', 'github'). Used by `add` to identify which agent definition to connect to. Other operations use `name` instead. If you call `add` with only `name` and no `ref`, `ref` defaults to `name`.",
95
+ },
96
+ name: {
97
+ type: "string",
98
+ description:
99
+ "Local identifier for this ref, used by all operations to look up the entry. On `add`, defaults to `ref` when omitted.",
100
+ },
101
+ scheme: {
102
+ type: "string",
103
+ description:
104
+ "Connection scheme: 'mcp' (direct MCP server), 'https' (REST proxy), or 'registry' (discovered via a registry). Auto-inferred from `url` or `sourceRegistry` when omitted.",
105
+ },
106
+ url: {
107
+ type: "string",
108
+ description:
109
+ "Direct URL to the agent (e.g. https://mcp.notion.com/mcp). Required for 'mcp' and 'https' schemes.",
110
+ },
111
+ sourceRegistry: {
112
+ type: "object",
113
+ properties: {
114
+ url: { type: "string" },
115
+ agentPath: { type: "string" },
116
+ },
117
+ description:
118
+ "When scheme is 'registry', the registry + agent path to resolve through.",
119
+ },
120
+ config: {
121
+ type: "object",
122
+ description:
123
+ "Per-instance config passed to the agent (headers, credentials, etc.). Supports `{{secret-uri}}` templates.",
124
+ },
125
+ tool: {
126
+ type: "string",
127
+ description:
128
+ "For `call` operation: the tool name on the ref to invoke.",
129
+ },
130
+ params: {
131
+ type: "object",
132
+ description: "For `call` operation: arguments to pass to the tool.",
133
+ },
134
+ full: {
135
+ type: "boolean",
136
+ description:
137
+ "For `inspect` operation: include full agent definition.",
138
+ },
139
+ uris: {
140
+ type: "array",
141
+ items: { type: "string" },
142
+ description: "For `read` operation: the resource URIs to read.",
143
+ },
144
+ apiKey: {
145
+ type: "string",
146
+ description: "For `auth` operation: pre-provisioned API key.",
147
+ },
148
+ credentials: {
149
+ type: "object",
150
+ description:
151
+ "For `auth` operation: key-value map of credential fields (keys match field names from the auth challenge).",
152
+ },
153
+ },
154
+ required: ["operation"],
155
+ },
314
156
  execute: async (input: Record<string, unknown>, ctx) => {
315
- const parsedInput = parseRefToolInput(input);
316
157
  const adk = await resolveScope(
317
- parsedInput.scope as string | undefined,
158
+ input.scope as string | undefined,
318
159
  ctx as TCtx,
319
160
  );
320
- const op = parsedInput.operation as string;
161
+ const op = input.operation as string;
321
162
 
322
163
  switch (op) {
323
164
  case "add": {
@@ -325,24 +166,18 @@ export function createAdkTools<TCtx extends ToolContext = ToolContext>(
325
166
  // other defaults to it. The stored entry always has an explicit
326
167
  // `name`, so downstream auth/callback state can distinguish the
327
168
  // canonical ref from the local connection handle.
328
- const refValue = (parsedInput.ref ?? parsedInput.name) as
329
- | string
330
- | undefined;
169
+ const refValue = (input.ref ?? input.name) as string | undefined;
331
170
  if (!refValue) {
332
171
  throw new Error(
333
172
  "ref.add: must supply either 'ref' (canonical agent path) or 'name' (local identifier); both may be the same string for the common single-instance case.",
334
173
  );
335
174
  }
336
- const nameValue = (parsedInput.name ?? refValue) as string;
337
- const entry: Record<string, unknown> = {
338
- ref: refValue,
339
- name: nameValue,
340
- };
341
- if (parsedInput.scheme) entry.scheme = parsedInput.scheme;
342
- if (parsedInput.url) entry.url = parsedInput.url;
343
- if (parsedInput.sourceRegistry)
344
- entry.sourceRegistry = parsedInput.sourceRegistry;
345
- if (parsedInput.config) entry.config = parsedInput.config;
175
+ const nameValue = (input.name ?? refValue) as string;
176
+ const entry: Record<string, unknown> = { ref: refValue, name: nameValue };
177
+ if (input.scheme) entry.scheme = input.scheme;
178
+ if (input.url) entry.url = input.url;
179
+ if (input.sourceRegistry) entry.sourceRegistry = input.sourceRegistry;
180
+ if (input.config) entry.config = input.config;
346
181
  const { security } = await adk.ref.add(entry as unknown as RefEntry);
347
182
  return {
348
183
  added: true,
@@ -352,25 +187,25 @@ export function createAdkTools<TCtx extends ToolContext = ToolContext>(
352
187
  };
353
188
  }
354
189
  case "remove":
355
- return { removed: await adk.ref.remove(parsedInput.name as string) };
190
+ return { removed: await adk.ref.remove(input.name as string) };
356
191
  case "list":
357
192
  return { refs: await adk.ref.list() };
358
193
  case "update":
359
194
  return {
360
195
  updated: await adk.ref.update(
361
- parsedInput.name as string,
362
- parsedInput as unknown as Partial<RefEntry>,
196
+ input.name as string,
197
+ input as unknown as Partial<RefEntry>,
363
198
  ),
364
199
  };
365
200
  case "inspect":
366
- return await adk.ref.inspect(parsedInput.name as string, {
367
- full: parsedInput.full as boolean,
201
+ return await adk.ref.inspect(input.name as string, {
202
+ full: input.full as boolean,
368
203
  });
369
204
  case "call":
370
205
  return await adk.ref.call(
371
- parsedInput.name as string,
372
- parsedInput.tool as string,
373
- parsedInput.params as Record<string, unknown>,
206
+ input.name as string,
207
+ input.tool as string,
208
+ input.params as Record<string, unknown>,
374
209
  );
375
210
  case "auth": {
376
211
  const authOpts: {
@@ -378,31 +213,27 @@ export function createAdkTools<TCtx extends ToolContext = ToolContext>(
378
213
  credentials?: Record<string, string>;
379
214
  stateContext?: Record<string, unknown>;
380
215
  } = {};
381
- if (parsedInput.apiKey)
382
- authOpts.apiKey = parsedInput.apiKey as string;
383
- if (parsedInput.credentials)
384
- authOpts.credentials = parsedInput.credentials as Record<
385
- string,
386
- string
387
- >;
216
+ if (input.apiKey) authOpts.apiKey = input.apiKey as string;
217
+ if (input.credentials)
218
+ authOpts.credentials = input.credentials as Record<string, string>;
388
219
  if (opts.hooks?.getAuthStateContext) {
389
220
  authOpts.stateContext = await opts.hooks.getAuthStateContext(
390
- parsedInput,
221
+ input,
391
222
  ctx as TCtx,
392
223
  );
393
224
  }
394
- return await adk.ref.auth(parsedInput.name as string, authOpts);
225
+ return await adk.ref.auth(input.name as string, authOpts);
395
226
  }
396
227
  case "auth-status":
397
- return await adk.ref.authStatus(parsedInput.name as string);
228
+ return await adk.ref.authStatus(input.name as string);
398
229
  case "refresh-token":
399
- return await adk.ref.refreshToken(parsedInput.name as string);
230
+ return await adk.ref.refreshToken(input.name as string);
400
231
  case "resources":
401
- return await adk.ref.resources(parsedInput.name as string);
232
+ return await adk.ref.resources(input.name as string);
402
233
  case "read":
403
234
  return await adk.ref.read(
404
- parsedInput.name as string,
405
- parsedInput.uris as string[],
235
+ input.name as string,
236
+ input.uris as string[],
406
237
  );
407
238
  default:
408
239
  throw new Error(`Unknown ref operation: ${op}`);
@@ -521,7 +521,73 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
521
521
  return value;
522
522
  }
523
523
 
524
+ // ─────────────────────────────────────────────────────────────────────
525
+ // Credential resolution helpers
526
+ //
527
+ // Three callsites used to inline a `tryResolve`/`canResolve` closure
528
+ // (auth, authStatus, refreshToken) and two of them duplicated the
529
+ // client_id/client_secret lookup chain verbatim. Those chains MUST stay
530
+ // symmetric — if `auth` accepts a credential source, `refreshToken` has
531
+ // to read from the same source or refresh silently no-ops on every ref
532
+ // `auth` succeeded against. Centralising it here removes that drift
533
+ // risk.
534
+ // ─────────────────────────────────────────────────────────────────────
535
+
536
+ type OAuthServerMetadata = import("./mcp-client.js").OAuthServerMetadata;
537
+
538
+ interface CredentialResolverContext {
539
+ name: string;
540
+ entry: RefEntry;
541
+ security: SecuritySchemeSummary | null;
542
+ }
543
+
544
+ /**
545
+ * Build a `tryResolve(field, oauthMetadata?)` function bound to a
546
+ * specific ref + entry + security context. Wraps the host-injected
547
+ * `resolveCredentials` callback (e.g. atlas's env/static/tenant chain
548
+ * for first-party agents). Errors propagate to the caller.
549
+ */
550
+ function makeTryResolve(ctx: CredentialResolverContext) {
551
+ return async (
552
+ field: string,
553
+ oauthMetadata?: OAuthServerMetadata | null,
554
+ ): Promise<string | null> => {
555
+ const resolve = options.resolveCredentials;
556
+ if (!resolve) return null;
557
+ return resolve({
558
+ ref: ctx.name,
559
+ field,
560
+ entry: ctx.entry,
561
+ security: ctx.security,
562
+ oauthMetadata,
563
+ });
564
+ };
565
+ }
524
566
 
567
+ /**
568
+ * Resolve OAuth client credentials (client_id + client_secret) for a
569
+ * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
570
+ * Used by both `auth` (initial OAuth flow) and `refreshToken` (token
571
+ * refresh) — must be a single function so the two paths can never
572
+ * disagree about where credentials live.
573
+ *
574
+ * Returns null when no client_id is available anywhere; caller decides
575
+ * whether to attempt dynamic registration (`auth`) or bail (`refresh`).
576
+ */
577
+ async function resolveOAuthClient(
578
+ ctx: CredentialResolverContext & { metadata?: OAuthServerMetadata | null },
579
+ ): Promise<{ clientId: string; clientSecret?: string } | null> {
580
+ const tryResolve = makeTryResolve(ctx);
581
+ const clientId =
582
+ (await tryResolve("client_id", ctx.metadata)) ??
583
+ (await readRefSecret(ctx.name, "client_id"));
584
+ if (!clientId) return null;
585
+ const clientSecret =
586
+ (await tryResolve("client_secret", ctx.metadata)) ??
587
+ (await readRefSecret(ctx.name, "client_secret")) ??
588
+ undefined;
589
+ return { clientId, ...(clientSecret && { clientSecret }) };
590
+ }
525
591
 
526
592
  const PENDING_OAUTH_PATH = "pending-oauth.json";
527
593
 
@@ -1806,12 +1872,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1806
1872
  }
1807
1873
 
1808
1874
  const configKeys = Object.keys(entry.config ?? {});
1809
- const resolve = options.resolveCredentials;
1810
-
1811
- async function canResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<boolean> {
1812
- if (!resolve || !entry) return false;
1813
- const val = await resolve({ ref: name, field, entry, security, oauthMetadata });
1814
- return val !== null;
1875
+ const tryResolveField = makeTryResolve({ name, entry, security });
1876
+ async function canResolve(
1877
+ field: string,
1878
+ oauthMetadata?: OAuthServerMetadata | null,
1879
+ ): Promise<boolean> {
1880
+ return (await tryResolveField(field, oauthMetadata)) !== null;
1815
1881
  }
1816
1882
 
1817
1883
  const fields: Record<string, CredentialField> = {};
@@ -1944,12 +2010,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1944
2010
 
1945
2011
  const status = await ref.authStatus(name);
1946
2012
  const security = status.security;
1947
- const resolve = options.resolveCredentials;
1948
-
1949
- async function tryResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<string | null> {
1950
- if (!resolve) return null;
1951
- return resolve({ ref: name, field, entry: entry!, security, oauthMetadata });
1952
- }
2013
+ const tryResolve = makeTryResolve({ name, entry, security });
1953
2014
 
1954
2015
  if (!security || security.type === "none") {
1955
2016
  return { type: "none", complete: true };
@@ -2101,11 +2162,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2101
2162
  const redirectUri = callbackUrl();
2102
2163
 
2103
2164
  // Resolve client credentials: callback → stored → dynamic registration
2104
- let clientId = await tryResolve("client_id", metadata)
2105
- ?? await readRefSecret(name, "client_id");
2106
- let clientSecret = await tryResolve("client_secret", metadata)
2107
- ?? await readRefSecret(name, "client_secret")
2108
- ?? undefined;
2165
+ const fromHelper = await resolveOAuthClient({
2166
+ name,
2167
+ entry,
2168
+ security,
2169
+ metadata,
2170
+ });
2171
+ let clientId: string | undefined = fromHelper?.clientId;
2172
+ let clientSecret: string | undefined = fromHelper?.clientSecret;
2109
2173
 
2110
2174
  if (!clientId && metadata.registration_endpoint) {
2111
2175
  const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
@@ -2365,22 +2429,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2365
2429
  const refreshToken = await readRefSecret(name, "refresh_token");
2366
2430
  if (!refreshToken) return null;
2367
2431
 
2368
- // Read client credentials
2369
- const clientId = await readRefSecret(name, "client_id");
2370
- if (!clientId) return null;
2371
- const clientSecret = await readRefSecret(name, "client_secret");
2372
-
2373
- // Get the agent's token endpoint from its security metadata
2432
+ // Resolve token endpoint + OAuth client via the host's
2433
+ // `resolveCredentials` chain. Same chain `auth` uses (see
2434
+ // `resolveOAuthClient`) kept symmetric so refresh works on every
2435
+ // ref `auth` works on, including first-party registry-hosted
2436
+ // clients whose creds live in env / tenant scope, not the user's
2437
+ // per-ref config.
2374
2438
  const entry = await ref.get(name);
2375
2439
  if (!entry) return null;
2376
2440
 
2377
- const info = await ref.inspect(name);
2378
- const security = (info as any)?.security as Record<string, unknown> | undefined;
2379
- const flows = (security?.flows as Record<string, Record<string, unknown>> | undefined);
2441
+ const status = await ref.authStatus(name);
2442
+ const security = status.security;
2443
+ const flows = security && "flows" in security
2444
+ ? (security as { flows?: Record<string, { tokenUrl?: string; refreshUrl?: string }> }).flows
2445
+ : undefined;
2380
2446
  const authCodeFlow = flows?.authorizationCode;
2381
- const tokenUrl = (authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl) as string | undefined;
2447
+ const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
2382
2448
  if (!tokenUrl) return null;
2383
2449
 
2450
+ const oauthClient = await resolveOAuthClient({ name, entry, security });
2451
+ if (!oauthClient) return null;
2452
+ const { clientId, clientSecret } = oauthClient;
2453
+
2384
2454
  // POST to the token endpoint with grant_type=refresh_token
2385
2455
  const body = new URLSearchParams({
2386
2456
  grant_type: "refresh_token",
@@ -10,7 +10,7 @@
10
10
  import { describe, expect, test } from "bun:test";
11
11
  import { createAdkTools } from "./adk-tools";
12
12
  import type { FsStore } from "./agent-definitions/config";
13
- import { createAdk, createAgentRegistry, defineAgent } from "./index";
13
+ import { createAdk } from "./index";
14
14
  import type { ToolContext } from "./types";
15
15
 
16
16
  function createMemoryFs(): FsStore {
@@ -326,54 +326,6 @@ describe("ref tool — add operation defaults ref to name", () => {
326
326
  const raw = await fs.readFile("consumer-config.json");
327
327
  expect(raw).toBeNull();
328
328
  });
329
-
330
- test("invalid add input returns schema details through registry call", async () => {
331
- const fs = createMemoryFs();
332
- const adk = createAdk(fs);
333
- const refTool = makeRefTool(adk);
334
- const registry = createAgentRegistry();
335
- registry.register(
336
- defineAgent({
337
- path: "@config",
338
- entrypoint: "Config agent",
339
- tools: [refTool],
340
- visibility: "public",
341
- }),
342
- );
343
-
344
- const response = await registry.call({
345
- action: "execute_tool",
346
- path: "@config",
347
- tool: "ref",
348
- params: {
349
- operation: "add",
350
- ref: "google-calendar",
351
- },
352
- });
353
-
354
- expect(response.success).toBe(false);
355
- if (response.success) throw new Error("expected invalid input error");
356
- expect(response.code).toBe("TOOL_INPUT_INVALID");
357
- expect(response.error).toContain("Invalid ref.add input");
358
- expect(response.details?.issues).toEqual(
359
- expect.arrayContaining([
360
- expect.objectContaining({
361
- path: "sourceRegistry",
362
- }),
363
- ]),
364
- );
365
- expect(response.details?.schema).toMatchObject({
366
- anyOf: expect.any(Array),
367
- });
368
- expect(response.details?.operationSchema).toMatchObject({
369
- type: "object",
370
- });
371
- expect(response.hint).toContain("details.schema");
372
- expect(response.details).not.toHaveProperty("examples");
373
- expect(JSON.stringify(response.details?.operationSchema)).toContain(
374
- "sourceRegistry",
375
- );
376
- });
377
329
  });
378
330
 
379
331
  describe("ref tool — auth state hook", () => {