@slashfi/agents-sdk 0.67.0 → 0.68.0
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 +3 -0
- package/dist/adk-tools.js.map +1 -1
- package/dist/cjs/adk-tools.js +3 -0
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/config-store.js +122 -23
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry-consumer.js +11 -0
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +19 -3
- package/dist/cjs/server.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/config-store.d.ts +24 -1
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +122 -23
- package/dist/config-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +11 -0
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +19 -3
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +26 -6
- 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 +3 -1
- package/src/config-store.test.ts +223 -0
- package/src/config-store.ts +158 -26
- package/src/index.ts +1 -0
- package/src/registry-consumer.ts +8 -0
- package/src/server.ts +22 -3
- package/src/types.ts +24 -6
package/src/config-store.ts
CHANGED
|
@@ -143,11 +143,29 @@ export interface OAuthResult {
|
|
|
143
143
|
clientId: string;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** A field the caller needs to collect from the user */
|
|
147
|
+
export interface AuthChallengeField {
|
|
148
|
+
/** Field key (e.g. "api_key", "token") */
|
|
149
|
+
name: string;
|
|
150
|
+
/** Human-readable label (e.g. "API Key", "DD-API-KEY") */
|
|
151
|
+
label: string;
|
|
152
|
+
/** Whether this is a secret value (should be masked in UI) */
|
|
153
|
+
secret: boolean;
|
|
154
|
+
/** Optional description / help text */
|
|
155
|
+
description?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
export interface AuthStartResult {
|
|
147
159
|
type: string;
|
|
148
160
|
complete: boolean;
|
|
149
161
|
/** For OAuth: the URL to open in the browser */
|
|
150
162
|
authorizeUrl?: string;
|
|
163
|
+
/**
|
|
164
|
+
* When complete=false and type is "apiKey" or "http",
|
|
165
|
+
* these are the fields the caller should collect from the user.
|
|
166
|
+
* The caller can render these as a form (Slack blocks, web modal, CLI prompts).
|
|
167
|
+
*/
|
|
168
|
+
fields?: AuthChallengeField[];
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
export interface AdkRefApi {
|
|
@@ -168,8 +186,14 @@ export interface AdkRefApi {
|
|
|
168
186
|
* adk.ref.authLocal() to spin up a local server and block.
|
|
169
187
|
*/
|
|
170
188
|
auth(name: string, opts?: {
|
|
171
|
-
/** For API key / bearer auth: the key/token value */
|
|
189
|
+
/** For API key / bearer auth: the key/token value (single-key shorthand) */
|
|
172
190
|
apiKey?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Credentials map for multi-field auth. Keys match the `name` field
|
|
193
|
+
* from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
|
|
194
|
+
* For single-key apiKey or http bearer, `apiKey` shorthand also works.
|
|
195
|
+
*/
|
|
196
|
+
credentials?: Record<string, string>;
|
|
173
197
|
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
174
198
|
stateContext?: Record<string, unknown>;
|
|
175
199
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
@@ -290,20 +314,18 @@ async function decryptConfigSecrets(
|
|
|
290
314
|
// ============================================
|
|
291
315
|
|
|
292
316
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
317
|
+
* Check if a tool call response indicates a 401 Unauthorized from the upstream API.
|
|
318
|
+
* Primary: httpStatus set by consumer from HTTP res.status
|
|
319
|
+
* Fallback: _httpStatus from tool result body
|
|
295
320
|
*/
|
|
296
|
-
function
|
|
321
|
+
function isUnauthorized(result: unknown): boolean {
|
|
297
322
|
if (!result || typeof result !== 'object') return false;
|
|
298
323
|
const r = result as Record<string, unknown>;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
const
|
|
303
|
-
if (
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
|
|
324
|
+
// Primary: HTTP status forwarded by the registry and set by callRegistry
|
|
325
|
+
if (r.httpStatus === 401) return true;
|
|
326
|
+
// Fallback: _httpStatus in the nested tool result body
|
|
327
|
+
const inner = r.result as Record<string, unknown> | undefined;
|
|
328
|
+
if (inner?._httpStatus === 401) return true;
|
|
307
329
|
return false;
|
|
308
330
|
}
|
|
309
331
|
|
|
@@ -894,7 +916,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
894
916
|
const result = await doCall(accessToken);
|
|
895
917
|
|
|
896
918
|
// Check if the response indicates a 401 — try refreshing the token and retry once
|
|
897
|
-
if (accessToken &&
|
|
919
|
+
if (accessToken && isUnauthorized(result)) {
|
|
898
920
|
const refreshed = await ref.refreshToken(name);
|
|
899
921
|
if (refreshed) {
|
|
900
922
|
return doCall(refreshed.accessToken);
|
|
@@ -1000,12 +1022,32 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1000
1022
|
resolvable: false,
|
|
1001
1023
|
};
|
|
1002
1024
|
} else if (security.type === "apiKey") {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
automated: false,
|
|
1006
|
-
present: configKeys.includes("api_key"),
|
|
1007
|
-
resolvable: await canResolve("api_key"),
|
|
1025
|
+
const apiKeySec = security as {
|
|
1026
|
+
name?: string; headers?: Record<string, { description?: string }>;
|
|
1008
1027
|
};
|
|
1028
|
+
const toStorageKey = (headerName: string) =>
|
|
1029
|
+
headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1030
|
+
|
|
1031
|
+
if (apiKeySec.headers && Object.keys(apiKeySec.headers).length > 0) {
|
|
1032
|
+
// Multi-key mode
|
|
1033
|
+
for (const headerName of Object.keys(apiKeySec.headers)) {
|
|
1034
|
+
const storageKey = toStorageKey(headerName);
|
|
1035
|
+
fields[storageKey] = {
|
|
1036
|
+
required: true,
|
|
1037
|
+
automated: false,
|
|
1038
|
+
present: configKeys.includes(storageKey),
|
|
1039
|
+
resolvable: await canResolve(storageKey),
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
} else {
|
|
1043
|
+
// Single-key mode (backwards compat)
|
|
1044
|
+
fields.api_key = {
|
|
1045
|
+
required: true,
|
|
1046
|
+
automated: false,
|
|
1047
|
+
present: configKeys.includes("api_key"),
|
|
1048
|
+
resolvable: await canResolve("api_key"),
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1009
1051
|
} else if (security.type === "http") {
|
|
1010
1052
|
fields.token = {
|
|
1011
1053
|
required: true,
|
|
@@ -1024,6 +1066,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1024
1066
|
|
|
1025
1067
|
async auth(name: string, opts?: {
|
|
1026
1068
|
apiKey?: string;
|
|
1069
|
+
credentials?: Record<string, string>;
|
|
1027
1070
|
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
1028
1071
|
stateContext?: Record<string, unknown>;
|
|
1029
1072
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
@@ -1047,15 +1090,92 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1047
1090
|
}
|
|
1048
1091
|
|
|
1049
1092
|
if (security.type === "apiKey") {
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1093
|
+
const apiKeySec = security as {
|
|
1094
|
+
name?: string; prefix?: string;
|
|
1095
|
+
headers?: Record<string, { description?: string }>;
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const toStorageKey = (headerName: string) =>
|
|
1099
|
+
headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
1100
|
+
|
|
1101
|
+
if (apiKeySec.headers && Object.keys(apiKeySec.headers).length > 0) {
|
|
1102
|
+
// Multi-key mode: iterate all declared headers
|
|
1103
|
+
const missingFields: AuthChallengeField[] = [];
|
|
1104
|
+
const resolvedKeys: Array<{ storageKey: string; value: string }> = [];
|
|
1105
|
+
|
|
1106
|
+
for (const [headerName, meta] of Object.entries(apiKeySec.headers)) {
|
|
1107
|
+
const storageKey = toStorageKey(headerName);
|
|
1108
|
+
const value = opts?.credentials?.[storageKey] ?? await tryResolve(storageKey);
|
|
1109
|
+
|
|
1110
|
+
if (value) {
|
|
1111
|
+
resolvedKeys.push({ storageKey, value });
|
|
1112
|
+
} else {
|
|
1113
|
+
missingFields.push({
|
|
1114
|
+
name: storageKey,
|
|
1115
|
+
label: headerName,
|
|
1116
|
+
secret: true,
|
|
1117
|
+
description: meta.description,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (missingFields.length > 0) {
|
|
1123
|
+
return { type: "apiKey", complete: false, fields: missingFields };
|
|
1124
|
+
}
|
|
1125
|
+
for (const { storageKey, value } of resolvedKeys) {
|
|
1126
|
+
await storeRefSecret(name, storageKey, value);
|
|
1127
|
+
}
|
|
1128
|
+
return { type: "apiKey", complete: true };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Single-key mode (backwards compat)
|
|
1132
|
+
const key = opts?.credentials?.["api_key"] ?? opts?.apiKey ?? await tryResolve("api_key");
|
|
1133
|
+
if (!key) {
|
|
1134
|
+
return {
|
|
1135
|
+
type: "apiKey",
|
|
1136
|
+
complete: false,
|
|
1137
|
+
fields: [{
|
|
1138
|
+
name: "api_key",
|
|
1139
|
+
label: apiKeySec.name ?? "API Key",
|
|
1140
|
+
secret: true,
|
|
1141
|
+
description: apiKeySec.prefix
|
|
1142
|
+
? `Value sent as "${apiKeySec.prefix} <key>"`
|
|
1143
|
+
: undefined,
|
|
1144
|
+
}],
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1052
1147
|
await storeRefSecret(name, "api_key", key);
|
|
1053
1148
|
return { type: "apiKey", complete: true };
|
|
1054
1149
|
}
|
|
1055
1150
|
|
|
1056
1151
|
if (security.type === "http") {
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1152
|
+
const httpSec = security as { scheme?: string };
|
|
1153
|
+
const isBasic = httpSec.scheme === "basic";
|
|
1154
|
+
|
|
1155
|
+
if (isBasic) {
|
|
1156
|
+
const username = opts?.credentials?.["username"] ?? await tryResolve("username");
|
|
1157
|
+
const password = opts?.credentials?.["password"] ?? await tryResolve("password");
|
|
1158
|
+
if (!username || !password) {
|
|
1159
|
+
const missingFields: AuthChallengeField[] = [];
|
|
1160
|
+
if (!username) missingFields.push({ name: "username", label: "Username", secret: false });
|
|
1161
|
+
if (!password) missingFields.push({ name: "password", label: "Password", secret: true });
|
|
1162
|
+
return { type: "http", complete: false, fields: missingFields };
|
|
1163
|
+
}
|
|
1164
|
+
// Store as base64 encoded basic auth token
|
|
1165
|
+
const token = btoa(`${username}:${password}`);
|
|
1166
|
+
await storeRefSecret(name, "token", token);
|
|
1167
|
+
return { type: "http", complete: true };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Bearer token
|
|
1171
|
+
const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
|
|
1172
|
+
if (!token) {
|
|
1173
|
+
return {
|
|
1174
|
+
type: "http",
|
|
1175
|
+
complete: false,
|
|
1176
|
+
fields: [{ name: "token", label: "Bearer Token", secret: true }],
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1059
1179
|
await storeRefSecret(name, "token", token);
|
|
1060
1180
|
return { type: "http", complete: true };
|
|
1061
1181
|
}
|
|
@@ -1064,7 +1184,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1064
1184
|
const flows = (security as { flows?: { authorizationCode?: { authorizationUrl?: string; tokenUrl?: string } } }).flows;
|
|
1065
1185
|
const authCodeFlow = flows?.authorizationCode;
|
|
1066
1186
|
if (!authCodeFlow?.authorizationUrl) {
|
|
1067
|
-
return {
|
|
1187
|
+
return {
|
|
1188
|
+
type: "oauth2",
|
|
1189
|
+
complete: false,
|
|
1190
|
+
fields: [
|
|
1191
|
+
{ name: "client_id", label: "Client ID", secret: false },
|
|
1192
|
+
{ name: "client_secret", label: "Client Secret", secret: true },
|
|
1193
|
+
],
|
|
1194
|
+
};
|
|
1068
1195
|
}
|
|
1069
1196
|
|
|
1070
1197
|
const authUrl = authCodeFlow.authorizationUrl;
|
|
@@ -1118,9 +1245,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1118
1245
|
}
|
|
1119
1246
|
|
|
1120
1247
|
if (!clientId) {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
)
|
|
1248
|
+
// Return fields telling the caller what OAuth credentials to provide
|
|
1249
|
+
const missingFields: AuthChallengeField[] = [];
|
|
1250
|
+
if (!clientId) {
|
|
1251
|
+
missingFields.push({ name: "client_id", label: "Client ID", secret: false });
|
|
1252
|
+
}
|
|
1253
|
+
// Always ask for client_secret alongside client_id — most providers need it
|
|
1254
|
+
missingFields.push({ name: "client_secret", label: "Client Secret", secret: true });
|
|
1255
|
+
return { type: "oauth2", complete: false, fields: missingFields };
|
|
1124
1256
|
}
|
|
1125
1257
|
|
|
1126
1258
|
// State ties the callback back to this ref. Encode as base64 JSON
|
package/src/index.ts
CHANGED
package/src/registry-consumer.ts
CHANGED
|
@@ -727,6 +727,14 @@ export async function createRegistryConsumer(
|
|
|
727
727
|
});
|
|
728
728
|
|
|
729
729
|
if (!res.ok) {
|
|
730
|
+
// Upstream 401 — return structured response so ref.call() can refresh + retry
|
|
731
|
+
if (res.status === 401) {
|
|
732
|
+
// Still try to parse the body for context
|
|
733
|
+
const body = await res.text().catch(() => "");
|
|
734
|
+
let parsed: Record<string, unknown> = { success: false, error: "unauthorized" };
|
|
735
|
+
try { parsed = JSON.parse(body); } catch {}
|
|
736
|
+
return { ...parsed, success: false, httpStatus: 401 } as unknown as CallAgentResponse;
|
|
737
|
+
}
|
|
730
738
|
const text = await res.text().catch(() => "unknown error");
|
|
731
739
|
throw new Error(
|
|
732
740
|
`Registry call failed (${registry.url}): ${res.status} ${text}`,
|
package/src/server.ts
CHANGED
|
@@ -554,6 +554,19 @@ export function createAgentServer(
|
|
|
554
554
|
async function handleJsonRpc(
|
|
555
555
|
request: JsonRpcRequest,
|
|
556
556
|
auth: ResolvedAuth | null,
|
|
557
|
+
): Promise<{ rpc: JsonRpcResponse; httpResponse?: { status: number } }> {
|
|
558
|
+
const rpc = await handleJsonRpcInner(request, auth);
|
|
559
|
+
// Extract upstream HTTP status from tool results (set by REST proxy handlers)
|
|
560
|
+
const httpStatus = (rpc as any)?.result?._httpStatus as number | undefined;
|
|
561
|
+
return {
|
|
562
|
+
rpc,
|
|
563
|
+
...(httpStatus ? { httpResponse: { status: httpStatus } } : {}),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function handleJsonRpcInner(
|
|
568
|
+
request: JsonRpcRequest,
|
|
569
|
+
auth: ResolvedAuth | null,
|
|
557
570
|
): Promise<JsonRpcResponse> {
|
|
558
571
|
switch (request.method) {
|
|
559
572
|
case "initialize":
|
|
@@ -662,7 +675,11 @@ export function createAgentServer(
|
|
|
662
675
|
}
|
|
663
676
|
|
|
664
677
|
const result = await registry.call(req);
|
|
665
|
-
|
|
678
|
+
const mcp = mcpResult(result);
|
|
679
|
+
// Preserve upstream HTTP status from tool execution (e.g. REST proxy 401)
|
|
680
|
+
const upstreamStatus = (result as any)?.result?._httpStatus;
|
|
681
|
+
if (upstreamStatus) (mcp as any)._httpStatus = upstreamStatus;
|
|
682
|
+
return mcp;
|
|
666
683
|
}
|
|
667
684
|
|
|
668
685
|
case "list_agents": {
|
|
@@ -1109,8 +1126,10 @@ export function createAgentServer(
|
|
|
1109
1126
|
// ── POST / → MCP JSON-RPC ──
|
|
1110
1127
|
if (path === "/" && req.method === "POST") {
|
|
1111
1128
|
const body = (await req.json()) as JsonRpcRequest;
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1129
|
+
const { rpc, httpResponse } = await handleJsonRpc(body, effectiveAuth);
|
|
1130
|
+
const status = httpResponse?.status ?? 200;
|
|
1131
|
+
const res = jsonResponse(rpc, status);
|
|
1132
|
+
return cors ? addCors(res) : res;
|
|
1114
1133
|
}
|
|
1115
1134
|
|
|
1116
1135
|
// ── POST /oauth/token → OAuth2 token exchange ──
|
package/src/types.ts
CHANGED
|
@@ -204,17 +204,35 @@ export interface OAuth2SecurityScheme {
|
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* API key authentication.
|
|
207
|
-
* Used by agents that wrap APIs using
|
|
207
|
+
* Used by agents that wrap APIs using one or more keys
|
|
208
208
|
* (e.g. OpenAI, Anthropic, Stripe, Datadog).
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* // Single key (backwards-compatible)
|
|
212
|
+
* security: { type: 'apiKey', in: 'header', name: 'Authorization', prefix: 'Bearer' }
|
|
213
|
+
*
|
|
214
|
+
* // Multiple keys (e.g. Datadog)
|
|
215
|
+
* security: {
|
|
216
|
+
* type: 'apiKey',
|
|
217
|
+
* headers: {
|
|
218
|
+
* 'DD-API-KEY': { description: 'Your Datadog API key' },
|
|
219
|
+
* 'DD-APPLICATION-KEY': { description: 'Your Datadog application key' },
|
|
220
|
+
* }
|
|
221
|
+
* }
|
|
209
222
|
*/
|
|
210
223
|
export interface ApiKeySecurityScheme {
|
|
211
224
|
type: "apiKey";
|
|
212
|
-
/** Where the key is sent */
|
|
213
|
-
in
|
|
214
|
-
/** Header or query parameter name (e.g. "X-API-Key"
|
|
215
|
-
name
|
|
216
|
-
/** Optional prefix (e.g. "Bearer"
|
|
225
|
+
/** Where the key is sent (single-key mode) */
|
|
226
|
+
in?: "header" | "query";
|
|
227
|
+
/** Header or query parameter name (single-key mode, e.g. "X-API-Key") */
|
|
228
|
+
name?: string;
|
|
229
|
+
/** Optional prefix (single-key mode, e.g. "Bearer") */
|
|
217
230
|
prefix?: string;
|
|
231
|
+
/**
|
|
232
|
+
* Named headers the user must provide values for (multi-key mode).
|
|
233
|
+
* When present, this is the source of truth — `in`/`name`/`prefix` are ignored.
|
|
234
|
+
*/
|
|
235
|
+
headers?: Record<string, { description?: string }>;
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
/**
|