@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/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +104 -253
- package/dist/adk-tools.js.map +1 -1
- package/dist/cjs/adk-tools.js +104 -253
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/config-store.js +68 -26
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/registry.js +2 -33
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +68 -26
- package/dist/config-store.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -33
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adk-tools.ts +113 -282
- package/src/config-store.ts +97 -27
- package/src/ref-naming.test.ts +1 -49
- package/src/registry.ts +2 -40
- package/src/types.ts +6 -21
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 {
|
|
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:
|
|
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
|
-
|
|
158
|
+
input.scope as string | undefined,
|
|
318
159
|
ctx as TCtx,
|
|
319
160
|
);
|
|
320
|
-
const op =
|
|
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 = (
|
|
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 = (
|
|
337
|
-
const entry: Record<string, unknown> = {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
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(
|
|
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
|
-
|
|
362
|
-
|
|
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(
|
|
367
|
-
full:
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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 (
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
221
|
+
input,
|
|
391
222
|
ctx as TCtx,
|
|
392
223
|
);
|
|
393
224
|
}
|
|
394
|
-
return await adk.ref.auth(
|
|
225
|
+
return await adk.ref.auth(input.name as string, authOpts);
|
|
395
226
|
}
|
|
396
227
|
case "auth-status":
|
|
397
|
-
return await adk.ref.authStatus(
|
|
228
|
+
return await adk.ref.authStatus(input.name as string);
|
|
398
229
|
case "refresh-token":
|
|
399
|
-
return await adk.ref.refreshToken(
|
|
230
|
+
return await adk.ref.refreshToken(input.name as string);
|
|
400
231
|
case "resources":
|
|
401
|
-
return await adk.ref.resources(
|
|
232
|
+
return await adk.ref.resources(input.name as string);
|
|
402
233
|
case "read":
|
|
403
234
|
return await adk.ref.read(
|
|
404
|
-
|
|
405
|
-
|
|
235
|
+
input.name as string,
|
|
236
|
+
input.uris as string[],
|
|
406
237
|
);
|
|
407
238
|
default:
|
|
408
239
|
throw new Error(`Unknown ref operation: ${op}`);
|
package/src/config-store.ts
CHANGED
|
@@ -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
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
return
|
|
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
|
|
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
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
-
//
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
//
|
|
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
|
|
2378
|
-
const security =
|
|
2379
|
-
const flows =
|
|
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 =
|
|
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",
|
package/src/ref-naming.test.ts
CHANGED
|
@@ -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
|
|
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", () => {
|